背景:该准备实习了,接下来这段时间将总结一些常见的树,这次就先从AVL开始。
AVL定义
- 空树为AVL
- 任何一个节点,左子树上的所有值均小于该节点的值,右子树上的所有值均大于该节点的值
- 任何一个节点,其左右子树的高度差不超过1
- 任何一个节点,其左子树和右子树均为AVL
通过对AVL的定义,我们能抽象出AVL节点的定义如下:
struct Node{
int val;//节点保存的值
Node* left;//左子树
Node* right;//右子树
int height;//以该节点为根的树的高度
Node(int val) : val(val), left(NULL), right(NULL), height(1) {}
}
查找元素
由于AVL是一个二叉搜索树,可以通过二叉搜索树的搜索方法来进行搜索,其时间复杂度为lgn。
下面分别给出AVL查找元素的递归和非递归实现方式。
//AVL查找元素的递归版本
Node* search(Node* node, int target){
if(node == NULL) return NULL;
if(target == node->val)
return node;
else if(target > node->val)
return search(node->right, target);
else
return search(node->left, target);
}
//AVL查找元素的非递归版本
Node* search(Node* node, int target){
if(node == NULL) return node;
Node* cur = node;
while(cur){
if(target == node->val)
return cur;
else if(target > node->val)
cur = cur->right;
else
cur = cur->left;
}
return cur;
}
添加元素
AVL添加元素是比较麻烦的一个操作,因为随便一个插入就可能破坏掉AVL的平衡属性。参考《算法笔记》,提出一个平衡因子的概念,每个节点平衡因子的值为左子树高度减去右子树高度。根据AVL的定义,正常情况下,AVL中每个节点的平衡因子只有0,1,-1三种情况。当插入节点破坏平衡性时,插入节点的父节点的平衡因子会变为2或者-2,此时就需要对插入节点的所有祖先节点进行调整使其平衡因子恢复正常。
通过对插入情况的分析并抽象,插入后破坏平衡性的情况有如下四种:
-
父节点平衡因子为1,祖父节点平衡因子为2
在该情况下,以中间节点为轴进行右旋,此时树中所有节点的平衡因子都恢复正常。
-
父节点平衡因子为-1,祖父节点平衡因子为2
在该情况下,先以最底层节点为轴进行左旋,之后以中间节点为轴进行右旋,此时树中所有节点的平衡因子都恢复正常。
-
父节点平衡因子为1,祖父节点平衡因子为-2
在该情况下,先以最底层节点为轴进行右旋,之后以中间节点为轴进行左旋,此时树中所有节点的平衡因子都恢复正常。
-
父节点平衡因子为-1,祖父节点平衡因子为-2
在该情况下,以中间节点为轴进行左旋,此时树中所有节点的平衡因子都恢复正常。
插入元素的代码实现如下:
void leftRotate(Node* &node){
Node* right = node->right;
node->right = right->left;
right->left = node;
node = right;
}
void rightRotate(Node* &node){
Node* left = node->left;
node->left = left->right;
left->right = node;
node = left;
}
bool insert(Node* node, int val){
//节点为空则直接插入
if(node == NULL){
node = new Node(val);
return true;
}
//插入值已存在,则返回插入错误
if(node->val == val)
return false;
else if(node->val > val){
if(insert(node->left, val)){
//更新节点高度
updateHeight(node);
//由于是往左子树插入,因此当平衡因子异常时只能变为2
if(getBanlancedFactor(node) == 2){
int factor = getBanlancedFactor(node->left);
if(factor == 1){
//情况1:父节点平衡因子为1,祖父节点为2
//右旋中间节点
rightRotate(node);
}else if(factor == -1){
//情况2:父节点平衡因子为-1,祖父节点为2
//左旋底层节点,再右旋中间节点
leftRotate(node->left);
rightRotate(node);
}
}
return true;
}else
return false;
}else{
if(insert(node->right, val)){
//更新节点高度
updateHeight(node);
//由于是往右子树插入,因此当平衡因子异常时只能变为-2
if(getBanlancedFactor(node) == -2){
int factor = getBanlancedFactor(node->left);
if(factor == 1){
//情况3:父节点平衡因子为1,祖父节点为-2
//右旋底层节点,再左旋中间节点
rightRotate(node->right);
leftRotate(node);
}else if(factor == -1){
//情况4:父节点平衡因子为-1,祖父节点为-2
//左旋中间节点
leftRotate(node);
}
}
return true;
}else
return false;
}
}
删除元素
删除元素相较于插入元素更是复杂,为何这样说呢?我们分析一下上面插入元素的过程,四种情况下,假设原来子树的高度为n,在插入元素后高度为n+1,进行旋转调节操作后,新子树的高度仍然为n,对该子树外的其他节点没有一点影响,因此如果插入元素导致破坏平衡性,只需要在第一个平衡因子为2或者-2的节点做一次或者两次旋转操作就能恢复平衡状态。而在删除时的旋转调整过程就不仅仅是一次了,以下图为例,当删除元素16时,以15为根根节点的子树平衡状态被破坏,为上述的第二种情况,因此需要通过先左旋后右旋操作调整为平衡状态,调整后该子树高度减1,这又可能导致该子树的祖先节点的平衡状态被破坏,如下图,以9为根节点的子树平衡状态被破坏,为上述的第一种情况,需要通过右旋操作进行调整。以此类推,删除一个节点可能导致该节点的所有祖先节点都进行旋转调整。
删除元素的代码实现如下:
bool erase(Node* &node, int val){
if(node == NULL) return false;
if(node->val == val){
delete node;
node = NULL;
return true;
}else if(node->val < val){
if(erase(node->right), val){
//更新节点高度
updateHeight(node);
//由于是从右子树删除,因此当平衡因子异常时只能变为2
if(getBanlancedFactor(node) == 2){
int factor = getBanlancedFactor(node->left);
if(factor == -1){
//情况2:父节点平衡因子为-1,祖父节点为2
//左旋底层节点,再右旋中间节点
leftRotate(node->left);
rightRotate(node);
}else if(factor == 1){
//情况1:父节点平衡因子为1,祖父节点为2
//右旋中间节点
rightRotate(node);
}
}
return true;
}else
return false;
}else{
if(erase(node->left), val){
//更新节点高度
updateHeight(node);
//由于是从左子树删除,因此当平衡因子异常时只能变为-2
if(getBanlancedFactor(node) == -2){
int factor = getBanlancedFactor(node->left);
if(factor == -1){
//情况4:父节点平衡因子为-1,祖父节点为-2
//左旋中间节点
leftRotate(node);
}else if(factor == 1){
//情况3:父节点平衡因子为1,祖父节点为-2
//右旋底层节点,再左旋中间节点
rightRotate(node->left);
leftRotate(node);
}
}
return true;
}else
return false;
}
}
参考文献
胡凡, 曾磊.《算法笔记》[M].机械工业出版社, 2016.
至此,平衡二叉树的知识点都总结了一次,如果有什么不足的地方请大家不吝赐教,之后会再总结一下红黑树和B树。