目录
一、基本概念
1.定义
2.基本术语
3.性质(重点!!)
二、二叉树
1.定义
2.特殊二叉树
1.满二叉树
2.完全二叉树
3.二叉排序树
4.平衡二叉树
3.性质
4.存储结构
1.顺序存储
2.链式存储
三、二叉树的遍历和线索二叉树
1.二叉树的遍历
1.先序遍历(NLR)
2.中序遍历(LNR)
3.后序遍历(LRN)
4.层次遍历
5.递归与非递归算法转换
6.遍历序列构成二叉树
2.线索二叉树
1.概念
2.三种线索二叉树
3.线索二叉树找前驱/后继
四、树和森林
1.树的存储结构
1.双亲表示法
2.孩子表示法
3.孩子兄弟表示法
2.树、森林与二叉树的转换
1.树->二叉树
2.森林->二叉树
3.二叉树->树
3.树和森林的遍历
1.树的先根遍历
2.树的后根遍历
3.树的层次遍历
4.森林的先序遍历
5.森林的中序遍历
五、树与二叉树的应用
1.哈夫曼树
2.哈夫曼编码
3.并查集
六、课后习题(持续更新中~)
n个节点的有限集合,n=0时为空树,是一种递归的分层结构,满足:
1.有且仅有一个根节点
2.n>0时其余节点可分为m个互不相交的有限集,每个集合又单独成为一个树称为子树。
3.除根节点外所有节点都有且仅有一个前驱,所有节点都有零个或多个后继。
1.节点间关系描述:①祖先:节点K的父亲及父亲的父亲balabala那条线上一串全是K的祖先;②子孙:祖先那条中K扮演的角色;③双亲:直接祖先,即K的前驱节点;④孩子:节点的后继节点,上一条中K扮演的角色;⑤兄弟:同双亲的另几个节点。
2.度:一个节点的孩子个数叫该节点的度,树中节点最大度数叫树的度(度>0为非终端节点)。
3.分支节点:度大于0的节点。
4.节点的深度:从根节点自顶向下逐层累加。
节点的高度:从叶节点自底向上逐层累加。
树的高度(或深度):节点中最大层数。
5.有序树和无序树:有序树从左到右是有序的,不可交换,否则为无序树。
6.路径和路径长度:路径由两节点间所经过的节点序列构成;路径长度为路径上所经过的边的个数。
7.森林:m棵互不相交的树的集合,树将根节点去掉也能形成新的森林。
1.节点数 = 总度数+1
2.度为m的树&m叉树
度为m的树 | m叉树 |
任意节点的度<=m | 任意节点的度<=m |
至少一个节点度=m | 允许所有节点度 |
一定是非空树,至少m+1个节点 | 可以是空树 |
3.度为m的树第 i 层最多个节点
m叉树第 i 层最多个节点(i>=1)
4.高度为h的m叉树最多个节点。
5.高度为h的m叉树至少有h个节点(一串)。
高度为h、度为m的树至少有h+m-1个节点(一串串底下缀一堆)。
6.具有n个节点的m叉树的最小高度为 [向上取整]
7.总结:
m叉树 | 度为m的树 | |
第i层最多节点数 | ![]() |
![]() |
最多节点数 | ![]() |
|
最少节点数 | h(高h) | h+m-1 |
一种特殊的逻辑结构,特点是每个节点至多只有两棵子树,且子树有左右之分,和树相同是一种递归形式定义的结构,是有序树。
对比二叉树和度为2的有序树的区别:
二叉树 | 度为2的有序树 |
可为空 | 至少三个节点 |
节点次序固定,无论有几个孩子,孩子的左右属性都不改变 | 孩子左右顺序是相对另一个孩子而言,若仅有一个孩子则没有左右之分 |
高为h,含有2^h-1个节点的二叉树,即树每层都有最多的节点,不存在度为1的节点。对满二叉树按层编号,则编号为 i 的节点左孩子编号为 2i ,右孩子编号为 2i+1。
高度为h,有n个节点,且编号1~n与完全二叉树相符合。
性质:① i<=[n/2] (向下取整)为分支节点,否则为叶节点。(重要!!)
②叶节点只能出现在层次最大的两层,最大层中叶节点都排列在该层最左侧。
③有0个或1个度为1的节点,且度为1的节点只有左孩子无右孩子。
④编号为 i 的节点若为叶节点或只有左孩子,则编号大于 i 的节点均为叶节点。
⑤若n为奇数,则每个分支节点都有左右两个孩子;若n为偶数,则编号最大的分支节点(编号n/2)只有左孩子而没有右孩子,其余分支节点左右孩子都有(原因:n0 = n2+1,n = n0+n1+n2,n奇数n1偶 = 0;n偶n1奇 = 1)
⑥ i>1时双亲节点为[i/2](向下取整),i 偶数时双亲为 i/2,i 奇数时双亲 (i-1)/2
⑦ 2i<=n时节点 i 左孩子编号 2i,否则无左孩子;2i+1<=n时,节点 i 的右孩子编号 2i+1,否则无右孩子。
⑧ i 所在层次深度为[log2(i)](向下取整)+1
关键字大小:左子树<根节点<右子树,子树中依旧递归遵守该定义。
树上任意一个节点的左子树和右子树深度差不超过1。
①n0 = n2 + 1
②二叉树第 i 层至多有2^(i-1)个节点;m叉树第 i 层至多有m^(i-1)个节点。
③高为h的二叉树至多2^h-1个节点(满二叉树);高为h的m叉树最多个节点。
用一组连续地址的数组,按照从上到下、从左到右的顺序依次存储完全二叉树中各个节点,比较适用于完全二叉树和满二叉树。对于一般二叉树,可通过设置空节点,让每个节点与完全二叉树相对照。
存储时最好从下标为1的空间开始,这样计算根节点和左右孩子下标关系会比较方便。
#define MaxSize 10
struct TreeNode{
ElemType value;
bool isEmpty; //判断节点是否为空
};
TreeNode t[MaxSize];
for(int i=0;i
若非完全二叉树顺序对应存储则无法从节点编号反应节点间逻辑关系,因此使用一个二维数组,一行存放数据,另一行存放双亲信息,根节点双亲指针 = -1,非根节点双亲指针 = 父节点在数组的下标,利用下标建立节点与双亲的关系。
节点结构包含数据域 data、左指针域 lchild、右指针域 rchild。
在含有n个节点的二叉链表中,含有n+1个空链表
struct ElemType{
int value;
};
typedef struct BiTNode{
ElemType data;
struct BiTNode *lchild, *rchild;
//struct BiTNode *parent; //三叉链表便于查找父节点
}BiTNode, *BiTree;
//初始化根节点
BiTree root = NULL;
root = (BiTree)malloc(sizeof(BiTNode));
root->data = {1};
root->lchild = NULL;
root->rchild = NULL;
//插入新节点
BiTNode *p = (BiTNode *)malloc(sizeof(BiTNode));
p->data = {2};
p->lchild = NULL;
p->rchild = NULL;
root->lchild = p;
二叉树遍历是按某条搜索路径访问树中的各个节点,使每个节点都被访问一次,其中对根节点N、左子树L和右子树R的搜索方法包括NLR、LNR、LRN和层次遍历。
typedef struct BiTNode{
ElemType data;
struct BiTNode *lchild,*rchild;
}BiTNode, *BiTree;
判空,非空情况下做以下递归:
①访问根节点
②先序遍历左子树
③先序遍历右子树
//先序遍历
void PreOrder(BiTree T){
if(T!=NULL)
{
visit(T);
PreOrder(T->lchild);
PreOrder(T->rchild);
}
}
判空,非空情况下做以下递归:
①中序遍历左子树
②访问根节点
③中序遍历右子树
//中序遍历
void InOrder(BiTree T)
{
if(T!=NULL)
{
InOrder(T->lchild);
visit(T);
InOrder(T->rchild);
}
}
判空,非空情况下做以下递归:
①后序遍历左子树
②后序遍历右子树
③访问根节点
//后序遍历
void PostOrder(BiTree T)
{
if(T!=NULL)
{
PostOrder(T->lchild);
PostOrder(T->rchild);
visit(T);
}
}
应用:求树的深度
int treeDepth(BiTree T)
{
if(T==NULL) return 0;
else{
int l=treeDepth(T->lchild);
int r=treeDepth(T->rchild);
return l>r ? l+1 : r+1;
}
}
三种遍历方式中,每个节点都访问且只访问一次(递归是会路过3次,但不重复访问),因此三种遍历方法时间复杂度都为O(n)。同时,递归时发现递归工作栈的深度恰好为树的深度,因此最坏情况下节点数为n的二叉树树高为n,空间复杂度为O(n)。
算法思想:
①初始化一个辅助队列
②根节点入队
③若队列非空,则队头节点出队,访问该节点,将节点的左右孩子节点入队
④重复上述过程③直到队列为空。
//二叉树节点
typedef struct BiTNode{
char data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
//链式队列节点
typedef struct LinkNode{
BiTNode *data;
struct LinkNode *next;
}LinkNode;
typedef struct{
LinkNode *front, *rear; //队头队尾
}LinkQueue;
//层次遍历
void LevelOrder(BiTree T)
{
LinkQueue(Q);
InitQueue(Q);
BiTree p;
EnQueue(Q,T);
while(!IsEmpty(Q))
{
DeQueue(Q, p);
visit(p);
if(p->lchild!=NULL)
EnQueue(Q, p->lchild);
if(p->rchild!=NULL)
EnQueue(Q, p->rchild);
}
}
借助栈的思想分析中序遍历访问过程:
①沿着根的左孩子依次入栈,直到左孩子为空
②栈顶元素出栈并访问:若右孩子为空继续执行;若右孩子非空,将右子树转执行①
中序遍历非递归算法:
void InOrder2(BiTree T)
{
InitStack(S);
BiTree p=T;
while(p || !IsEmpty(S))
{
if(p)
{
Push(S,p);
p = p->lchild;
}
else
{
Pop(S,p);
visit(p);
p = p->rchild;
}
}
}
先序遍历非递归算法:
void PreOrder2(BiTree T)
{
InitStack(S);
BiTree p=T;
while(p || !IsEmpty(S))
{
if(p)
{
visit(p);
Push(S, p);
p = p->lchild;
}
else
{
Pop(S, p);
p = p->rchild;
}
}
}
后序遍历非递归算法思想(相对较复杂):
①沿根的左孩子,直到左孩子为空
后序遍历非递归算法:
//后序遍历非递归算法
void PostOrder2(BiTree T)
{
InitStack(S);
BiTNode *p = T;
BiTNode *r = NULL;
while(P || !IsEmpty(S))
{
if(p)
{
Push(S, p);
p = p->lchild;
}
else
{
GetTop(S, p);
if(p->rchild && p->rchild!=r) //若右子树存在且未被访问
{
p = p->rchild;
}
else
{
Pop(S, p);
visit(p->data);
r = p; //记录最近被访问过的节点
p = NULL; //节点访问过后,重置p指针
}
}
}
}
后序遍历非递归访问时,访问到一个p节点,栈中节点恰好是p节点的全部祖先,从栈底到栈顶节点再加上p节点,刚好构成从根节点到p节点的一条路径。
应用:求根到某节点的路径、求两个节点的最近公共祖先等。
需明确,给定一个前序/中序/后序遍历序列,该序列可能对应多个二叉树,因此单个序列无法确定构建一颗二叉树,因此需要将不同序列进行结合,共同进行二叉树构建,但注意,前序、后序、层序的两两组合是无法唯一确定一颗二叉树的。
核心:利用前序/后序/层序确定根节点,再结合中序中根节点所在位置判断左右子树,再利用这种思想确定各子树中的根节点和下一层子树,最终建立出整个二叉树。
1.前序+中序遍历序列
前序:根节点、左子树、右子树
中序:左子树、根节点、右子树
前序确定第一个出现的节点为根节点,对比中序中根节点位置确定该二叉树第一层左右子树内容,再利用前序分别确定第一层子树的根节点,结合中序确定第二层左右子树内容,依此进行以上步骤直到建成整个二叉树。
2.后序+中序遍历序列
后序:左子树、右子树、根节点
中序:左子树、根节点、右子树
和第一种类似,由后序先确定根节点,再利用中序确定左右子树内容,依此划分各层次根节点和子树直到叶子节点。
3.层序+中序遍历序列
层次:逐层从左到右输出
中序:左子树、根节点、右子树
利用层次遍历得到根节点,利用中序确定下一层的左右子树内容,再利用层次遍历确定第二层子树的根节点,结合中序对二叉树节点进行进一步划分,最终得到整个二叉树。
传统的二叉树仅能体现出父子关系,不能直接得到节点在遍历中的前驱或后继,因此线索二叉树利用链表中的 n+1个空指针(叶子节点每个有2个空指针,度为1的节点每个有1个空指针)来存放指向其前驱或后继的指针,来加快查找节点前驱和后继的速度。
//线索二叉树节点
typedef struct ThreadNode{
ElemType data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag; //左、右线索标示
}ThreadNode, *ThreadTree;
//tag标志为0时标示指针指向孩子;为1时标示指针指向“线索”
1.先序线索化(爱滴魔力转圈圈)
思想和中序一样,看下面(写文章的时候先写的中序dbq)
ThreadNode *pre = NULL;
//前序遍历二叉树,一边遍历一边线索化
void PreThread(ThreadTree T)
{
if(T!=NULL)
{
visit(T);
PreThread(T->lchild);
PreThread(T->rchild);
}
}
void visit(ThreadNode *q)
{
if(q->lchild == NULL)
{
q->lchild = pre;
q->ltag = 1;
}
if(pre != NULL && pre->rchild == NULL)
{
pre->rchild = q;
pre->rtag = 1;
}
pre = q;
}
//前序线索化二叉树T
void CreatePreThread(ThreadTree T)
{
pre = NULL;
if(T!=NULL)
{
PreThread(T);
if(pre->rchild == NULL)
pre->rtag = 1; //处理遍历的最后一个节点
}
}
2.中序线索化
附设指针pre指向刚刚访问过的节点,指针p指向正在访问的节点,即让pre指向p的前驱。在中序遍历的过程中,检查p的左指针是否为空,若为空就将他指向pre;检查pre的右指针是否为空,若为空就将它指向p
ThreadNode *pre = NULL;
//中序遍历二叉树,一边遍历一边线索化
void InTread(ThreadTree T)
{
if(T!=NULL)
{
InThread(T->lchild);
visit(T);
InThread(T->rchild);
}
}
void visit(ThreadNode *q)
{
if(q->lchild == NULL)
{
q->lchild = pre; //pre全局变量
q->ltag = 1; //一定记得修改指针状态
}
if(pre != NULL && pre->rchild == NULL)
{
pre->rchild = q;
pre->rtag = 1;
}
pre = q; //节点后移
}
//中序线索话二叉树T
void CreateInThread(ThreadTree T) //这个函数和书上有一点不同,但核心思想是一样的
{
pre = NULL;
if(T!=NULL)
{
InThread(T);
if(pre->rchild == NULL)
pre->rtag = 1; //处理遍历的最后一个节点
}
}
3.后序线索化
同理
ThreadNode *pre = NULL;
//后序遍历二叉树,一边遍历一边线索化
void PostThread(ThreadTree T)
{
if(T!=NULL)
{
PostThread(T->lchild);
PostThread(T->rchild);
visit(T);
}
}
void visit(ThreadNode *q)
{
if(q->lchild == NULL)
{
q->lchild = pre;
q->ltag = 1;
}
if(pre != NULL && pre->rchild == NULL)
{
pre->rchild = q;
pre->rtag = 1;
}
pre = q;
}
//后序线索化二叉树T
void CreatePostThread(ThreadTree T)
{
pre = NULL;
if(T!=NULL)
{
PostThread(T);
if(pre->rchild == NULL)
pre->rtag = 1; //处理遍历的最后一个节点
}
}
1.中序线索二叉树
在中序线索二叉树中找到指定节点*p的中序后继next:
① p->rtag == 1, next = p->rchild
② p->rtag == 0,next = p的右子树中最左下节点,代码:
//找到以p为根的子树中,第一个被中序遍历的节点
ThreadNode *Firstnode(ThreadNode *p)
{
//循环找到最左下节点(不定为叶节点)
while(p->ltag == 0)
p = p->lchild;
return p;
}
//在中序线索二叉树中找到节点p的后继节点
ThreadNode *Nextnode(ThreadNode *p)
{
//右子树的最左下节点
if(p->rtag == 0)
return Firstnode(p->rchild);
else
return p->rchild; //rtag == 1直接返回后继线索
}
//对中序线索二叉树进行中序遍历(利用线索实现的非递归算法)
void Inorder(ThreadNode *T)
{
for(ThreadNode *p = Firstnode(T); p!=NULL; p=Nextnode(p))
visit(p);
}
在中序线索二叉树中找到指定节点*p的中序前驱pre:
① p->ltag == 1, pre = p->lchild
② p->ltag == 0,pre = p的左子树中最右下节点,代码:
//找到以p为根的子树中,最后一个被中序遍历的节点
ThreadNode *Lastnode(ThreadNode *p)
{
//循环找到最右下节点(不定为叶节点)
while(p->rtag == 0)
p = p->rchild;
return p;
}
//在中序线索二叉树中找到节点p的前驱节点
ThreadNode *Prenode(ThreadNode *p)
{
//左子树中最右下节点
if(p->ltag == 0)
return Lastnode(p->lchild);
else
return p->lchild;
}
//对中序线索二叉树进行逆向中序遍历
void RevInorder(ThreadNode *T)
{
for(ThreadNode *p = Lastnode(T); p!=NULL; p=Prenode(p))
visit(p);
}
2.先序线索二叉树
1.在先序线索二叉树中找到指定节点*p的先序后继next:
① p->rtag == 1, next = p->rchild
② p->rtag == 0,next = 左孩子(没有就右孩子)
2.在先序线索二叉树中找到指定节点*p的先序前驱pre:
① p->ltag == 1, pre = p->lchild
② p->ltag == 0,可能做不到,只能用土办法从头进行遍历,或改用三叉链表
i 如果能找到p的父节点且p为左孩子(或为右孩子,左孩子为空),则前驱为p的父节点
ii 如果p为右孩子且左孩子非空,则p的前驱为左兄弟子树中最后一个被先序遍历的节点
iii 如果p为根节点,则无前驱
3.后序线索二叉树
1.在后序线索二叉树中找到指定节点*p的后序后继next:
① p->rtag == 1, next = p->rchild
② p->rtag == 0,只能用土办法从头开始遍历,后序遍历中左右子树否为根的前驱,不可能是后继:
i 如果能找到p的父节点,p是右孩子(或没有右孩子时的左孩子),则p的后继是其父节点
ii 如果能找到p的父节点,p是左孩子且有右兄弟,则p的后继为右兄弟子树中第一个被后序遍历的节点
iii 如果p是根节点,则p没有后继节点。
2.在后序线索二叉树中找到指定节点*p的后序前驱pre:
① p->ltag == 1, pre = p->lchild
② p->ltag == 0,pre = 右孩子(没有就左孩子)
这部分重点在于理解,千万不要死记硬背。
本质是顺序存储,详见上文顺序存储中二维数组存储法(一行存数据,一行存双亲的数组下标)。
#define MAX_TREE_SIZE 100 //节点结构体
typedef struct{
ElemType data;
int parent; //双亲位置域
}PTNode;
typedef struct{ //树结构体
PTNode nodes[MAX_TREE_SIZE];
int n; //节点个数
}PTree;
双亲表示法也可用于表示森林,即另每个树的根节点位置域都置-1,其他元素位置域对应存放双亲的数组下标。
优点:找父节点方便
缺点:找孩子不方便
适合找父亲节点多而找孩子节点少的情况,如:并查集。
本质是链式存储,用数组顺序存储各个节点,每个节点保存数据元素、孩子链表表头指针。
struct CTNode{
int child; //孩子节点在数组中的位置
struct CTNode *next; //下一个孩子
};
typedef struct{
ElemType data;
struct CTNode *firstChild; //第一个孩子
}CTBox;
typedef struct{
CTBox nodes[MAX_TREE_SIZE];
int n,r; //节点数和根的位置
}
孩子法存储森林需要记录多个根位置,较冗杂。
优点:找孩子方便
缺点:找父亲麻烦,只能遍历每个链表
适合找孩子多而找父亲少的情景,如:服务流程树。
利用二叉链表实现,每个节点内保存数据元素和两个指针,但两个指针指向为前一个孩子和右边的第一个兄弟,与二叉树两个指针指向不同。
typedef struct CSNode{
ElemType data;
struct CSNode *firstchild, *nextsibling;
//第一个指向第一个孩子,第二个指向右边第一个兄弟
}CSNode, *CSTree;
孩子兄弟表示法存储树或森林,从存储视角看形态与二叉树类似,相对更方便。
①在二叉树中画出一个根节点
②按“树的层序”依此处理每个节点:如果当前节点有孩子就把所有孩子节点用右指针串起来,并把第一个孩子挂在该节点左指针下方
③进行步骤②,直到各节点左指针指向第一个孩子,右指针连接最近的一个右兄弟为止。
将各树的根节点视为同级的兄弟,其他和树到二叉树的转换方式一样。
①画出树的根节点
②从根节点开始,按“树的层序”恢复每个节点的孩子
4.二叉树->森林
步骤与二叉树到树的转变相同,只不过第一串右子树要拆成不同的几棵树
若树非空,先访问根节点,再依此对每棵子树进行先根遍历
树的先根遍历序列与这棵树相应二叉树的先序序列相同
若树非空,先依此对每棵子树进行后根遍历,再访问其根节点。
树的后根遍历序列与这棵树相应二叉树的中序序列相同
①若树非空,则根节点入队
②若队列非空,队头元素出队并访问,同时将该元素的孩子依此入队
③重复②直到队列为空
若森林非空:
①访问森林中第一棵树的根节点
②先序遍历第一棵树中根节点的子树森林
③先序遍历除去第一棵树之后剩余的树构成的森林
效果等同于依此对各个树进行先根遍历
若森林非空:
①中序遍历森林中第一棵树的根节点的子树森林
②访问第一棵树中根节点
③中序遍历除去第一棵树之后剩余的树构成的森林
效果等同于依此对各个树进行后根遍历
内容总结:
树 | 森林 | 二叉树 |
先根遍历 | 先序遍历 | 先序遍历 |
后根遍历 | 中序遍历 | 中序遍历 |
节点的权:某种现实含义的数值
节点的带权路径长度:从树的根到该节点的路径长度(经过的边数)与该节点上权值的乘积(节点高度*节点权值)
树的带权路径长度:树中所有叶节点的带权路径长度之和(WPL)
哈夫曼树:在含有n个带权叶节点的二叉树中,带权路径长度(WPL)最小的二叉树,也称为最优二叉树。
固定长度编码:每个字符用相等长度的二进制位表示。
可变长度编码:允许对不同字符用不等长的二进制位表示,若没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码。
哈夫曼编码:字符集中每个字符作为一个叶子节点,各个字符出现的频度作为节点权值,根据创建的哈夫曼树确定各字符的哈夫曼编码。
用互不相交的树表示多个“集合”,存储方法为双亲表示法。
查:从指定元素开始查询其所在树的根节点,比对确定两个元素是否在同一集合内。
并:将一棵树成为另一棵树的子树,即利用指针将根节点直接挂到另一棵树的根节点上。
#define SIZE 13
int UFSets[SIZE]; //集合元素数组
//初始化并查集
void Initial(int S[])
{
for(int i=0; i=0)
{
x = S[x]; //循环找x的根
}
return x;
}
//Union————并操作,将两个集合合并 时间复杂度:O(1) 全部合并:O(n^2)
void Union(int S[], int Root1, int Root2)
{
if(Root1 == Root2) return;
S[Root2] = Root1;
}
Union优化思路:每次Union操作创建树时尽可能不让树变高
①用根节点绝对值表示树的节点总数
②Union操作让小树合并到大树。
优化后Find操作最坏时间复杂度为:O(log2(n)),树高不超过[log2(n)]+1
//Union————并操作,小树合并到大树 时间复杂度:O(1) 全部合并:O(nlog2(n))
void Union(int S[], int Root1, int Root2)
{
if(Root1 == Root2) return;
if(S[Root2]>S[Root1])
{
S[Root1] += S[Root2];
S[Root2] = Root1;
}
else
{
S[Root2] += S[Root1];
S[Root1] = Root2;
}
}
Find优化思路:压缩路径,先找到根节点,再将查找路径上所有节点都挂到根节点下。
优化后Union操作整体时间复杂度:O(na[n])
//Find————查操作,先找到根节点再压缩路径 最坏时间复杂度:O(a(n)) a(n)增长缓慢
int Find(int S[], int x)
{
int root = x;
while(S[root]>=0)
{
root = S[root]; //循环找根
}
while(x!=root) //压缩路径
{
int t=S[x];
S[x] = root;
x = t;
}
return root;
}
核心思想:让树变矮