1.二叉树的概念及结构
1.1树的概念
1.1.1概念
在了解二叉树前,咱们先来了解一下什么是树:树是一种非线性的数据结构,它是由n(n>=0)个有限节点组成的一个具有层次关系的的集合。把它叫做树是因 为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的.
其有一个特殊的结点,称为根节点,根节点没有前驱节点,除了根节点之外其余节点被分为M(M>0)个互不相交的集合,而每个集合又是一个结构与树类似的子树。每棵子树的根节点有且仅有一个前驱,可以有0个或者N个后继。因此,树是递归定义的。要注意的是,树形结构中子树不能有交集,不然就不是树。
1.1.2相关概念

以上图为例来说明一些数的基本概念:
结点的度:一个结点含有的子树的个数称为该结点的度; 如上图:A的为6
叶结点或终端结点:度为0的结点称为叶结点; 如上图:B、C、H、I...等结点为叶结点
双亲结点或父结点:若一个结点含有子结点,则这个结点称为其子结点的父结点; 如上图:A是B的父结点
孩子结点或子结点:一个结点含有的子树的根结点称为该结点的子结点; 如上图:B是A的孩子结点
堂兄弟结点:双亲在同一层的结点互为堂兄弟;如上图:H、I互为兄弟结点
树的度:一棵树中,最大的结点的度称为树的度; 如上图:树的度为6
结点的层次:从根开始定义起,根为第1层,根的子结点为第2层,以此类推
树的高度或深度:树中结点的最大层次; 如上图:树的高度为4
1.1.3树的表示
树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既然保存值域,也要保存结点和结点之间 的关系,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法 等。我们这里就简单的了解其中最常用的孩子兄弟表示法,如下。
typedef int DataType;
struct Node
{
    struct Node* firstChild1;//第一个孩子的结点
    struct Node* pNextBrother;//指向下一个兄弟节点
    DataType data;//结点中的数值             
};

1.2二叉树的概念
了解了树的基本概念之后,我们来了解一下二叉树。
一颗二叉树是结点的一个有限的集合,该集合可以为空,或者由一个根节点和两棵左右子树组成。

由上图可以得出,二叉树不存在度大于2的结点,且有左右子树之分,次序不能颠倒,因此二叉树是有序树。
对于任何二叉树都是由以下几种情况符合而成:

1.3特殊二叉树
二叉树一共有两种特殊情况,分别称为满二叉树和完全二叉树。
满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是 说,如果一个二叉树的层数为K,且结点总数是,则它就是满二叉树。
完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K 的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对 应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。

1.4二叉树的性质
二叉树还有以下性质:
1. 若规定根结点的层数为1,则一棵非空二叉树的第i层上最多有2^(i-1)个结点.
2. 若规定根结点的层数为1,则深度为h的二叉树的最大结点数是 2^h-1.
3. 对任何一棵二叉树, 如果度为0其叶结点个数为 , 度为2的分支结点个数为 ,则有n0=n2+1.
4. 若规定根结点的层数为1,具有n个结点的满二叉树的深度,h=log(n+1).(是以2为底n+1的对数)
5. 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有结点从0开始编号,则对 于序号为i的结点有:
1. 若i>0,i位置结点的双亲序号:(i-1)/2;i=0,i为根结点编号,无双亲结点
2. 若2i+1<n,左孩子序号:2i+1,2i+1>=n否则无左孩子
3. 若2i+2<n,右孩子序号:2i+2,2i+2>=n否则无右孩子
1.5二叉树的储存结构
二叉树一般有两种储存结构,一种是顺序结构,一种是链式结构。
顺序结构:即用数组来储存,但是一般只适合表示完全二叉树,否则就会产生空间上的浪费(如下图)。而现实使用中只有堆才会使用数组来储存(堆在之后有讲)。二叉树顺序储存在物理上是一个数组,在逻辑上是棵树。

