基础算法–平衡二叉树
在阅读本章之前建议先阅读基础算法–二叉排序树
定义
AVL树本质上还是一棵二叉搜索树,它的特点是
- 本身首先是一棵二叉搜索树
- 带有平衡条件:每个结点的左右子树的高度之差的绝对值最多为 1 1 1
作用
我们在基础算法–二叉排序树中提到过,如果输入元素是降序或者升序的,那么二叉排序树将退化为链表,因此插入、删除、查找的时间复杂度将会变为 O ( n ) O(n) O(n)。而平衡二叉树每个节点的左右子树高度之差的绝对值最多为 1 1 1,因此不管输入元素怎么样,插入、删除、查找的时间复杂度始终保持 O ( l o g 2 n ) O(log_2n) O(log2n)
我们按顺序将一组数据 1 , 2 , 3 , 4 , 5 {1, 2, 3, 4, 5} 1,2,3,4,5分别插入到一颗空二叉排序树和平衡二叉树中,插入的结果如下图:
显然同样的结点,由于插入方式不同导致树的高度也有所不同。特别是在带插入结点个数很多且正序的情况下,会导致二叉排序树的高度是 n n n,而平衡二叉树树就不会出现这种情况,树的高度始终是 ⌊ l o g 2 n ⌋ \lfloor log_2n \rfloor ⌊log2n⌋
操作
平衡二叉树的基本操作与二叉排序树基本相同,我们主要关注变化比较大的两个操作:插入和删除。在此我们依然是先定义平衡二叉树的节点
struct Node {
int val;
int depth;
Node *left;
Node *right;
Node *parent;
Node(int v) {
val = v;
depth = 0;
left = nullptr;
right = nullptr;
parent = nullptr;
}
};
注意parent
和depth
并不是必须的,我们只是为了方便实现引入的。值得一提的是depth
,depth
是当前节点子树的深度,用于计算平衡因子。这里规定平衡因子为右子树深度减左子树深度。
插入
平衡二叉树也是一颗二叉排序树,因此在插入一个节点时首先还是找到要插入的位置。与二叉排序树不同的是,平衡二叉树插入节点后需要对节点的深度进行调整,因为节点的深度会直接影响此节点的平衡因子。如果插入节点后平衡因子的绝对值大于 1 1 1,则还需要对该树进行旋转,使得这颗树在次平衡。
-
找到插入的位置,并将节点插入。如果根节点为空,则直接返回即可。这一步与二叉排序树基本一致,就不在展开叙述
-
更新树的深度,计算平衡因子。插入一个节点后对原平衡二叉树中部分节点的深度可能产生影响,因此我们需要更新受影响节点的深度,从而得出准确的平衡因子。我们基于下面这颗平衡二叉树来讨论插入节点后对各个节点深度的影响
-
插入一个节点只会影响根节点到其父节点路径上的节点的深度(即只会影响这些节点的平衡因子)。在上面这颗平衡二叉树上插入一个节点 0.9 0.9 0.9,此时只会影响 5 , 3 , 1 , 0 5, 3, 1, 0 5,3,1,0这些节点的深度。
-
插入节点后,如果插入节点的父节点的深度没有发生变化,那么插入节点后整棵树的高度也不会发生变化,则只影响了父节点的平衡因子。如我们往平衡二叉树里插入一个节点 7.5 7.5 7.5,父节点的深度没有发生变化,但是平衡因子从 1 1 1变成了 0 0 0,其它节点的平衡因子均不会发生变化
-
插入节点后,插入节点的父节点的深度发生变化,在继续向上更新过程中如果某一个节点的深度没有发生变化,则停止更新,最坏的情况时一直更新到根节点
void update_depth(Node *node) { if (node == nullptr) return; int depth_l_child = get_depth(node->left); int depth_r_child = get_depth(node->right); node->depth = std::max(depth_l_child, depth_r_child) + 1; }
-
-
再次平衡。向平衡二叉树中插入一个节点后,节点的平衡因子可能会发生变化,因此需要对节点的平衡因子进行调整。但是,调整后的节点的平衡因子的绝对值可能会大于 1 1 1,也就是说插入一个节点后不在是一颗平衡二叉树。因此,需要通过旋转将树旋转成一颗平衡二叉树。平衡二叉树的失衡调整主要是通过旋转最小失衡子树(在新插入的结点向上查找,以第一个平衡因子的绝对值超过 1 1 1的结点为根的子树称为最小失衡子树。也就是说,一棵失衡的树,是有可能有多棵子树同时失衡的。而这个时候,我们只要调整最小的不平衡子树,就能够将不平衡的树调整为平衡的树)来实现的。根据旋转的方向有两种处理方式,左旋 与右旋。但是有一些情况我们只做一次循环依然不能使得二叉树恢复平衡,因此我们需要需要左旋右旋与右旋左旋
-
左旋
- 节点的右孩子替代此节点位置 —— 节点 66 66 66的右孩子是节点 77 77 77,将节点 77 77 77代替节点 66 66 66的位置
- 右孩子的左子树变为该节点的右子树 —— 节点 77 77 77的左子树为节点 75 75 75,将节点 75 75 75挪到节点 66 66 66的右子树位置
- 节点本身变为右孩子的左子树 —— 节点 66 66 66变为了节点 77 77 77的左子树
-
右旋
- 节点的左孩子代表此节点
- 节点的左孩子的右子树变为节点的左子树
- 将此节点作为左孩子节点的右子树
-
左旋右旋
- 先将失衡节点的左孩子作为失衡节点进行一次左旋
- 将失衡节点进行一次右旋
-
右旋左旋
- 先将失衡节点的左孩子作为失衡节点进行一次右旋
- 将失衡节点进行一次左旋
-
-
根据上面推论,我们对旋转做如下总结
节点平衡因子 左孩子平衡因子 右孩子平衡因子 旋转方式 -2 -1 右旋 -2 1 左旋右旋 2 1 左旋 2 -1 右旋左旋
插入具体方法和步骤讲解完之后,我们给出一个完整的插入算法
int get_depth(Node *node) {
if (!node) return 0;
return node->depth;
}
int get_balance(Node *node) {
if (!node) return 0;
return get_depth(node->right) - get_depth(node->left);
}
void update_depth(Node *node) {
if (node == nullptr) return;
int depth_l_child = get_depth(node->left);
int depth_r_child = get_depth(node->right);
node->depth = std::max(depth_l_child, depth_r_child) + 1;
}
Node *rotate_right(Node *node) {
Node *parent = node->parent;
Node *left_child = node->left;
if (left_child->right) left_child->right->parent = node;
node->left = left_child->right;
update_depth(node);
left_child->right = node;
left_child->parent = parent;
if (parent) {
if (parent->left == node) {
parent->left = left_child;
} else {
parent->right = left_child;
}
}
node->parent = left_child;
update_depth(left_child);
return left_child;
}
Node *rotate_left(Node *node) {
Node *parent = node->parent;
Node *right_child = node->right;
if (right_child->left) right_child->left->parent = node;
node->right = right_child->left;
update_depth(node);
right_child->left = node;
right_child->parent = parent;
if (parent) {
if (parent->left == node) {
parent->left = right_child;
} else {
parent->right = right_child;
}
}
node->parent = right_child;
update_depth(right_child);
return right_child;
}
Node *rotate_left_right(Node *node) {
rotate_left(node->left);
return rotate_right(node);
}
Node *rotate_right_left(Node *node) {
rotate_right(node->right);
return rotate_left(node);
}
Node *insert(Node *root, int val) {
Node *new_node = new Node(val);
if (root == nullptr) return new_node;
Node *parent = nullptr;
Node *cur = root;
while (cur) {
if (cur->val < val) {
parent = cur;
cur = cur->right;
} else if (cur->val > val) {
parent = cur;
cur = cur->left;
} else {
return root;
}
}
if (parent->val > val) {
parent->left = new_node;
} else {
parent->right = new_node;
}
new_node->parent = parent;
while (parent) {
update_depth(parent);
int balance = get_balance(parent);
if (balance == 2) {
int rd_balance = get_balance(parent->right);
if (rd_balance == 1) {
parent = rotate_left(parent);
} else if (rd_balance == -1) {
parent = rotate_right_left(parent);
}
} else if (balance == -2) {
int rd_balance = get_balance(parent->left);
if (rd_balance == -1) {
parent = rotate_right(parent);
} else if (rd_balance == 1) {
parent = rotate_left_right(parent);
}
}
root = parent;
parent = parent->parent;
}
return root;
}
删除
删除操作和插入操作非常类似,这里就不在过多叙述。