C语言实现二叉树链式结构的示例详解

目录
  • 前言
  • 1. 链式二叉树结构
  • 2. 二叉树的遍历
    • 2.1 前序遍历
    • 2.2 中序遍历
    • 2.3 后序遍历
    • 2.4 层序遍历
  • 3. 常见功能
    • 3.1 二叉树结点个数
    • 3.2 二叉树叶子结点个数
    • 3.3 第K层结点的个数
    • 3.4 二叉树的深度
    • 3.5 判断是不是树是不是完全二叉树
    • 3.6 在二叉树中查找值为x的结点
    • 3.7 拿到每一层的数据
  • 4. 二叉树的创建和销毁
    • 4.1 二叉树的创建
    • 4.2 二叉树的销毁

前言

前面我们已经对堆进行学习,堆就是一个顺序结构的二叉树,把数组看成二叉树,下面一起学习一下链式结构的二叉树,这里是用递归实现功能

1. 链式二叉树结构

typedef char BTDataType;
typedef struct BinaryTreeNode
{
	BTDataType data;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}BTNode;

2. 二叉树的遍历

首先,我要说明一下递归实现;递归实现一般分为三个步骤(递归三要素):初步明确函数功能,限制条件,找到实现此功能的等式

单项递归和二叉树递归(多项递归)的区别?

单项递归并没有分支,多项递归是有分支的,这就意味着二叉树更看中整体,单项递归更看重分治。

单项递归和二叉树递归的共同点?

都是分治思想,子问题再分子问题再分子问题的思想

2.1 前序遍历

思想:把树看成整体:根、左子树、右子树,先遍历根再走左子树再走右子树

void BinaryTreePrevOrder(BTNode* root)
{
    //根的情况(到底的限制条件)
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}

	printf("%c ", root->data);
	BinaryTreePrevOrder(root->left);
	BinaryTreePrevOrder(root->right);
}

2.2 中序遍历

思想:把树看成整体:根、左子树、右子树,先遍历左子树再走根再走右子树

void BinaryTreeInOrder(BTNode* root)
{
    //根的情况(到底的限制条件)
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}

	BinaryTreeInOrder(root->left);
	printf("%c ", root->data);
	BinaryTreeInOrder(root->right);
}

2.3 后序遍历

void BinaryTreePostOrder(BTNode* root)
{
    //根的情况(到底的限制条件)
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}

	BinaryTreePostOrder(root->left);
	BinaryTreePostOrder(root->right);
	printf("%c ", root->data);
}

2.4 层序遍历

思想:出上一层的同时带着下一层入队列

//链式队列的结构
typedef struct BinaryTreeNode* QueueDataType;
typedef struct QueueNode
{
	QueueDataType data;
	struct QueueNode* next;
}QueueNode;
//因为要直接得到队头的元素和队尾的元素
typedef struct QueueLinkList
{
	QueueNode* head; //队头
	QueueNode* tail; //队尾
	int size; //元素总数
}QLL;
//队列初始化
void QLLInit(QLL* queue)
{
	assert(queue);

	queue->head = NULL;
	queue->tail = NULL;
	queue->size = 0;
}
//进队
void QLLPush(QLL* queue, QueueDataType x)
{
	assert(queue);

	QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));
	if (newnode == NULL)
	{
		perror("QLLPush:malloc is failed!\n");
		exit(-1);
	}

	newnode->data = x;
	newnode->next = NULL;

	if (queue->head == NULL)
	{
		queue->head = queue->tail = newnode;
	}
	else
	{
		queue->tail->next = newnode;
		queue->tail = newnode;
	}

	queue->size++;
}
//出队
void QLLPop(QLL* queue)
{
	assert(queue != NULL);
	assert(QLLEmpty(queue) != true);

	//只有一个结点时
	if (queue->head->next == NULL)
	{
		free(queue->head); //free的是这个结点的空间,并不是指针
		queue->head = queue->tail = NULL;
	}
	else
	{
		//通常情况
		QueueNode* del = queue->head;
		queue->head = queue->head->next;
		free(del);
		//无需置空
	}

	queue->size--;
}
//拿取队头数据
QueueDataType QLLFront(QLL* queue)
{
	assert(queue != NULL);
	assert(QLLEmpty(queue) != true);

	return queue->head->data;
}
//判空
bool QLLEmpty(QLL* queue)
{
	assert(queue);

	//return queue->size == 0;
	return queue->head == NULL && queue->tail == NULL;
}
//销毁
void QLLDestroy(QLL* queue)
{
	assert(queue);

	QueueNode* cur = queue->head;
	while (cur != NULL)
	{
		QueueNode* del = cur;
		cur = cur->next;
		free(del);
		del = NULL;
	}

	queue->head = queue->tail = NULL;
	queue->size = 0;
}