链式结构:指用链表来表示一棵树,用链表来指示元素的逻辑关系。通常的表示方法是链表中有一个左指针、一个右指针和一个储存数据的变量。左右指针分别储存左右孩子节点的地址。链式结构分为二叉链和三叉链,在二叉树这部分咱们用到的是二叉链,三叉链会多一个指向父节点的指针。
typedef int BTDataType;
// 二叉链
struct BinaryTreeNode
{
    struct BinTreeNode* left;   // 指向当前结点左孩子
    struct BinTreeNode* right;  // 指向当前结点右孩子
    BTDataType data;            // 当前结点值域
}
2.二叉树的顺序结构及实现
2.1堆的概念及结构
前面咱们提到,顺序结构是指用数组来储存,而现实中一般只有堆会用数组储存,那么什么是堆呢?
实际上,这里的堆不是我们内存中的堆,而是一种数据结构,是一种二叉树。
堆的性质:堆中某个节点的值,总不大于(称为大堆结构)或不小于(称为小堆结构)其父节点的值,且堆总是一棵完全二叉树。

2.2堆的实现
接下来咱们将要逐步实现堆。堆的基本结构如下,一个典型的顺序表结构,那么初始化和销毁也和顺序表类似,在此不再赘述。
typedef int HPDataType;
typedef struct Heap
{
	HPDataType* a;
	int size;
	int capacity;
}Heap;
2.2.1堆的插入(向上调整算法)
以建小堆为例,如果咱们需要插入一个数据到现有的堆中(也就是插入到最后),如果这个数据的值比其父节点小,那么这个堆就不再是堆,此时我们需要调整最后一个叶子节点的位置,使得二叉树仍为堆。于是,我们需要一个向上调整算法,来帮咱们实现这一步。
//传入堆和,子节点的位置
void ArrUp(HPDataType* a, int child)
{
	assert(a);
	//小堆
    //根据性质求父节点的位置
	int parent = (child - 1) / 2;
    //最坏的情况,从最后一个节点调整到了根节点,此时child为0,结束循环
	while (child > 0)
	{
        //父节点和子节点比较,大于则交换
		if (a[parent] > a[child])
		{
            //封装的两数交换函数,传入堆和需要交换的位置
			Swap(a, parent, child);
            //此时孩子到了父亲的位置,
			child = parent;
            //找到新的父节点
			parent = (child - 1) / 2;
		}
        //父节点小于子节点,调整完毕,退出循环
		else
		{
			break;
		}
	}
}
这时候我们就可以进行插入操作,操作和顺序表的插入相同,只是在最后加上了向上调整的操作。
void HeapPush(Heap* ph, HPDataType x)
{
	assert(ph);
	if (ph->size == ph->capacity)
	{
		int Doublecapacity = ph->capacity == 0 ? 4 : 2 * ph->capacity;
		ph->capacity = Doublecapacity;
		HPDataType* ptr = (HPDataType*)realloc(ph->a, sizeof(HPDataType) * Doublecapacity);
		if (ptr == NULL)
		{
			perror("realloc fail");
			return;
		}
		ph->a = ptr;
	}
	(ph->a)[ph->size++] = x;
	//向上排序
	ArrUp(ph->a, ph->size-1);
}
2.2.2堆的删除(向下调整算法)
堆的删除,指的是删除根节点,那根节点缺的这块谁给咱补啊!于是就有了一个操作,把根节点和最后一个叶子节点交换,此时再把此时的大小-1(size-1)这时候根节点被删除但是仍然存在了,但这也同样不是堆了口牙,这时咱们需要一个向下调整算法,把根节点的异端调到下面去。
以小堆为例,
void ArrDown(HPDataType* a, int size,int parent)
{
	assert(a);
	//小堆
    //找到左孩子
	int child = parent * 2 + 1;
	//向下调整的过程中孩子节点的位置在变大,最坏情况调整到最后一个位置,孩子越界的时候跳出循环
	while (child < size)
	{
        //如果在不越界的情况下,右孩子比左孩子小,选择右孩子作为比较的对象
		if (child + 1 < size  && a[child + 1] < a[child])
		{
			child++;
		}
        //父节点大于孩子节点
		if (a[parent] > a[child])
		{
            //两数交换
			Swap(a, parent, child);
            //父亲来到孩子的位置
			parent = child;
            //寻找新的孩子,并在上一个if选择出小的那个
			child = parent * 2 + 1;
		}
        //父节点小于孩子节点,排好了,退出循环
		else
			break;
	}
	
}
这样,我们就得到了重新获得堆的办法,这时候就可以进行删除,
void HeapPop(Heap* ph)
{
	assert(ph);
	Swap(ph->a,ph->size-1,0);
	ph->size--;
	//向下调整
	ArrDown(ph->a, ph->size,0);
}
2.2.3取根数据/取数据个数/判空
取数据、取个数、判空这一块过于简单,一笔带过。
//取顶数据
HPDataType HeapTop(Heap* ph)
{
	assert(ph);
	assert(ph->size > 0);
	return ph->a[0];
}
//取数据个数
int HeapSize(Heap* ph)
{
	assert(ph);
	return ph->size;
}
//判空
int HeapEmpty(Heap* ph)
{
	assert(ph);
	return ph->size == 0 ? 1 : 0;
}
2.3应用
2.3.1建堆
在前面堆的实现,我们可以通过输入数据来得到一个堆,那如果直接给咱们一个数组的数据呢?虽然说咱们可以通过遍历的方式传入数据来建立一个堆,那么有没有什么不用创建新变量的方法,直接改动原数组的方式来获得一个堆呢?
答案是有的,且就在咱们上面提到的向下调整算法中。因为数组本身就是一个完全二叉树,那么,我们就可以从最后一个非叶子节点开始,向前遍历向下调整算法,子树从小到大被调整成堆结构,最终调整到根,以至于所有的结点被调整,就形成了堆。
以建小堆为例,
//最后一个叶子节点是n-1,最后一个叶子节点的父节点是(n-1)-1/2
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
    //传入数组,数组大小,和i
	ArrDown(a, n, i);
}
2.3.2堆排序
有人就要问了,叽里咕噜说了这么多,堆有什么用呢?
你看哈,这个堆顶必然是所有数据中最大/最小的,再加上我们删除操作实际上不会删除数据,而是相当于把数据隐藏起来了,那咱们建了个小堆,进行一次删除,最小的元素在最后,此时堆顶是第二小的数字,反复操作,不就实现了降序排序吗?于是就有了堆排序。
void HeapSort(int* a, int n)
{
	assert(a);
	//降序建小堆
	//先把数组排成堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		ArrDown(a, n, i);
	}
	//pop掉,再进行排序
	while(--n)
	{
        //交换首尾
		Swap(a, n , 0);
        //在减少了元素的基础上重新调整成堆
		ArrDown(a, n , 0);
	}
}
2.3.3TopK问题
TopK问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。比如游戏前百名、富豪榜等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但如果数据量非常大,排序就不太可取了,那么咱们可以用堆来解决这个问题,思路如下:
1.用需要找的K个数量的元素建堆:
如果找前K个最小的元素,建大堆;如果找前K个最大的元素,建小堆。
2.用剩余的N-K与堆顶进行比较:
建大堆,比根小的替换根,重新调整堆,比根大的跳过。
建小堆,比根大的替换根,重新调整堆,比根小的跳过。
当N-K个元素比较完时,剩下在堆里的元素就是最大/最小的前K个元素。
接下来,咱们造点数据实现一下这个过程:
//生成数据
void CreateNDate()
{
	// 造数据
	int n = 10000;
    //产生随机数
	srand((unsigned int)time(0));
	const char* file = "data.txt";
	FILE* fin = fopen(file, "w");
	if (fin == NULL)
	{
		perror("fopen error");
		return;
	}
    //写入一万个数据
	for (size_t i = 0; i < n; ++i)
	{
		int x = rand() % 1000000;
		fprintf(fin, "%d\n", x);
	}
	fclose(fin);
}
为了使结果醒目,所以咱事先在文件中修改了五个值,使其大于其他的数据.
void test03()
{
	int k = 0;
	scanf("%d", &k);
	int* a = (int*)malloc(sizeof(int) * k);
	if (a == NULL)
	{
		perror("malloc fail");
		return;
	}
	FILE* fout = fopen("data.txt", "r");
	if (fout == NULL)
	{
		perror("fopen error");
		return;
	}
	int x = 0;
	//读取k个数据放入数组
	for (int i = 0; i < k; i++)
	{
		fscanf(fout, "%d", &x);
		a[i] = x;
	}
	//建堆
	for (int i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		ArrDown(a, k, i);
	}
    //开始比较
	while (fscanf(fout, "%d", &x) > 0)
	{
		if (x > a[0])
		{
			a[0] = x;
			ArrDown(a, k, 0);
		}
	}
	fclose(fout);
	fout = NULL;
	print(a, k);
}
运行结果如下,因为只是找前5个元素,所以并没有进行排序。

