树-二叉搜索树-AVL树
树
树的基本概念
节点的度:节点的儿子数
树的度:Max{节点的度}
节点的高度:节点到各叶节点的最大路径长度
树的高度:根节点的高度
节点的深度(层数):根节点到该节点的路径长度
树的遍历
·前序遍历:根左右(x,Tl,Tr)
·中序遍历:左根右(Tl,x,Tr)
·后序遍历:左右根(Tl,Tr,x)
树的表示法
1.父节点数组表示法
(寻找父节点O(1),寻找儿子节点O(n))
2.儿子链表表示法
(为克服找父节点不方便,可牺牲空间换时间:)
3.左儿子右兄弟表示法
(通过左儿子右兄弟表示法将一个树存储方式变化为二叉树形态)
(可将森林用类似的方法变化为二叉树(不同的树根节点用右兄弟的指针连接))
二叉树基本概念
·n个节点的二叉树 高度h = [logn向下取整,n-1]
·高度为h的二叉树 节点数n = [h+1,2^(h+1)-1]
·满二叉树:高度为h且有2^(h+1)-1个节点
·近似满二叉树:除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边。
对每个结点i,满足:
i
/
2i 2i+1
因此可用数组存储(节点间关系显见),若为非近似满二叉树想要这样存储,没有该节点的位置可用空占位(牺牲空间)。
·节点数(设度为i的节点数为ni)
N = n0+n1+n2 = n0×0+n1×1+n2×2 +1 ( 节点数 = 分枝数+1 )
可得结论 =>
① n0 = n2 + 1
② N = 2×n0 + n1 - 1 = 2×n2 + n1 + 1
在哈夫曼树中n1 = 0,则有N = 2×n0 - 1 = 2×n2 + 1
·含有n个结点二叉树的不同形态数Bn
Bn = ∑(Bl × Bn-l-1) (左子树+右子树)
=>卡特兰数Bn = 1/(n+1) × C(2n,n)
·二叉树与树
①二叉树是一棵度不超过2的树。(错误,二叉树的儿子节点有左右之分)
②二叉树是一棵读不超过2的有序数。(错误,当只有1个儿子节点时二叉树也有左右之分)
ADT二叉树
·支持的运算
1.BinaryInit():创建一棵空二叉树
2.BinaryEmpty(T):判断二叉树T是否为空
3.Root(T):返回根节点标号
4.MakeTree(x,T,L,R):以x为根节点,L、R为左右子树构建新二叉树T
5.BreakTree(T,L,R):将T拆分为根节点元素、左右子树三个部分
6.PreOrder(T):前序遍历
7.InOrder(T):中序遍历
8.PostOrder(T):后序遍历
/*6、7、8在书上写的函数参数是(visit,T),没解释visit是什么,然后9,10,11是输出前中后序列表,参数只有T。博客这里就使用6,7,8函数名字当它作用就是输出列表了,略去了书上的9,10,11。*/
9.Delete(T):删除二叉树
10.Height(T):二叉树高度
11.Size(T):二叉树结点数
二叉树的实现
1.顺序存储结构
(从树根起,从上往下逐层编号存在数组里。适用于满二叉树,若非满二叉树,则用0占空。)
以该种存储方式得到的性质:
1.当且仅当i = 1时为根节点。
2.不为1时,父节点为i/2(向下取整)
3.左儿子为2i
4.右儿子为2i+1
5.i为奇数且不为1时,左兄弟为i-1
6.i为偶数时,右兄弟为i+1
2.结点度表示法
(将所有结点以后序列表排列,给每个结点附加一个0~3的整数表示状态,0表示叶节点,1表示有一个左儿子,2表示有一个右儿子,3表示有两个儿子。)
得到性质:
- i-1 为右儿子结点
2.必然有左儿子在 i 前,父节点在 i 后
3.用指针实现
线索二叉树
(在每个结点中增加指向在某种遍历下其前驱和后继结点的指针)
线索:所引入的非空指针称为线索。 线索二叉树:加上了线索的二叉树称为线索二叉树。 线索标志位:为了区分一个结点的指针是指向其儿子结点的
指针,还是指向其前驱或后继结点的线索,在每个结点中增 加2个位—leftThread、rightThread分别称为左、右线索标志位。
线索化:对一棵非线索二叉树以某种次序遍历使其变为一棵 线索二叉树的过程称为二叉树的线索化。二叉树线索化: 由于线索化的实质是将二叉树中的空指针改为指向其前 驱结点或后继结点的线索(并做上线索标志),而一个结
点的前驱或后继结点只有遍历才能知道,因此线索化的 过程是在对二叉树遍历的过程中修改空指针的过程。
为了记下遍历过程中访问结点的先后次序,可引入指针p 指引遍历,而引入指针pre跟踪p的前驱。首先将pre和p
初始化为遍历的第一结点。然后让p往遍历的方向走找 pre的后继。一旦找到,则它们互为前驱和后继,建立相
应线索。接着将p赋给pre,重复下去直到遍历结束。 二叉树的中序线索化 增加一个头结点,其LeftChild指针指向二叉树的根结
点,其RightChild指针指向中序遍历的最后一个结点。 而最后一个结点的RightChild指针指向头结点。这样一
来,就好象为二叉树建立了一个双向线索链表,既可从 中序遍历的第一个结点起进行中序的遍历;也可从中序
遍历的最后一个结点起进行逆中序的遍历。
在树的运算实现中常常用到递归。
如:
1.求二叉树叶结点数
2.求二叉树高度 H = Max{Hl,Hr} + 1
3.求二叉树结点数 N = Nl+Nr+1
伪代码:
if(!t) return 0;
l = Size(t->left)
r = Size(t->right)
return l+r+1;
思考:给定二叉树前中后序中的任意2个能否唯一确定该树?给出证明和设计算法确定树。
(转)下面是这个问题的证明与结论:
①给定二叉树结点的前序序列和对称序(中序)序列,可以唯一确定该二叉树。
证明:因为前序序列的第一个元素是根结点,该元素将二叉树中序序列分成两部分,左边(设1个元素)表示左子树,若左边无元素,则说明左子树为空;右边(设r个元素)是右子树,若为空,则右子树为空。根据前序遍历中'根-左子树-右子树'的顺序,则由从第二元素开始的1个结点序列和中序序列根左边的1个结点序列构造左子树,由前序序列最后r个元素序列与中序序列根右边的r个元素序列构造右子树。
②由中序序列和先序序列能唯一确定一棵二叉树,但是由先序序列和后序序列不能唯一确定一棵二叉树,因无法确定左右子树两部分。
反例:任何结点只有左子树的二叉树和任何结点只有右子树的二叉树,其前序序列相同,后序序列相同,但却是两棵不同的二叉树。
③已经说明由二叉树的前序序列和中序序列可以确定一棵二叉树,现在来证明由二叉树的中序序列和后序序列,也可以唯一确定一棵二叉树。
证明:
当n=1时,只有一个根结点,由中序序列和后序序列可以确定这棵二叉树。
设当n=m-1时结论成立,现证明当n=m时结论成立。
设中序序列为S1,S2,…,Sm,后序序列是P1,P2,…,Pm。因后序序列最后一个元素Pm是根,则在中序序列中可找到与Pm相等的结点(设二叉树中各结点互不相同)Si(1≤i≤m),因中序序列是由中序遍历而得,所以Si是根结点,S1,S2,…,Si-1是左子树的中序序列,而Si+1,Si+2,…,Sm是右子树的中序序列。
若i=1,则 S1是根,这时二叉树的左子树为空,右子树的结点数是m-1,则{S2,S3,…,Sm}和{P1,P2,…,Pm-1}可以唯一确定右子树,从而也确定了二叉树。
若i=m,则Sm是根,这时二叉树的右子树为空,左子树的结点数是m-1,则{S1,S2,…,Sm-1}和{P1,P2,…,Pm-1}唯一确定左子树,从而也确定了二叉树。
最后,当1 < i < m时,Si把中序序列分成{S1,S2,…,Si-1}和{Si+1,Si+2,…,Sm}。由于后序遍历是'左子树-右子树-根结点',所以{P1,P2,…,Pi-1}和{Pi,Pi+1,…Pm-1}是二叉树的左子树和右子树的后序遍历序列。因而由{S1,S2,…,Si-1}和{P1,P2,…,Pi-1}可唯一确定二叉树的左子树,由{Si+1,Si+2,…,Sm}和{Pi,Pi+1,…,Pm-1}可唯一确定二叉树的右子树。
字典
当集合中的元素有一个线性序,即全集合是一个有序集时,涉及到与这个线性序有关的集合运算的时候,用符号表表示时会很难实现或效率不高,因此引入字典这一抽象数据类型,字典中元素有一个线性序,且支持涉及线性序的一些集合运算。
字典:以有序集为基础的抽象数据类型。
·支持运算:
1.Member(x,S):成员运算
2.Insert(x,S):插入运算
3.Delete(x,S):删除运算
4.Predecessor(x,S):前驱运算(返回集合S中小于x的最大元素)
5.Successor(x,S):后继运算(返回集合S中大于x的最小元素)
6.Range(x,y,S):区间查找运算(返回集合S中介于x和y之间的所有元素组成的集合)
7.Min(S):最小元运算(返回当前集合S中依线性序最小的元素)
用数组实现字典
用数组实现字典与用数组实现符号表的不同之处在于,可以利用线性序将字典中的元素从小到大依序存储在数组中,用数组下标的序关系来反映字典元素之间的序关系。如在这种表述法下,可以用二分查找来实现Menber运算,时间复杂度O(nlogn)。但缺陷在于插入和删除运算效率较低,由于需要移动部分元素,复杂度O(n)。
用二叉搜索树实现字典
· 二叉搜索树是满足以下条件的二叉树:1.左子树上的所有节点值均小于根节点值,2右子树上的所有节点值均不小于根节点值,3,左右子树也满足上述两个条件。
· 二叉搜索树的中序是所有结点的递增序列。由此性质只需知道了前序或后序则可确定一棵二叉搜索树。
· 二叉搜索树的实现(转)<- 链接中有用链表实现的代码
插入:
1.若当前的二叉查找树为空,则插入的元素为根节点
2.若插入的元素值小于根节点值,则将元素插入到左子树中
3.若插入的元素值不小于根节点值,则将元素插入到右子树中。
删除:
1.p为叶子节点,直接删除该节点,再修改其父节点的指针(注意分是根节点和不是根节点),如图a。
2.p为单支节点(即只有左子树或右子树)。让p的子树与p的父亲节点相连,删除p即可;(注意分是根节点和不是根节点);如图b。
3.p的左子树和右子树均不空。找到p的后继y,因为y一定没有左子树,所以可以删除y,并让y的父亲节点成为y的右子树的父亲节点,并用y的值代替p的值;或者方法二是找到p的前驱x,x一定没有右子树,所以可以删除x,并让x的父亲节点成为y的左子树的父亲节点。如图c。
(找前驱时只要在其左子树中不断找右儿子,而后继则是在右子树中不断的往下找左儿子)
· 给定序列x1,x2,...,xn 建立一棵二叉搜索树?
一个方法是把序列元素一个个插入,另一个办法则是先将元素排序然后找出中间的数值放在树根,然后这个序列变被分为两段,递归地分别在左右段中找中间的数值放在左右子树。两种实现都是O(nlogn)
用AVL树实现字典
由于二叉树搜索树在一定条件下可能退化为一条链(如序列1,2,3,4以1为根节点的时候)。这样它相比链表实现的优势已经消失,查找的效率为O(n)。想要使得它的效率维持在O(nlogn)那么就需要用到AVL树。
·AVL树(平衡二叉搜索树):
- 本身首先是一棵二叉搜索树。
- 带有平衡条件:每个结点的左右子树的高度之差的绝对值最多为1。
为方便描述左右子树的高度差,这里引入平衡因子的定义:该结点的左子树高度减右子树高度。
如何在插入、删除过程中维持AVL树?
在依照二叉搜索树的插入删除操作不变的基础上,从这个结点开始不断向根回溯直到找到不平衡的位置然后做旋转操作。不平衡的类型分为LL,RR,LR,RL。LL型时单向右旋,RR时单向左旋。LR,RL时双向旋转(先左后右、先右后左),下面进行详细分析。
·AVL树的旋转操作(转载+修改(原博好像写错了一部分地方))
情况1(LL):对该结点的左儿子的左子树进行了一次插入。(单向右旋得到右图)
解决办法是将x上移一层,并将z下移一层,由于在原树中k2 > k1,所以k2成为k1的右子树,而y是小于k2的,所以成为k2的左子树。
举例:在下图的情况中,插入节点“6”。插入后沿根回找发现“8”子树不平衡,并且是LL的情况。首先我们不考虑其父节点的情况(因为我们创建节点是递归创建的,可以不用考虑其父节点与其的连接)这里“8”的右孩子不发生变化,将其左孩子设为“7”的右孩子,将7的右孩子设为“8”及其子树,然后更新两结点的子树高度值,最后返回“7”节点的指针。
AVLTree SingleRotateWithLeft(PAVLNode k2)
{
PAVLNode k1;
k1 = k2->l;
k2->l = k1->r;
k1->r = k2;
k2->h = MAX( Height( k2->l ), Height( k2->r ) ) + 1;
k1->h = MAX( Height( k1->l ), k2->h ) + 1;
return k1; /* New root */
}
情况4(RR):对该结点的右儿子的右子树进行了一次插入。(单向左旋得到右图)
解决办法是将z上移一层,并将x下移一层,由于在原树中k2 > k1,所以k1成为k2的左子树,而y是大于k1的,所以成为k1的右子树。
举例:在下图的情况下插入节点“6”。插入后沿根回找发现“2”子树不平衡,并且是RR的情况。操作同情况1类似。
AVLTree SingleRotateWithRight(PAVLNode k1)
{
PAVLNode k2;
k2 = k1->r;
k1->r = k2->l;
k2->l = k1;
k1->h = MAX( Height( k1->l ), Height( k1->r ) ) + 1;
k2->h = MAX( Height( k2->r ), k1->h ) + 1;
return k2; /* New root */
}
情况3(LR):对该结点的左儿子的右子树进行了一次插入。(先左旋再右旋)
这种情况是单旋调整不了的,如下图
只能通过左-右双旋调整
所以这里正确做法是,先对k3的左子树k1做一次左旋,
然后对k3做右旋:
AVLTree DoubleRotateWithLeft( PAVLNode k3 )
{
/* Rotate between K1 and K2 */
k3->l = SingleRotateWithLeft( k3->l );
/* Rotate between K3 and K2 */
return SingleRotateWithRight(k3);
}
情况4(RL):对该结点的右儿子的左子树进行了一次插入。(先右旋再左旋)
做法类似,先对k1的右子树进行一次右旋,然后再对k1进行一次左旋。
AVLTree DoubleRotateWithRight( PAVLNode k1 )
{
/* Rotate between K3 and K2 */
k1->r = SingleRotateWithRight( k1->r );
/* Rotate between K1 and K2 */
return SingleRotateWithLeft( k1 );
}
AVL树插入操作完整代码:
AVLTree Insert(Item X, AVLTree T )
{
if( T == NULL )
{
/* Create and return a one-node tree */
T = (PAVLNode)malloc( sizeof(AVLNode ) );
if( T == NULL )
perror("malloc failed");
else
{
T->item = X;
T->h = 0;
T->l = T->r = NULL;
T->count = 1;
}
}
else if(compare(&X,&T->item) == -1)//插入情况1
{
T->l = Insert( X, T->l );
if( Height( T->l ) - Height( T->r ) == 2 )
if(compare(&X, &T->l->item ) == -1)//左边左子树 单旋转
T = SingleRotateWithLeft( T );
else
T = DoubleRotateWithLeft( T );//左边右子树
}
else if( compare(&X,&T->item) == 1 ) //插入情况2
{
T->r = Insert( X, T->r );
if( Height( T->r ) - Height( T->l ) == 2 )
if(compare(&X , &T->r->item) == 1)//右边右子树 单旋转
T = SingleRotateWithRight( T );
else
T = DoubleRotateWithRight( T );//右边左子树
}
else//插入情况3
T->count++;
/* Else X is in the tree already; we'll do nothing */
T->h = MAX( Height( T->l ), Height( T->r ) ) + 1;
return T;
}