//层序遍历实现
void BinaryTreeLevelOrder(BTNode* root)
{
	QLL queue;
	QLLInit(&queue);
    //根先入队列
	if (root != NULL) {
		QLLPush(&queue, root);
	}
	//队列不为NULL的时候进行出队头带下层数据入队操作
	while (QLLEmpty(&queue) != true)
	{
        //出队头操作
		BTNode* front = QLLFront(&queue);
		printf("%c ", front->data);
		QLLPop(&queue);
        //带下一层进队
		if (front->left != NULL)
		{
			QLLPush(&queue, front->left);
		}
		if (front->right != NULL)
		{
			QLLPush(&queue, front->right);
		}
	}
	printf("\n");
	QLLDestroy(&queue);
}

说明:为什么递归不画图来解决呢?

多项递归画图是很难理解的,因为他不是我们逻辑上想的,就拿前序遍历来说,首先是根,再遍历左子树再遍历右子树这样循环来走,但是在实际递归中逻辑是左子树走到底,直到NULL时返回访问右子树,如果说是画图是理解不了二叉树递归的,这里我们就要扣住树的结构:根、左子树、右子树,这样是我们逻辑上的实现,并不是实际中的过程实现,这里我需要说明一下,画图是为了在原有基础上来进行纠错,这里纠正的错也是和根的限制条件有关,这里我还会出几期二叉树的相关练习,到时候希望大佬们看看就能理解了二叉树递归!

3. 常见功能

3.1 二叉树结点个数

递归三要素解决问题

  • 首先二叉树想到整体结构:根、左子树、右子树
  • 函数功能:求二叉树结点的个数
  • 限制条件:根为NULL的时候就不是一个结点(起初的结束条件:针对根来说)
  • 等式:计算左子树中的结点个数和右子树的结点个数,最后加上根这个结点
int BinaryTreeSize(BTNode* root)
{
	if (root == NULL) {
		return 0;
	}

	return BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}

代码分析

上述列出来的思路就是实现思路,这里注意的是树的整体结构,我一直扣的就是整体结构,等式中写的也是整体结构的逻辑;这里来看代码很简单就是根和左子树和右子树结构

为什么不写子结构:根、左孩子、右孩子?

原因就是如果写成子结构的话就不是整体,而是把整体分为多个相同的结构来讨论,这里就不是整体观念就很容易陷进去,为什么二叉树递归难,难就难在你没扣住整体,而是扣住的是子结构,如果扣住子结构那就很容易陷进去,只要陷进去了就不是我们自己想的逻辑,而是实际递归的过程逻辑,实际递归的过程逻辑和我们想的逻辑有很大的区别

为什么首先要有个前提:树的结构:根、左子树、右子树?

原因很简单,我们考虑整体就少不了这个结构,这是我们首先要考虑的问题;另外也是因为这里三要素中的实现是离不开这个整体结构的,如果离开了整体结构就又被陷进去了

限制条件是怎么来限制的?

首先我们考虑的结构就是树的整体结构:根、左子树、右子树,我们不可能是来对左右子树来限制吧,因为左右子树中有很多结点,从整体上来说是考虑不到的,另外你只要考虑左右子树中的结点恭喜你,你又被陷进去出不来了哈哈,所以这里的限制条件是针对根来讲的:也就是根的起初的结束条件以及和题意的联系

3.2 二叉树叶子结点个数