3.二叉树链式结构及实现
前情提要:用二叉链来实现二叉树。以下是基本结构。
typedef int BTDataType;
// 二叉链
struct BinaryTreeNode
{
    struct BinTreeNode* left;   // 指向当前结点左孩子
    struct BinTreeNode* right;  // 指向当前结点右孩子
    BTDataType data;            // 当前结点值域
}
3.1二叉树的遍历
二叉树遍历,指按照某种特定的规则,依次对二叉树的结点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。
按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历和层序遍历:
1. 前序遍历——访问根结点的操作发生在遍历其左右子树之前,即访问顺序:根 左子树 右子树。
2. 中序遍历——访问根结点的操作发生在遍历其左右子树之中(间),即访问顺序:左子树 根 右子树。
3. 后序遍历——访问根结点的操作发生在遍历其左右子树之后,即访问顺序:左子树 右子树 根。
4.层序遍历——从第一层开始向下逐层从左向右访问每一个节点。
3.1.1前序遍历
以下图为例,前序遍历的经过就是:根节点1->1的左子树根2->2的左子树根3->3的左NULL(->回到3)->3的右NULL(->回到3->回到2)->2的右NULL(->回到2->回到1)->1的右子树根4->4的左子树根5->5的左NULL(->回到5)->5的右NULL(->回到5->回到4)->4的右子树根6->6的左NULL(->回到6)->6的右NULL(->回到6->回到4->回到1)->结束.
要注意的是,空节点是遍历的一部分,如果我们用N来表示空的话,我们可以将这棵树的前序遍历表示为:1 2 3 N N N 4 5 N N 6 N N.当然在实际表达中咱们不会加上N,但是这更有利于咱们理解前序遍历。

