图解平衡二叉树
平衡二叉树
平衡二叉树的背景
由于一些众所周知的原因, 我们选择了平衡二叉树, 好吧, 其实就是因为对二叉搜索树的限制太少了, 导致在一些特殊的情况下, 二叉搜索树不太听话, 查找, 插入与删除的时间均变成了 \(O(n)\) 也可以认为, 熵增就会更加有序, 熵减就会更加自由, 这里我们追求的是有序的查找, 但是啊, 年轻人, 为了自由而战吧.
平衡二叉树的定义
- 平衡二叉树首先满足二叉搜索树的特征: 对于任何一颗子树的根结点而言, 它的左子树任何节点的key一定比root小, 而右子树任何节点的key 一定比root大.
- 对于AVL树而言, 其中任何子树仍然是AVL树;
- 每个节点的左右子节点的高度之差的绝对值最多为1(平衡特性);
显然, 由于上述第三点的限制, 很容易计算出它的查找时间为 \(O(log\ n)\). 它的最坏的情况也很容易计算与模拟, 最坏的情况如下:
假设树的高度为 \(h\), 节点的个数为 \(n\), 由于平衡二叉树层数最多只差一层, 那么我们最多再补充 \(n\) 个节点, 将这棵树变成一棵深度为 \(h+1\) 的满二叉树, 那么.
平衡二叉树的自平衡机制
平衡二叉树的自平衡机制其实就是每次对树进行插入或者删除操作之后, 都会判断这颗子树是否满足条件, 是否需要调整, 需要调整, 再进行调整.
实际实现的步骤, 我们依然可以采用递归回溯的方式, 首先递归向下找到需要插入或者删除的节点, 进行插入或者删除操作, 然后回溯判断以该节点为根节点的子树是否需要调整.
平衡二叉树的调整一共存在下列四种情况:
我们将一颗子树的平衡因子定义为: 这颗子树的根节点的左子树的高度与右子树的高度差
LL型失衡(右旋)
这颗子树的平衡因子为2, 并且根节点的左子树的平衡因子为1, 那么以这颗子树的根节点的左孩子节点为轴节点, 进行右旋转, 如下:
RR型失衡(左旋)
这颗子树的平衡因子为-2, 并且根节点的右子树的平衡因子为-1, 那么以这颗子树的根节点的右孩子节点为轴节点, 进行左旋转, 如下:
LR型失衡:(左旋+右旋)
这颗子树的平衡因子为2, 并且根节点的左子树的平衡因子为-1, 那么先以这颗子树的左子树的右孩子节点为轴节点, 进行左旋转, 然后以这颗子树的左孩子节点为轴节点, 进行右旋转, 如下:
RL失衡:(右旋+左旋)
这颗子树的平衡因子为-2, 并且根节点的左子树的平衡因子为1, 那么先以这颗子树的右子树的左孩子节点为轴节点, 进行右旋转, 然后以这颗子树的右孩子节点为轴节点, 进行左旋转, 如下:
当一颗子树的高度发生变化的时候, 就有可能导致子树的失衡, 此时就需要对树的结构进行调整, 具体的操作就是我们上述的各种旋转.
平衡二叉树的插入
插入操作实际上比较简单, 因为插入操作实际上都是从叶子节点插入的, 我们记录从根节点到叶子节点的路径, 插入节点之后, 这个路径上所有节点的高度都会发生变化, 也就有可能导致树的失衡. 实现部分的代码如下:
template<class T>
void AVLTree<T>::insert(T value) {
// 记录修改的子树的路径, 从根节点到叶子节点
std::vector<AVLTreeNode<T>*> insert_path;
AVLTreeNode<T>* direct = root;
// 找到插入的节点
while (direct != nullptr && direct->value != value) {
insert_path.emplace_back(direct);
if (direct->value < value) {
direct = direct->right;
}
else {
direct = direct->left;
}
}
// 如果这个节点已经存在, 直接返回
if (direct != nullptr) {
return;
}
// 新建插入的节点
AVLTreeNode<T>* new_node = new AVLTreeNode<T>(value);
// 如果当前树为空, 新建节点就是根节点
if (root == nullptr) {
root = new_node;
return;
}
// 将新的节点插入到叶子节点的左节点还是右节点
if (insert_path.back()->value < value) {
insert_path.back()->right = new_node;
}
else {
insert_path.back()->left = new_node;
}
// 需要平衡的节点都在该路径上
balance(insert_path);
}
平衡树的调整
在对树进行操作的过程中, 我们记录从根节点到叶子节点的路径. 因为这个路径上的每一个节点的高度与子树实际都是改变的, 最后, 我们调整的时候, 从叶子节点向根节点进行调整.
下图是我们进行一次插入操作之后, 树的结构发生的变化, 我们需要从叶子节点向根节点进行调整这棵树.
代码实现的时候, 我新增了一个虚拟根节点, 用于旋转的时候, 保持根节点不变, 具体实现代码如下:
template <class T>
void AVLTree<T>::balance(std::vector<AVLTreeNode<T>*>& path) {
// 将路径设置为从下到上, 从叶子节点到根节点
std::reverse(path.begin(), path.end());
// 新建一个假的根节点作为当前根节点的根节点
AVLTreeNode<T>* pre_root = new AVLTreeNode<T>(path.back()->value);
pre_root->left = path.back();
path.emplace_back(pre_root);
for (size_t i = 0; i < path.size() - 1; i++) {
AVLTreeNode<T>* avltree_node = path[i];
// 更新从叶子节点到根节点的高度与count, 高度可能会修改, count一定会+1
// 前面的节点可能存在调整, 需要更新节点的信息
avltree_node->updateValues();
// 如果左子树的高度比右子树的高度大2, 那么需要调整
if (avltree_node->balanceFactor() == 2) {
// 如果这棵子树的左子树需要调整, 进行调整
if (avltree_node->left->balanceFactor() == -1) {
avltree_node->left = avltree_node->left->left_rotate();
}
// 这里需要判断返回的子树的根节点是父节点的左子树还是右子树
if (path[i + 1]->left == avltree_node) {
path[i + 1]->left = avltree_node->right_rotate();
}
else {
path[i + 1]->right = avltree_node->right_rotate();
}
}
else if (avltree_node->balanceFactor() == -2) {
if (avltree_node->right->balanceFactor() == 1) {
avltree_node->right = avltree_node->right->right_rotate();
}
if (path[i + 1]->left == avltree_node) {
path[i + 1]->left = avltree_node->left_rotate();
}
else {
path[i + 1]->right = avltree_node->left_rotate();
}
}
}
root = pre_root->left;
pre_root->left = nullptr;
delete pre_root;
return;
}
平衡二叉树的删除
与二叉树的插入不同, 平衡二叉树的插入都是从叶子节点插入的, 所以平衡调整也是从叶子节点到根节点即可, 平衡二叉树的删除则不同, 会涉及到下面几种情况.
- 删除的节点是叶子节点, 将该节点的父节点的原来指向该节点的指针指向
nullptr
, 然后删除该节点. 记录从根节点到该叶子节点的路径, 从叶子节点向根节点开始调整树的结构. - 删除的节点是中间节点:
a) 找到需要被删除的节点 delete, 并且记录从根节点到该节点的路径
b) 找到需要删除的节点的直接后续节点, 我采用的是, 该节点的右子树的最左节点, 记录从被删除节点到这个叶子的路径. 然后将这个直接后续节点与需要被删除的节点替换.
c) 替换之后, 删除需要被删除的节点, 合并从根节点到delete节点, delete节点到替换节点之间的路径信息.
d) 从叶子节点到根节点, 调整树的结构
平衡二叉树的删除步骤我的实现如下, 写的巨丑:
template<class T>
void AVLTree<T>::erase(T value) {
std::vector<AVLTreeNode<T>*> find_path;
std::vector<AVLTreeNode<T>*> change_path;
AVLTreeNode<T>* direct = root; // 遍历访问的节点
AVLTreeNode<T>* temp_node = nullptr; // 临时保存需要删除的节点
AVLTreeNode<T>* pre_direct = nullptr; // 删除节点的直接后续节点(叶子节点)的父节点
AVLTreeNode<T>* pre_delete = nullptr; // 被删除节点的父节点
// 找到插入的节点
while (direct != nullptr && direct->value != value) {
find_path.emplace_back(direct);
pre_delete = direct;
if (direct->value < value) {
direct = direct->right;
}
else {
direct = direct->left;
}
}
// 找不到该节点
if (direct == nullptr) {
std::cout << "The Value would bd deleted not found in the tree" << std::endl;
return;
}
// 临时保存需要删除的节点
temp_node = direct;
// 如果删除的节点左右子树都没有, 也就是删除叶子节点
if (direct->left == nullptr && direct->right == nullptr) {
if(pre_delete->value > value) {
pre_delete->left = nullptr;
}
else {
pre_delete->right = nullptr;
}
delete direct;
balance(find_path);
return;
}
// 设置被保存的节点
pre_direct = pre_delete;
// 如果当前节点存在右子树
if(direct->right != nullptr) {
// 找到该节点的右子树的最左节点, 将该节点替换需要删除的节点
pre_direct = direct;
direct = direct->right;
}
else {
pre_direct = direct;
direct = direct->left;
}
// 找到被删除节点的直接后续节点, 这个节点就是叶子节点
while (direct != nullptr && direct->left != nullptr) {
change_path.emplace_back(direct);
pre_direct = direct;
direct = direct->left;
}
// 找到需要替换的节点, 将这个节点与需要删除的节点替换
pre_direct->left = nullptr; // 将叶子节点隔开
// 如果被删除的节点是根节点
if (pre_delete != nullptr) {
if (pre_delete->value < value) {
pre_delete->right = direct;
}
else {
pre_delete->left = direct;
}
}
// 替换需要删除的节点
if(temp_node->left == direct) {
direct->left = nullptr;
}
else {
direct->left = temp_node->left;
}
if(temp_node->right == direct) {
direct->right = nullptr;
}
else {
direct->right = temp_node->right;
}
// 将替换后的节点删除
temp_node->left = nullptr;
temp_node->right = nullptr;
delete temp_node;
// 将后续节点添加到修改路径中
find_path.emplace_back(direct);
find_path.insert(find_path.end(), change_path.begin(), change_path.end());
balance(find_path);
return;
}
我的具体实现代码请参考我的实现记录, 还增加了树的可视化, 传送门