递归三要素解决问题

  • 前提:树的结构:根、左子树、右子树
  • 函数功能:求二叉树叶子节点的个数
  • 限制条件:root=NULL的时候就不是叶子结点,根的左右孩子是空的时候但根不是空的时候就是叶子结点
  • 等式:在左右子树中找叶子节点,左右子树中的叶子结点之和也就是树的叶子结点
int BinaryTreeLeafSize(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}

	if (root->left == NULL && root->right == NULL)
	{
		return 1;
	}

	return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}

代码分析

3.3 第K层结点的个数

递归三要素解决问题

  • 前提:树的结构:根、左子树、右子树
  • 函数功能:求第K层结点的个数
  • 限制条件:root=NULL(起初的结束条件),根所处的是第一层所以K=1的时候返回1(题意结束条件)
  • 等式:在左右子树的第k-1层中的结点个数(因为第一层是根,所以第K-1层才是我们要求的第K层)
int BinaryTreeLevelKSize(BTNode* root, int k)
{
	if (root == NULL)
	{
		return 0;
	}

	if (k == 1)
	{
		return 1;
	}

	return BinaryTreeLevelKSize(root->left, k - 1)
		+ BinaryTreeLevelKSize(root->right, k - 1);
}

代码分析

3.4 二叉树的深度

递归三要素解决问题

  • 前提:树的结构:根、左子树、右子树
  • 函数功能:求树的深度
  • 限制条件:根为NULL时结束
  • 等式:树的根是第一层,那么我们只用计算出左子树和右子树的哪个深度大就再加上1(根的深度)就是树的深度
int BinaryTreeDepth(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}

	int LeftTreeDepth = BinaryTreeDepth(root->left);
	int RightTreeDepth = BinaryTreeDepth(root->right);

	if (LeftTreeDepth > RightTreeDepth) {
		return LeftTreeDepth + 1;
	}
	else {
		return RightTreeDepth + 1;
	}
}

代码分析

没进行优化的代码:

int BinaryTreeDepth(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	if (BinaryTreeDepth(root->left) > BinaryTreeDepth(root->right)) {
		return BinaryTreeDepth(root->left) + 1;
	}
	else {
		return BinaryTreeDepth(root->right) + 1;
	}
}

这个代码也是对的,但是时间复杂就要多了1倍,因为判断中用到递归了,找到了并没有记录深度,也就进入判断中的递归,再此递归一次,这样时间复杂度就增了1倍。

3.5 判断是不是树是不是完全二叉树

思路:

完全二叉树的性质:前K-1层是满二叉树,最后一层是从左到右是连续的

思路:用层序遍历来解决,出上一层的同时带下一层的数据,知道遇到NULL的时候就要进行判断队列中是不是还有不为NULL的值,如果有就不是完全二叉树,没有则是

bool BinaryTreeComplete(BTNode* root)
{
	QLL queue;
	QLLInit(&queue);
	if (root != NULL)
	{
		QLLPush(&queue, root);
	}

	//拿到每层的
	while (QLLEmpty(&queue) != true)
	{
		BTNode* front = QLLFront(&queue);
		QLLPop(&queue);

		//当这层遇到NULL的时候进行判断
		if (front == NULL)
		{
			break;
		}
		else
		{
			QLLPush(&queue, front->left);
			QLLPush(&queue, front->right);
		}
	}

	//出到NULL进行检查
	//如果后面有非NULL就不是完全二叉树
	while (QLLEmpty(&queue) != true)
	{
		BTNode* front = QLLFront(&queue);
		QLLPop(&queue);
		//不为NULL说明最后一层不是连续的
		if (front != NULL)
		{
			QLLDestroy(&queue);
			return false;
		}
	}

	QLLDestroy(&queue);
	return true;
}

3.6 在二叉树中查找值为x的结点

递归三要素解决问题

前提:树的结构:根、左子树、右子树

函数功能: 在二叉树中查找值为x的结点

限制条件:root=NULL时结束,root->val=x时找到了就结束

等式:在根里面找,在左子树和右子树里面找

BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
	{
		return NULL;
	}

	if (root->data == x)
	{
		return root;
	}

	BTNode* ret1 = BinaryTreeFind(root->left, x);
	if (ret1 != NULL)
	{
		return ret1;
	}

	BTNode* ret2 = BinaryTreeFind(root->right, x);
	if (ret2 != NULL)
	{
		return ret2;
	}

	return NULL;
}