有了上面的思路,我们可以写出前序遍历的函数
//传入一个根节点
void BinaryTreePrevOrder(BTNode* root)
{
	if (root == NULL)
	{
        //可不打印这一项,打印会更清晰
		printf("N ");
		return;
	}
	printf("%d ", root->data);
    //递归,先递归左子树,后右子树
	BinaryTreePrevOrder(root->left);
	BinaryTreePrevOrder(root->right);
}
3.1.2中序遍历
仍然以这棵树为例,中序遍历的经过是:(根1->根1的左子树根2->2的左子树根3)->3的左NULL->回到3->3的右NULL(->回到3)->回到2->2的右NULL(->回到2)->回到1(->1的右子树根4->4的左子树根5)->5的左NULL->回到5->5的右NULL(->回到5)->回到4(->4的右子树6)->6的左NULL->回到6->6的右NULL(->回到6->回到4->回到1)->结束。
带上空来表示: N 3 N 2 N 1 N 5 N 4 N 6 N
        
实现中序实际上就是变更遍历的顺序,如下。
void BinaryTreeInOrder(BTNode* root)
{
	if(root==NULL)
	{
		printf("N ");
		return;
	}
	BinaryTreeInOrder(root->left);
	printf("%d ", root->data);
	BinaryTreeInOrder(root->right);
}
3.1.3后序遍历
还是老树,简单捋一下后序遍历的经过:(1->2->3)->左NULL(->3)->右NULL->3(->2)->右NULL->2(->1->4->5)->左NULL(->5)->右NULL->5(->4->6)->左NULL(->6)->右NULL->6->4->1->结束
表示为: N N 3 N 2 N N 5 N N 6 4 1

