各种树结构之二 AVL树
闲话不多说
接着来
二叉搜索树的深度与搜索效率
一个有n个节点的二叉树,它的最小深度为log(n),最大深度为n。比如下面两个二叉树:
深度为n
这两个二叉树同时也是二叉搜索树(参考这里)。注意,log以2为基底。log(n)是指深度的量级。根据我们对深度的定义,精确的最小深度为floor(log(n)+1)。
我们将处于同一深度的节点归为一层。如果除最后一层外的其他层都被节点填满时,二叉树有最小深度log(n)。
二叉搜索树的深度越小,那么搜索所需要的运算时间越小。一个深度为log(n)的二叉搜索树,搜索算法的时间复杂度也是log(n)。然而,我们在二叉搜索树中已经实现的插入和删除操作并不能让保持log(n)的深度。如果我们按照8,7,6,5,4,3,2,1的顺序插入节点,那么就是一个深度为n的二叉树。那么,搜索算法的时间复杂度为n。
n和log(n)的时间复杂度意味着什么呢?时间复杂度代表了完成算法所需要的运算次数。时间复杂度越小,算法的速度越快。
可以看到,随着元素的增加,log(n)的时间复杂度的增长要远小于n。所以,我们自然希望二叉搜索树能尽可能保持log(n)的深度。在上面深度为n的例子中,我们发现,每个节点只有左节点被填满。树的每一层都有很多空位。能不能尽可能减少每一层的空位呢? (相应的,减少树的深度)
一种想法是先填满一层,再去填充下一层,这样就是一个完全二叉树(complete binary tree)。这样的二叉树实现插入算法会比较复杂。所以今天介绍一种思路相似,但比较容易实现的树状数据结构——AVL树。
AVL树
AVL树是最先发明的自平衡二叉查找树,也被称为高度平衡树。相比于”二叉查找树”,它的特点是:AVL树中任何节点的两个子树的高度最大差别为1。
如下图:
AVL树的查找、插入和删除在平均和最坏情况下都是O(logn)。
如果在AVL树中插入或删除节点后,使得高度之差大于1。此时,AVL树的平衡状态就被破坏,它就不再是一棵二叉树;为了让它重新维持在一个平衡状态,就需要对其进行旋转处理。学AVL树,重点的地方也就是它的旋转算法;在后文的介绍中,再来对它进行详细介绍。
AVL树的实现
1.基本定义
1.1树的节点
typedef int datatype; typedef struct avltrees { datatype key; int height; struct avltrees *rson; struct avltrees *lson; }avlnode,*avltree;
1.2获取高度
#define height(p) ((p==NULL) ? -1 :(((avlnode *)(p))->height))
1.3 计算高度
两子树高度最大者加一
int count_height(avlnode *x) { return (max(height(x->lson),height(x->rson))+1) }
2. 旋转
前面说过,如果在AVL树中进行插入或删除节点后,可能导致AVL树失去平衡。这种失去平衡的可以概括为4种姿态:LL(左左),LR(左右),RR(右右)和RL(右左)。下面给出它们的示意图:
总的来说,AVL树失去平衡时的情况一定是LL、LR、RL、RR这4种之一,它们都由各自的定义:
(1) LL:LeftLeft,也称为”左左”。插入或删除一个节点后,根节点的左子树的左子树还有非空子节点,导致”根的左子树的高度”比”根的右子树的高度”大2,导致AVL树失去了平衡。
例如,在上面LL情况中,由于”根节点(8)的左子树(4)的左子树(2)还有非空子节点”,而”根节点(8)的右子树(12)没有子节点”;导致”根节点(8)的左子树(4)高度”比”根节点(8)的右子树(12)”高2。
(2) LR:LeftRight,也称为”左右”。插入或删除一个节点后,根节点的左子树的右子树还有非空子节点,导致”根的左子树的高度”比”根的右子树的高度”大2,导致AVL树失去了平衡。
例如,在上面LR情况中,由于”根节点(8)的左子树(4)的左子树(6)还有非空子节点”,而”根节点(8)的右子树(12)没有子节点”;导致”根节点(8)的左子树(4)高度”比”根节点(8)的右子树(12)”高2。
(3) RL:RightLeft,称为”右左”。插入或删除一个节点后,根节点的右子树的左子树还有非空子节点,导致”根的右子树的高度”比”根的左子树的高度”大2,导致AVL树失去了平衡。
例如,在上面RL情况中,由于”根节点(8)的右子树(12)的左子树(10)还有非空子节点”,而”根节点(8)的左子树(4)没有子节点”;导致”根节点(8)的右子树(12)高度”比”根节点(8)的左子树(4)”高2。
(4) RR:RightRight,称为”右右”。插入或删除一个节点后,根节点的右子树的右子树还有非空子节点,导致”根的右子树的高度”比”根的左子树的高度”大2,导致AVL树失去了平衡。
例如,在上面RR情况中,由于”根节点(8)的右子树(12)的右子树(14)还有非空子节点”,而”根节点(8)的左子树(4)没有子节点”;导致”根节点(8)的右子树(12)高度”比”根节点(8)的左子树(4)”高2。
前面说过,如果在AVL树中进行插入或删除节点后,可能导致AVL树失去平衡。AVL失去平衡之后,可以通过旋转使其恢复平衡,下面分别介绍”LL(左左),LR(左右),RR(右右)和RL(右左)”这4种情况对应的旋转方法。
2.1 LL的旋转
LL失去平衡的情况,可以通过一次旋转让AVL树恢复平衡。如下图:
图中左边是旋转之前的树,右边是旋转之后的树。从中可以发现,旋转之后的树又变成了AVL树,而且该旋转只需要一次即可完成。
对于LL旋转,你可以这样理解为:LL旋转是围绕”失去平衡的AVL根节点”进行的,也就是节点k2;而且由于是LL情况,即左左情况,就用手抓着”左孩子,即k1″向上拔。将k1变成根节点,k2变成k1的右子树,”k1的右子树”变成”k2的左子树”。
LL的旋转代码
avlnode *left_left_rotation(avlnode *k2) { avlnode *k1=k2->lson; k2->lson=k1->rson; k1->rson=k2; k1->height=count_height(k1); k2->height=count_height(k2); return k1; }
2.2 RR的旋转
理解了LL之后,RR就相当容易理解了。RR是与LL对称的情况!RR恢复平衡的旋转方法如下:
图中左边是旋转之前的树,右边是旋转之后的树。RR旋转也只需要一次即可完成。
RR的旋转代码
avlnode *right_right_rotation(avlnode *k1) { avlnode *k2=k1->rson; k1->rson=k2->lson; k2->lson=k1; k2->height=count_height(k1); k2->height=count_height(k2); return k2; }
2.3 LR的旋转
LR失去平衡的情况,需要经过两次旋转才能让AVL树恢复平衡。如下图:
第一次旋转是围绕”k1″进行的”RR旋转”,第二次是围绕”k3″进行的”LL旋转”。
LR的旋转代码
avlnode *left_right_rotation(avlnode *k3) { k3->lson=right_right_rotation(k3->lson); return left_left_rotation(k3); }
2.4 RL的旋转
RL是与LR的对称情况!RL恢复平衡的旋转方法如下:
第一次旋转是围绕”k3″进行的”LL旋转”,第二次是围绕”k1″进行的”RR旋转”。
RL的旋转代码
avlnode *right_left_rotation(avlnode *k1) { k1->rson=left_left_rotation(k1->rson); return right_right_rotation(k1); }
3. 插入
对于AVL树,可以证明,在新增一个节点时,总可以通过一次旋转恢复AVL树的性质。
当我们插入一个新的节点时,在哪里旋转?是用单旋转还是双旋转?
我们按照如下基本步骤进行:
1. 按照二叉搜索树的方式增加节点,新增节点称为一个叶节点。
2. 从新增节点开始,回溯到第一个失衡节点(5)。
(如果回溯到根节点,还没有失衡节点,就说明该树已经符合AVL性质。)
3. 找到断的边(5->3),并确定断弦的方向(5的左侧)
4. 以断边下端(3)为根节点,确定两个子树中的哪一个深度大(左子树还是右子树)。
(这两棵子树的深度不可能相等,而且深度大的子树包含有新增节点。)
5. 如果第2和第3步中的方向一致(都为左或者都为右),需要单旋转以失衡节点为根节点的子树。
否则,双旋转以失衡节点为根节点的子树。
插入节点的代码
avlnode *tree_insert(avltree t,datatype key) { if (t==NULL) { t=create_node(key,NULL,NULL); } else if (key < t->key) { t->lson=tree_insert(t->lson,key); if (height(t->lson)-height(t->rson)==2) { //if (t->lson->rson==NULL) if (key<t->lson->key) t=left_left_rotation(t); else t=left_right_rotation(t); } } else if (key > t->key) { t->rson=tree_insert(t->rson,key); if (height(t->rson)-height(t->lson)==2) { //if (t->rson->lson==NULL) if (key>t->rson->key) t=right_right_rotation(t); else t=right_left_rotation(t); } } else { error(); } t->height=count_height(t); return t; }
4.删除
删除的方案有很多,但一般都会旋转下面这种,因为对整棵树各个分支深度的影响较小。
a.当被删除节点n是叶子节点,直接删除
b.当被删除节点n只有一个孩子,删除n,用孩子替代该节点的位置
c.当被删除结点n存在左右孩子时,真正的删除点是左子树最大的节点或右子树中最小的节点,之后n的值替换为真正删除点的值。这就把c归结为a,b的问题。
删除节点的代码
avlnode *delete_node(avltree t,avlnode *x) { if (t==NULL || x==NULL) return NULL; if (x->key < t->key) { t->lson=delete_node(t->lson,x); if (height(t->rson)-height(t->lson)==2) { if (height(t->rson->lson)<height(t->rson->rson)) right_right_rotation(t); else right_left_rotation(t); } } else if (x->key > t->key) { t->rson=delete_node(t->rson,x); if (height(t->lson)-height(t->rson)==2) { if (height(t->lson->lson)>height(t->lson->rson)) t=left_left_rotation(t); else t=left_right_rotation(t); } } else if (x->key == t->key) { if (t->lson && t->rson) { if (height(t->lson)>height(t->rson)) { avlnode *y; y=tree_max(x->lson); t->key=y->key; t->lson=delete_node(t->lson,y); } else { avlnode *y; t=tree_min(t->rson); t->key=y->key; t->rson=delete_node(t->rson,y); } } else { avlnode *y=t; t=t->lson ? t->lson : t->rson; free(t); } } else { error(); } return t; }
其他的操作和bst一样
参见 这里
The end.