代码分析

错误列举

BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
	{
		return NULL;
	}

	if (root->data == x)
	{
		return root;
    }

	return BinaryTreeFind(root->left, x) || BinaryTreeFind(root->right, x);
}

为什么逻辑上是对的,但是是错的?

最后的return的意思翻译过来就是在左子树里面找,找到了就返回,不进右子树,如果左子树中没找到就进右子树,找到了返回,如果都没找到就直接返回NULL;逻辑上是对的,但是呢,这里我们返回的是指针,指针的的关系不能用逻辑关系来表达,所以是错的

3.7 拿到每一层的数据

思路

也是围绕层序遍历来写:记录每一层的结点树来出队列就行了,这里也是层序遍历的知识是主要的,就不再进行讨论了

void EveryLayer(BTNode* root)
{
	QLL queue;
	int levelCount = 0;
	QLLInit(&queue);
	if (root != NULL) {
		QLLPush(&queue, root);
		//第一层就是一个数据
		levelCount = 1;
	}

	while (QLLEmpty(&queue) != true)
	{
		while (levelCount--)
		{
			BTNode* front = QLLFront(&queue);
			printf("%c ", front->data);
			QLLPop(&queue);
			if (front->left != NULL)
			{
				QLLPush(&queue, front->left);
			}
			if (front->right != NULL)
			{
				QLLPush(&queue, front->right);
			}
		}
		//下一层的个数
		levelCount = QLLSize(&queue);
		printf("\n");
	}

	QLLDestroy(&queue);
}

结合上述题就很容易看出实际上我们写代码来划分的话也是树的整体结构:根、左子树、右子树,时刻把握着树的整体结构,这个结构充分体现在等式中,同时也影响到了限制条件,限制条件中只用管根的结束条件以及形成条件,其他的不用管,这就是在代码中实现的过程,这里我就不在画图,觉得这个过程不能实现我说的对应的功能的时候你就画图去尝试理解一下,谢谢

4. 二叉树的创建和销毁

4.1 二叉树的创建

思路:

这里用到前序遍历来创建,也就是数组的元素按个放进根的数据域中

限制条件:就是当元素为#,代表的是二叉树中的NULL

BTNode* BinaryTreeCreate(BTDataType* a, int* pi)
{
	//形成条件
	if (a[(*pi)] == '#')
	{
		(*pi)++;
		return NULL;
	}

	BTNode* root = (BTNode*)malloc(sizeof(BTNode));
	if (root == NULL)
	{
		perror("BinaryTreeCreate: malloc is failed!\n");
		exit(-1);
	}
	//根
	root->data = a[(*pi)++];

	//左右子树
	root->left = BinaryTreeCreate(a, pi);
	root->right = BinaryTreeCreate(a, pi);

	return root;
}
void Test2()
{
	char str[] = { 'A','B','D','#','#','E','#','H','#','#','C','F','#','#','G','#','#' };

	int i = 0;
	BTNode* root = BinaryTreeCreate(str, &i);
}

4.2 二叉树的销毁

//二级指针
void BinaryTreeDestory(BTNode** root)
{
	if ((*root) == NULL)
	{
		return;
	}
	BinaryTreeDestory(&((*root)->left));
	BinaryTreeDestory(&((*root)->right));
	free((*root));
	*root = NULL;
}
void FirstPointBinaryTreeDestory(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}

	FirstPointBinaryTreeDestory(root->left);
	FirstPointBinaryTreeDestory(root->right);
	free(root);
	//root = NULL;(没必要)
}//需要说明的是用这个函数调用后要对root置空

为什么采用后序遍历来销毁二叉树?

因为后序遍历最开始走到的就是左子树的最后一层,然后逐次向上销毁,并不会影响每个结点的指向;如果采用前序遍历呢?采用前序遍历上来就是free掉了根结点,就找到不到这个根结点的左右孩子了

以上就是C语言实现二叉树链式结构的示例详解的详细内容,更多关于C语言二叉树链式结构的资料请关注我们其它相关文章!