实现同理
void BinaryTreePostOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}
	BinaryTreePostOrder(root->left);
	BinaryTreePostOrder(root->right);
	printf("%d ", root->data);
}
3.1.4层序遍历
层序遍历就不是用递归来实现了,而是需要利用到咱们队列的先进先出的特性(队列可参见往篇)。思路如下:
根节点入队列,取队列第一个结点,让这个结点的左右孩子节点入队列,释放第一个节点(此时左孩子成为第一个节点),再取第一个节点,让这个结点的左右孩子节点入队列,释放第一个节点(此时根节点的右孩子称为第一个节点),再取第一个节点(此时第二层已经遍历完毕),左右孩子节点入队列,释放第一个节点(此时第三层节点都在队列中)……
如此,当队列中没有数据的时候,循环结束,咱们也实现了层序遍历。
void BinaryTreeLevelOrder(BTNode* root)
{
	
	Queue q;
    //初始化
	QueueInit(&q);
    //把根节点入队列
	QueuePush(&q, root);
    //不为空时循环
	while(!QueueEmpty(&q))
	{
        //取第一个节点
		BTNode* front = QueueFront(&q);
        //释放队头
		QueuePop(&q);
        //打印可以略去
		if (front == NULL)
		printf("N ");
        //如果不为空,左右子节点入队列
		else
		{
			printf("%d ", front->data);
			QueuePush(&q, front->left);
			QueuePush(&q, front->right);
		}
	}
    //销毁
	QueueDestroy(&q);
}
可能会有人认为,把队头释放了,front不就成了野指针了吗?可别混为一谈了,这里的队列实际上是个链表,释放的是链表的结点,而并非二叉树的结点,咱只是将结点的指针入队列,并不会释放二叉树的结点,就好比是用罐子装了食物,然后不用罐子装了,食物也不会出现问题。
3.2二叉树的结点个数和高度等
这部分基本上是递归,咱们需要需要注意递归的结束条件。
3.2.1二叉树的高度
咱们假设,二叉树根的高度为1,然后咱们可以用前序递归来获得二叉树的高度,
int BinaryTreeLevel(BTNode* root)
{
	if (root == NULL)
		return 0;
	int left = BinaryTreeLevel(root->left) + 1;
	int right = BinaryTreeLevel(root->right) + 1;
	return left > right ? left : right;
}
用老图来分析一下:先走左子树,走到3时,3左右两边为空,返回值为1,回到2时,左边得到返回值1,+1得到2,右边返回1,将2返回到1,故left=3;走右子树,先到5,与3同理,返回1给4,同理6返回1给4,所以4返回1+1给1最终right=3,所以该树的层数为3.
3.2.2二叉树结点个数
统计节点结束就好比统计学校人数,1个校长发号施令,2个副校长让主任去统计,主任又让辅导员上报学生人数,层层加起来就是总人数。
节点个数就是根加上左右子树节点的个数,左右子树又可以看成一棵新的树,分成新的左右子树和根,最终分到只剩下一个,相加就得到了节点个数。
int BinaryTreeSize(BTNode* root)
{
	if (root == NULL)
		return 0;
	return BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}
