本系列文章为浙江大学陈越、何钦铭数据结构学习笔记,前面的系列文章链接如下:
数据结构基础:P1-基本概念
数据结构基础:P2.1-线性结构—>线性表
数据结构基础:P2.2-线性结构—>堆栈
数据结构基础:P2.3-线性结构—>队列
数据结构基础:P2.4-应用实例—>多项式加法运算
数据结构基础:P2.5-应用实例—>多项式乘法与加法运算-C实现
数据结构基础:P3.1-树(一)—>树与树的表示
数据结构基础:P3.2-树(一)—>二叉树及存储结构
数据结构基础:P3.3-树(一)—>二叉树的遍历
数据结构基础:P3.4-树(一)—>小白专场:树的同构-C语言实现
数据结构基础:P4.1-树(二)—>二叉搜索树
前言
从上一节二叉搜索树的例子我们可以看到,查找的效率跟树的高度有关,因为我们可能一直找找找找到叶结点为止。而对同样的n
个结点来讲,树的高度跟树的结构有很大关系。如果树的结构不好,都只有左/右儿子,全串在一起形成一条链往左/右边倾斜,它的高度是n-1
,这是最坏的情况。那这就达不到我们想要的
l
o
g
2
N
{\rm{lo}}{{\rm{g}}_{\rm{2}}}{\rm{N}}
log2N 复杂度。所以总的来讲我们希望这个树看起来比较平衡,不往一边倒。所以这就是我们现在要提到的一个重要的知识点:平衡二叉树。
一、什么是平衡二叉树
对于二叉搜索树来说,它的结点不同的插入顺序会导致这个树的结构不一样,也就是树的深度也会不一样,最终会导致它的查找效率会不一样。
下面我们看看三种不同的结点插入顺序所造成的不同的树的样子:
第一棵树的插入顺序是按照1-12月份的月份顺序来进行插入的。
第二棵树是按照我们指定的一种顺序来插入的。
第三棵树是按照月份字符串的大小(按照月份英文单词的字典顺序)这种顺序来插入。
分析
①平均查找长度
从上面我们可以看到,同样的12个结点按照不同的顺序插入,就形成了不同的二叉搜索树。这些不同的二叉搜索树,它的查找效率是不一样的。所谓查找效率,其衡量指标叫平均查找长度(Average Search Length, ASL)。也就是说,我要找这些月份平均要找几次。ASL计算方式:将每一层的结点总数×这一层的层数,并累加起来,最后除以总的结点数。
因此,对于上面三种插入顺序,其平均查找长度如下:
A S L ( a ) = ( 1 + 2 × 2 + 3 × 3 + 4 × 3 + 5 × 2 + 6 × 1 ) / 12 = 3 . 5 {\rm{ASL}}\left( {\rm{a}} \right){\rm{ = }}\left( {{\rm{1 + 2 \times 2 + 3 \times 3 + 4 \times 3 + 5 \times 2 + 6 \times 1}}} \right){\rm{/12 = 3}}{\rm{.5}} ASL(a)=(1+2×2+3×3+4×3+5×2+6×1)/12=3.5
A S L ( b ) = ( 1 + 2 × 2 + 3 × 4 + 4 × 5 ) / 12 = 3 {\rm{ASL}}\left( {\rm{b}} \right){\rm{ = }}\left( {{\rm{1 + 2 \times 2 + 3 \times 4 + 4 \times 5}}} \right){\rm{/12 = 3}} ASL(b)=(1+2×2+3×4+4×5)/12=3
A S L ( c ) = ( 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 ) / 12 = 6 . 5 {\rm{ASL}}\left( {\rm{c}} \right){\rm{ = }}\left( {{\rm{1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12}}} \right){\rm{/12 = 6}}{\rm{.5}} ASL(c)=(1+2+3+4+5+6+7+8+9+10+11+12)/12=6.5
②平衡因子
从上面可以明显看到,第二个图的平均查找长度是最短的,所以这样的一种树结构是比较好的。这样的树结构的特点大家一眼可以看出来,它总体上给你感觉是比较平衡的,左右两边比较均匀。我们可以使用平衡因子来衡量平衡程度。
平衡因子(Balance Factor,简称BF): B F ( T ) = h L − h R {\rm{BF(T) = }}{{\rm{h}}_{\rm{L}}}{\rm{ - }}{{\rm{h}}_{\rm{R}}} BF(T)=hL−hR,其中 h L {{\rm{h}}_{\rm{L}}} hL 和 h R {{\rm{h}}_{\rm{R}}} hR 分别为T的左、右子树的高度。平衡二叉树(Balanced Binary Tree)不为空时,任一结点的左、右子树高度差的绝对值不超过1,即 ∣ B F ( T ) ∣ ≤ 1 \left| {{\rm{BF}}\left( {\rm{T}} \right)} \right| \le {\rm{1 }} ∣BF(T)∣≤1。
③例子
我们看下面三棵树是否为平衡二叉树:
第一棵树3那个结点左子树高度为2,右子树高度为0,所以不是
第二棵树所有结点的平衡因子绝对值都小于等于1,所以是平衡二叉树
第三棵树的根结点7的左子树高度为3,右子树高度为2,所以不是。
对于有n个结点的完全二叉树,树的高度就是 log 2 n {\log _{\rm{2}}}{\rm{n}} log2n。那么平衡二叉树它的树的高度能达到 log 2 n {\log _{\rm{2}}}{\rm{n}} log2n 这样一个档次吗,这是我们关心的这样的一个问题。那么我们下面我们来推导看看到底它能不能达到。
设 n h {{\rm{n}}_{\rm{h}}} nh 是高度为 h \rm{h} h 的平衡二叉树的最少结点数。结点数最少时,根结点左右子树的高度相差为1,如下所示:
从上图可以看出: n h = n h − 1 + n h − 2 + 1 {{\rm{n}}_{\rm{h}}} = {{\rm{n}}_{{\rm{h - 1}}}} + {{\rm{n}}_{{\rm{h - 2}}}} + 1 nh=nh−1+nh−2+1,即最少结点数等于左子树结点数+右子树结点数+1。看到这个结果让我们想起了斐波那契序列,唯一不同的是,我们这里只是多加了个1。
现在我们一次看看各个深度的平衡二叉树对应的最小结点数是多少。
我们列出平衡二叉树的高度、最小结点数和斐波那契数列的关系,如下所示:
可以看出: n h = F h + 2 − 1 , ( h ≥ 0 ) {{\rm{n}}_{\rm{h}}}{\rm{ = }}{{\rm{F}}_{{\rm{h + 2}}}}{\rm{ - 1, (h}} \ge {\rm{0) }} nh=Fh+2−1,(h≥0)。
而斐波那契数列又可以换算成: F i ≈ 1 5 ( 1 + 5 2 ) i {{\rm{F}}_{\rm{i}}} \approx \frac{{\rm{1}}}{{\sqrt {\rm{5}} }}{\left( {\frac{{{\rm{1 + }}\sqrt {\rm{5}} }}{{\rm{2}}}} \right)^{\rm{i}}} Fi≈51(21+5)i。
所以可以推出: n h ≈ 1 5 ( 1 + 5 2 ) h + 2 {{\rm{n}}_{\rm{h}}} \approx \frac{{\rm{1}}}{{\sqrt {\rm{5}} }}{\left( {\frac{{{\rm{1 + }}\sqrt {\rm{5}} }}{{\rm{2}}}} \right)^{{\rm{h + 2}}}} nh≈51(21+5)h+2。所以取对数,
可以得到: h = O ( l o g 2 n ) {\rm{h = O(lo}}{{\rm{g}}_{\rm{2}}}{\rm{n)}} h=O(log2n)。
最终可以得出结论:给定结点数为 n 的AVL树的最大高度为 O ( l o g 2 n ) {\rm{O(lo}}{{\rm{g}}_{\rm{2}}}{\rm{n)}} O(log2n)。
二、平衡二叉树的调整
平衡二叉树它也是一个搜索树,搜索树的查找、插入、删除之类的操作在平衡二叉树上面也可以做。这里就碰到一个问题,当我插入的时候,会把原来平衡的一棵树变得不平衡了,这个时候怎么办。
我们看个例子
我们现在有一个树的根结点Mar,其平衡因子为0。
现在来了一个新的结点May,现在这棵树还是平衡二叉树,根结点的平衡因子为-1。
接下来又来了一个November,那么它插在May的右边,这一插上去这棵树就不平衡了。
2.1 RR旋转
所以我们要调整,很自然的一个想法就是调整成一个根结点,然后左边右边各一个就平衡了。大家要注意平衡二叉树是一个搜索树,后面所有的调整一定要保证它仍然是搜索树。对这样的一种调整方法,可以把它抽象成这种模式:就是说我这个树本来是平衡的,后来来了一个结点插入的时候,某个结点平衡点被破坏了,所以这个时候我们需要调。我们要去发现谁破坏了谁,这个Mar是平衡被破坏的发现者。是谁导致了不平衡呢,是有一个麻烦结点Nov。它们两者的关系是,麻烦的结点是挂在发现者的右子树的右子树上,这种旋转叫做RR旋转(Right Right Rotation),这种插入又叫RR插入,所以这是平衡树调整的第一种模式。
把它概括一下大概是这样的一种模式:就是我原来有一个二叉树是平衡的(下图所示),现在插入了某个结点( B R {{\rm{B}}_{\rm{R}}} BR下面那个结点)使得某个结点(结点 A \rm{A} A)平衡性被破坏了。那么插入的这个结点挂在被破坏的结点的右子树的右子树上,这个时候我们做RR旋转。它的基本过程为:把被破坏的结点的右子树( B \rm{B} B)拎上来,然后被破坏的结点A作为B的左儿子。那B原来的左儿子 B L {{\rm{B}}_{\rm{L}}} BL应该放哪里呢?它比B小,比A大,所以要挂在A的右边。
我们来看两个例子:
现在这平衡二叉树插入一个结点15,按照RR旋转后成为以下结构。
现在这颗平衡二叉树插入一个结点13,按照RR旋转后成为以下结构。
2.2 LL旋转
现在我有这么一颗平衡二叉树
现在我要插入八月份Apr,此时May和Mar的平衡性被破坏。
如果有两个结点平衡性被破坏,我们只处理最下面的那个就行。因此,类似于RR旋转,我们进行LL旋转(左单旋)。把被破坏者Mar的左子树Aug提起来,被破坏者Mar作为其右子树。
我们也可以用一个基本的框架把它画出来。
2.3 LR旋转
我们在这棵树上插入一个结点 January
②根据大小它应该是插在Mar的左边,此时破坏者(Jan)挂在破坏发现者May左子树的右子树上。这个时候我们要重点关注这三个结点(May、Aug、Mar)。
③要把这三个结点调平衡,根据大小关系应该调成这样。
最后调整结果如下:
我们也可以用一个基本框架把这个这个过程画下来:
2.4 RL旋转
我们现在有这样一棵平衡二叉树
现在插入一个结点Feb,它破坏了Aug的平衡性。现在要将Aug、Jan、Dec这三个结点调平衡。
这三个结点的调整后的关系如下所示。
最后调整结果如下:
我们也可以用一个基本框架把这个这个过程画下来:
我们看个例子
调整后结果如下:
三、代码实现
左单旋与左右双旋的代码如下:
typedef struct AVLNode *Position;
typedef Position AVLTree; /* AVL树类型 */
struct AVLNode{
ElementType Data; /* 结点数据 */
AVLTree Left; /* 指向左子树 */
AVLTree Right; /* 指向右子树 */
int Height; /* 树高 */
};
int Max ( int a, int b )
{
return a > b ? a : b;
}
AVLTree SingleLeftRotation ( AVLTree A )
{ /* 注意:A必须有一个左子结点B */
/* 将A与B做左单旋,更新A与B的高度,返回新的根结点B */
AVLTree B = A->Left;
A->Left = B->Right;
B->Right = A;
A->Height = Max( GetHeight(A->Left), GetHeight(A->Right) ) + 1;
B->Height = Max( GetHeight(B->Left), A->Height ) + 1;
return B;
}
AVLTree DoubleLeftRightRotation ( AVLTree A )
{ /* 注意:A必须有一个左子结点B,且B必须有一个右子结点C */
/* 将A、B与C做两次单旋,返回新的根结点C */
/* 将B与C做右单旋,C被返回 */
A->Left = SingleRightRotation(A->Left);
/* 将A与C做左单旋,C被返回 */
return SingleLeftRotation(A);
}
/*************************************/
/* 对称的右单旋与右-左双旋请自己实现 */
/*************************************/
AVLTree Insert( AVLTree T, ElementType X )
{ /* 将X插入AVL树T中,并且返回调整后的AVL树 */
if ( !T ) { /* 若插入空树,则新建包含一个结点的树 */
T = (AVLTree)malloc(sizeof(struct AVLNode));
T->Data = X;
T->Height = 0;
T->Left = T->Right = NULL;
} /* if (插入空树) 结束 */
else if ( X < T->Data ) {
/* 插入T的左子树 */
T->Left = Insert( T->Left, X);
/* 如果需要左旋 */
if ( GetHeight(T->Left)-GetHeight(T->Right) == 2 )
if ( X < T->Left->Data )
T = SingleLeftRotation(T); /* 左单旋 */
else
T = DoubleLeftRightRotation(T); /* 左-右双旋 */
} /* else if (插入左子树) 结束 */
else if ( X > T->Data ) {
/* 插入T的右子树 */
T->Right = Insert( T->Right, X );
/* 如果需要右旋 */
if ( GetHeight(T->Left)-GetHeight(T->Right) == -2 )
if ( X > T->Right->Data )
T = SingleRightRotation(T); /* 右单旋 */
else
T = DoubleRightLeftRotation(T); /* 右-左双旋 */
} /* else if (插入右子树) 结束 */
/* else X == T->Data,无须插入 */
/* 别忘了更新树高 */
T->Height = Max( GetHeight(T->Left), GetHeight(T->Right) ) + 1;
return T;
}
小测验
1、将1、2、3、4、5、6顺序插入初始为空的AVL树中,当完成这6个元素的插入后,该AVL树共有多少层?
A. 2
B. 3
C. 4
D. 5
答案:B
2、 若一AVL树的结点数是21,则该树的高度至多是多少?注:只有一个根节点的树高度为0
A. 4
B. 5
C. 6
D. 7
答案:B