不同结构的二叉查找树,查找效率有很大的不同(单支树结构的查找效率退化成了顺序查找)。如何解决这个问题呢?关键在于如何最大限度的减小树的深度。正是基于这个想法,平衡二叉树出现了。
平衡二叉树的定义 (AVL—— 发明者为Adel’son-Vel’skii 和 Landis)
平衡二叉查找树,又称 AVL树。 它除了具备二叉查找树的基本特征之外,还具有一个非常重要的特点:它 的左子树和右子树都是平衡二叉树,且左子树和右子树的深度之差的绝对值(平衡因子 ) 不超过1。 也就是说AVL树每个节点的平衡因子只可能是-1、0和1(左子树高度减去右子树高度)。
那么如何是二叉查找树在添加数据的同时保持平衡呢?基本思想就是:当在二叉排序树中插入一个节点时,首先检查是否因插入而破坏了平衡,若 破坏,则找出其中的最小不平衡二叉树,在保持二叉排序树特性的情况下,调整最小不平衡子树中节点之间的关系,以达 到新的平衡。所谓最小不平衡子树 指离插入节点最近且以平衡因子的绝对值大于1的节点作为根的子树。
平衡二叉树的操作
查找操作
平衡二叉树的查找基本与二叉查找树相同。插入操作
在平衡二叉树中插入结点与二叉查找树最大的不同在于要随时保证插入后整棵二叉树是平衡的。那么调整不平衡树的基本方法就是: 旋转 。 下面我们归纳一下平衡旋转的4种情况:
这里我们把必须重新平衡的节点叫做A。
平衡因子:左子树的深度减去右子树的深度。
相关旋转图示见 严蔚敏《数据结构》(C语言版)P235- 单旋转–向右旋转平衡处理–在A的左儿子的左子树进行一次插入。此时平衡因子变为2。
- 单旋转–向左旋转平衡处理–在A的右儿子的右子树进行一次插入操作。此时平衡因子变为-2.
- 双旋转–先右后左旋转平衡处理–在A的左儿子的右子树进行一次插入操作。此时平衡因子变为2.
- 双旋转–先左后右平衡处理–在A的右儿子的左子树进行一次插入操作。此时平衡因子变为-2.
代码实现:
/**
*AVL 树 插入新节点 的实现
*/
struct AVLNode{
int val;
AVLNode* left;
AVLNode* right;
int height;
AVLNode(const int& value, AVLNode* lt, AVLNode* rt, int h = 0):val(value), left(lt),right(rt),height(h){}
};
/**
*Return thr height of node t ot -1 if NULL
* /
int height(AVLNode* t) const
{
return t == NULL ? -1 : t->height;
}
void insert(const int& x, AVLNode* & t)
{
if(t == NULL) //empty Tree
t = new AVLNode(x, NULL, NULL);
else if(x < t->val) //插入到左儿子
{
//insert into left subtree
insert(x, t->left);
if(height(t->left) - height(t->right) == 2)
{
if(x < t->left->val) //插入到了左儿子的左子树中 ,向右单旋转
rotateWithLeftChild(t);
else //插入到左儿子的右子树, 先左后右双旋转
doubleRotateWithLeftChild(t);
}
}
else if(x > t->val) //插入到右儿子
{
insert(x, t->right);
if(height(t->right) - height(t->left) == 2)
{
if(x > t->right->val)
rotateWithRightChild(t); //插入到右儿子的右子树, 向左单旋转
else if(x < t->right->val)
doubleRotateWithRightChild(t); //插入到右儿子的左子树, 先右后左双旋转
}
}
else {
//待插入节点已经存在, do nothing
}
t->height = max(height(t->left), height(t->right)) + 1;
}
//向右单旋转
void rotateWithLeftChild( AVLNode* & root)
{
AVLNode *newRoot = root->left;
root->left = newRoot->right;
newRoot->right = root;
root->height = max(height(root->left), height(root->right)) + 1;
newRoot->height = max(height(newRoot->left), root->height) + 1;
root = newRoot; //root 指向新的root节点
}
//向左单旋转
void rotateWithRightChild(AVLNode* & root)
{
AVLNode* newRoot = root->right;
root->right = newRoot->left;
newRoot->left = root;
root->height = max(height(root->left), height(root->right)) + 1;
newRoot->height = max(height(newRoot->right), root->height) + 1;
root = newRoot;
}
//先左后右双旋转
void doubleRotateWithLeftChild(AVLNode* & root)
{
rotateWithRightChild(root->left); //先对root的左子树进行向左单旋转
rotateWithLeftChild(root); //然后对root进行向右单旋转
}
//先右后左双旋转
void doubleRotateWithRightChild(AVLNode* & root)
{
rotateWithLeftChild(root->right); //先对root的右子树进行向右单旋转
rotateWithRightChild(root); //然后对root进行向左单旋转
}
平衡二叉树性能分析
平衡二叉树的性能优势:
很显然,平衡二叉树的优势在于不会出现普通二叉查找树的最差情况。其查找的时间复杂度为O(logN)。
在平衡树上进行查找的过程和二叉排序树相同,因此,在查找的过程中和给定值进行比较的关键字个数不超过树的深度。
那么,
含有n个关键字的平衡树的最大深度是多少呢?
为了解答这个问题,
可以借助 深度为h的平衡树所具有的最少节点数的计算公式:
最少节点数
S(h) = S(h - 1) + S(h - 2) + 1
其中,h = 0, S(h) = 1; h = 1, S(h) = 2.
得到h的最大值。
平衡二叉树的缺陷:
(1) 很遗憾的是,为了保证高度平衡,动态插入和删除的代价也随之增加。因此,我们在下一专题中讲讲《红黑树》 这种更加高效的查找结构。
(2) 所有二叉查找树结构的查找代价都与树高是紧密相关的,能否通过减少树高来进一步降低查找代价呢。我们可以通过多路查找树的结构来做到这一点,在后面专题中我们将通过《多路查找树/B-树/B+树 》来介绍。
(3) 在大数据量查找环境下(比如说系统磁盘里的文件目录,数据库中的记录查询 等),所有的二叉查找树结构(BST、AVL、RBT)都不合适。如此大规模的数据量(几G数据),全部组织成平衡二叉树放在内存中是不可能做到的。那么把这棵树放在磁盘中吧。问题就来了:假如构造的平衡二叉树深度有1W层。那么从根节点出发到叶子节点很可能就需要1W次的硬盘IO读写。大家都知道,硬盘的机械部件读写数据的速度远远赶不上纯电子媒体的内存。 查找效率在IO读写过程中将会付出巨大的代价。在大规模数据查询这样一个实际应用背景下,平衡二叉树的效率就很成问题了。对这一问题的解决:我们也会在《多路查找树/B-树/B+树 》 将详细分析。
上面提到的红黑树和多路查找树都是属于深度有界查找树(depth-bounded tree —DBT)