3.2.3二叉树叶子结点的个数
要统计叶子结点的个数,咱们要知道叶子结点的特点是左右为空,所以与统计结点个数的函数类似遍历,但是只有当递归到的那个结点左右为空才能返回1。最终相加结果就是叶子结点的个数。
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.2.4二叉树第k层结点个数
要计算第k层的结点个数,咱们需要先到达第k层,在那一层的每一个节点都返回一个1.
int BinaryTreeLevelKSize(BTNode* root, int k)
{
	if (root == NULL)
		return 0;
	if (k == 0)
		return 1;
	return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}
3.2.5查找值为x的结点
前序遍历,需要注意结束条件和返回值可能为空的情况。如果实际上有多个结果,返回一个就够了,就说找没找到吧。
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
		return NULL;
	if (root->data == x)
		return root;
	BTNode*left = BinaryTreeFind(root->left, x);
    //返回值不为空,说明找到了
	if (left != NULL)
		return left;
	BTNode* right = BinaryTreeFind(root->right, x);
	if (right != NULL)
		return right;
	return NULL;
}
3.2.6判断是否为完全二叉树
对于完全二叉树咱们知道,除了最后一层外,其他层都是满的,而且最后一层是连续的,也就是说,除了最后一层外出现不满的情况以及最后一层不连续的情况,就可以认为这不是完全二叉树,而这两种情况都有一个特点,按层看先是访问元素,访问空,然后又访问到了元素。所以我们可以用层序遍历来判断,而这就又要用到咱们的队列。
int BinaryTreeComplete(BTNode* root)
{
	Queue q;
    //初始化
	QueueInit(&q);
    //根入队列
	QueuePush(&q, root);
    //不为空时继续循环
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
        //如果出现了空,需要判断之后是否会出现元素,退出当前循环
		if (front == NULL)
			break;
		else
		{
			QueuePush(&q, front->left);
			QueuePush(&q, front->right);
		}
	}
    //循环结束若没有返回0,说明是完全二叉树
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
        //出现了非空元素说明不是完全二叉树
		if (front != NULL)
			return 0;
	}
	QueueDestroy(&q);
	return 1;
}
3.3二叉树的创建和销毁
在前面的情况,咱们都是假设有一个建立好的二叉树,那么咱们怎么创建一个二叉树呢?这里咱给出一个数组"ABD##E#H##CF##G##",接着将要通过前序遍历来构建二叉树。
//创建一个节点并初始化
BTNode* BuyNewNode(BTDataType x)
{
	BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}
	newnode->data = x;
	newnode->left = NULL;
	newnode->right = NULL;
	return newnode;
}
//前序创建二叉树,传入数组,数组大小,和一个指针(*pi=0)用于控制循环,保证传址,*pi的内容可以变化
BTNode* BinaryTreeCreatePre(BTDataType* a, int n, int* pi)
{
	if (a == NULL)
		return NULL;
	if (a[*pi] == '#')
	{
		(*pi) += 1;
		return NULL;
	}
    //越界时,退出循环
	if (n <= *pi)
	{
		return NULL;
	}
	BTNode* root = BuyNewNode(a[(*pi)++]);
	root->left = BinaryTreeCreatePre(a, n, pi);
	root->right = BinaryTreeCreatePre(a, n, pi);
	return root;
}
二叉树销毁时也类似。
void BinaryTreeDestory(BTNode** root)
{
	if (*root == NULL)
		return;
    //保存左右子树
	BTNode* left = (*root)->left;
	BTNode* right = (*root)->right;
	free(*root);
	root = NULL;
	BinaryTreeDestory(&left);
	BinaryTreeDestory(&right);
}
以上就是C语言部分的二叉树,基本上由递归来实现,至于非递归的内容,咱们C++见
                  
                  
                  
                  
      
          
                
                
                
                
              
                
                
                
                
                
              
                
                
              
            
                  
					393
					
被折叠的  条评论
		 为什么被折叠?
		 
		 
		
    
  
    
  
            


            