(0)

相关推荐

  • C语言 二叉树的链式存储实例

    二叉树的链式存储 实现二叉树的基本操作:建立.遍历.计算深度.结点数.叶子数等. 输入C,先序创建二叉树,#表示空节点: 输入H:计算二叉树的高度: 输入L:计算二叉树的叶子个数: 输入N:计算二叉树节点总个数: 输入1:先序遍历二叉树: 输入2:中序遍历二叉树: 输入3:后续遍历二叉树: 输入F:查找值=x的节点的个数: 输入P:以缩格文本形式输出所有节点. 很简单就不需要多解释了,代码贴上 #include <stdio.h> #include <stdlib.h> #incl

  • C语言 链式二叉树结构详解原理

    目录 前言 二叉树节点声明 二叉树的遍历 构建二叉树 1.前序遍历 2.中序遍历 3.后序遍历 二叉树节点的个数 二叉树叶子节点的个数 二叉树第K层节点个数 二叉树的高度/深度 二叉树查找值为x的节点 整体代码 前言 二叉树不同于顺序表,一颗普通的二叉树是没有增删改查的意义.普通的二叉树用来存储数据是不方便的.但是二叉树的一些基本实现结构,例如前序遍历,中序遍历...等等都是对我们学习更深层次的二叉树打下夯实的基础. 二叉树节点声明 typedef char BTDataType; typede

  • C语言详解实现链式二叉树的遍历与相关接口

    目录 前言 一.二叉树的链式结构 二.二叉树的遍历方式 1.1 遍历方式的规则 1.2 前序遍历 1.3 中序遍历 1.4 后序遍历 1.5 层序遍历 三.二叉树的相关接口实现 3.1 二叉树节点个数 3.2 二叉树叶子节点个数 3.3 二叉树第 k 层节点个数 3.4 二叉树的深度(高度) 3.5 二叉树查找值为 x 的节点 3.6 总结 & 注意 四.二叉树的创建和销毁 4.1 通过前序遍历的字符串来构建二叉树 4.2 二叉树销毁 4.3 判断二叉树是否是完全二叉树 前言 二叉树的顺序结构就

  • C语言实现二叉树链式结构的示例详解

    目录 前言 1. 链式二叉树结构 2. 二叉树的遍历 2.1 前序遍历 2.2 中序遍历 2.3 后序遍历 2.4 层序遍历 3. 常见功能 3.1 二叉树结点个数 3.2 二叉树叶子结点个数 3.3 第K层结点的个数 3.4 二叉树的深度 3.5 判断是不是树是不是完全二叉树 3.6 在二叉树中查找值为x的结点 3.7 拿到每一层的数据 4. 二叉树的创建和销毁 4.1 二叉树的创建 4.2 二叉树的销毁 前言 前面我们已经对堆进行学习,堆就是一个顺序结构的二叉树,把数组看成二叉树,下面一起学

  • C语言线性表的链式表示及实现详解

    目录 前言 代码实现 1. 单链表的结点构造 2. 构造一个空的头结点 3. 对线性表进行赋值 4.对线性表进行销毁 5.对线性表进行重置 6.判断线性表是否为空 7.获取线性表的长度 8.获取线性表某一位置对应的元素 9.在线性表某一位置插入元素 10.删除线性表某一位置的元素 11.求线性表某一元素的前驱 12.求线性表某一元素的后继 13.打印线性表 运行结果演示 源码 前言 线性表的顺序表示指的是用一组地址连续的存储单元依次存储线性表的数据元素,而线性表的链式存储特点则是用一组任意的存储

  • PHP实现链式操作的原理详解

    在一个类中有多个方法,当你实例化这个类,并调用方法时只能一个一个调用,类似: db.php <?php class db { public function where() { //code here } public function order() { //code here } public function limit() { //code here } } index.php <?php $db = new db(); $db->where(); $db->order()

  • C语言实现顺序表的基本操作的示例详解

    目录 一.认识顺序表 1.线性表 2.顺序表的概念及结构 二.顺序表的基本操作(接口实现) 1.初始化顺序表 2.打印顺序表 3.尾插 4.尾删 5.扩容 6.头插 7.头删 8.任意位置插入 9.任意位置删除 10.查找某个数的位置 三.顺序表演示及代码(含源码) 1.演示效果 2.完整源代码 一.认识顺序表 1.线性表 线性表是n个具有相同特性的数据元素的有限序列,线性表是一种在实际中广泛使用的数据结构,常见的线性表有顺序表.链表.栈.队列.字符串……线性表在逻辑上是线性结构,也就是说是一条

  • Go语言基础变量的声明及初始化示例详解

    目录 一.概述 二.声明变量 三.编译器推导类型的格式[一定要赋值] 四.短变量声明并初始化 五.匿名变量--没有名字的变量 六.注意 七.案例 一.概述 变量的功能是存储用户的数据 二.声明变量 Go语言的每一个变量都拥有自己的类型,必须经过声明才能开始用 变量的声明格式: var <变量名称> [变量类型] var a int //声明一个整型类型的变量,可以保存整数数值 var b string //声明一个字符串类型的变量 var c float32 //声明一个32位浮点切片类型的变

  • Go语言基础if条件语句用法及示例详解

    目录 概述 语法 格式规则 概述 条件语句需要开发者通过指定一个或多个条件 并通过测试条件是否为 true 来决定是否执行指定语句 并在条件为 false 的情况再执行另外的语句. 语法 package main func main() { //第一种格式 if 条件表达式 { 语句1 } //第二种格式 if 初始化表达式; 条件表达式 { 语句1 } //第三种格式 if 初始化表达式; 条件表达式 { 语句1 }else{ 语句2 } //第四种格式 if 初始化表达式; 条件表达式 {

  • C语言动态内存管理malloc柔性数组示例详解

    目录 1.1为什么存在动态内存管理 1.2动态内存管理函数 1.2.1malloc 1.2.2free 1.2.3calloc 1.2.4realloc 1.3动态内存管理函数易错点 1.3.1对NULL指针的解引用操作 1.3.2对动态开辟空间的越界访问 1.3.3对非动态开辟内存使用free释放 1.3.4使用free释放一块动态开辟内存的一部分 1.3.5对同一块动态内存多次释放 1.3.6动态开辟内存忘记释放(内存泄漏) 2.1常见相关笔试题 2.2C/C++语言中的内存开辟 2.3柔性

  • 语言编程花絮内建构建顺序示例详解

    目录 1 构建 顺序 1.1 交叉编译 1.2 设置 2 构建测试支持 1 构建 顺序 依据词法名顺序 当导入一个包,且这个包 定义了 init(), 那么导入时init()将被执行. 具体执行顺序: 全局变量定义时的函数 import 执行导入 -> cont 执行常量 --> var 执行变量 --> 执行初始化 init() --> 执行 main() ----> main import pk1 ---> pk1 const ... import pk2 ---&

  • C语言编程C++旋转字符操作串示例详解

    目录 旋转字符串 字符串左旋 题前认知: 暴力移位: 三步翻转: 判断字符串旋转 题前认知 字符串追加判断 旋转字符串 字符串左旋 实现一个函数,可以左旋字符串中的k个字符. 例如: ABCD左旋一个字符得到BCDA ABCD左旋两个字符得到CDAB 题前认知: 一个字符串如果就定死了.eg:char arr[]="dfdf"什么的那多没意思,一点都没有人机交互的感觉,(虽然现在人机交互适合个体,不适合集群,但也是比死板的定死字符串舒服) 所以字符串得是我们可输入的,才有可玩性,玩的不

  • Go语言基础切片的创建及初始化示例详解

    目录 概述 语法 一.创建和初始化切片 make 字面量 二.使用切片 赋值和切片 切片增长 遍历切片 总结 总示例 示例一  两个slice是否相等 示例二 两个数字是否包含 概述 切片是一种动态数组 按需自动改变大小 与数组相比,切片的长度可以在运行时修改 语法 一.创建和初始化切片 make 使用内置函数make()创建切片: var slice []type = make([]type, len, cap) //简写: slice := make([]type, len, cap) 字面

随机推荐