3.删除(Delete)
删除是AVL树最复杂的操作,因为它可能导致多个祖先节点连续失衡,修复可能一直传播到根节点。
第一步:标准BST删除。分三种情况处理待删节点:
1.无子节点:直接删除。
2.只有一个子节点:用子节点顶替自己。
3.有两个子节点:找到中序后继(右子树的最左节点)或中序前驱(左子树的最右节点)来替换值,然后递归删除那个后继/前驱节点(该节点最多只有一个子节点)。
源码见二叉搜索树(BST Tree)讲解
第二步:计算平衡因子 BF = height(left) - height(right)。从实际被删除节点的父节点开始,自底向上回溯到根节点,更新所有经过节点的平衡因子。
如果 BF 为 -1、0、1:说明平衡,但先别急着停,更新完高度后继续向上回溯(因为底层高度变了,可能影响祖先)。
如果 BF 为 +2 或 -2:失衡了! 必须旋转修复。
第三步:检查并修复失衡。在回溯过程中,每遇到一个失衡节点,就执行旋转修复。与插入不同,删除后的旋转会降低子树的高度,这可能导致其祖先节点再次失衡。因此,删除操作在修复一个节点后,必须继续向上回溯检查,直到根节点。
假设失衡节点为 X(BF = ±2),看 X 的子节点(Y)的平衡因子来决定用哪种旋转。
情况 A:BF(X) = +2(左子树比右子树高)
说明问题出在左子树上,此时看左子节点 Y = X.left 的平衡因子:
Y 的 BF 失衡类型 执行操作 口诀
≥ 0(即 +1 或 0) LL 对 X 执行右旋 左左向右转
< 0(即 -1) LR 先对 Y 执行左旋,再对 X 执行右旋 左右先左后右
情况 B:BF(X) = -2(右子树比左子树高)
说明问题出在右子树上,此时看右子节点 Y = X.right 的平衡因子:
Y 的 BF 失衡类型 执行操作 口诀
≤ 0(即 -1 或 0) RR 对 X 执行左旋 右右向左转
0(即 +1) RL 先对 Y 执行右旋,再对 X 执行左旋 右左先右后左
第四步:决定是否继续向上回溯(删除最关键的区别)
执行完旋转修复后,你需要检查以 X 为根的子树的新高度,和删除前(或旋转前)的高度是否发生了变化:
如果高度降低了:这说明变矮了,必须继续向上回溯,检查 X 的父节点是否因此失衡(因为子树变矮会导致父节点的平衡因子改变)。
如果高度没变:说明这棵子树的高度和删除前一样,可以立即停止回溯,修复结束。
通俗理解:删除就像一个"抽砖块"。如果抽掉砖块导致这一层变矮了,那上面的楼层受力会变,必须一层层查上去;如果抽掉后找了块别的砖垫上,总高度没变,那楼上的结构就安全了。
关键特性:删除操作可能触发 O(log n) 次旋转,因为修复了一个失衡点,高度降低后,上面的节点可能又失衡了。
cpp
//获取当前节点的高度
int BinarySearchTree::treeHeight(const Node * node)
{
if (node == nullptr)
{
return 0;
}
int l = treeHeight(node->left);
int r = treeHeight(node->right);
return 1 + (l > r ? l : r);
}
//平衡修复
bool AVLTree::reBalance(Node * node)
{
if (node == nullptr)
{
return true;
}
Node* right = node->right;
Node* left = node->left;
int r = treeHeight(right);
int l = treeHeight(left);
if (r - l > 1)
{
//右高左低
if (right->right)
{
//右右型
levorotation(node, right);
return reBalance(right);
}
else
{
//右左型 先右旋后左旋
Node* s = right->left;
rightHandedRotation(right, s);
levorotation(node, s);
return reBalance(s);
}
}
else if (l - r > 1)
{
//左高右低
if (left->left)
{
//右旋
rightHandedRotation(node, left);
return reBalance(left);
}
else
{
//只有右子树 为左右型 先左旋后右旋
Node* s = left->right;
levorotation(left, s);
rightHandedRotation(node, s);
return reBalance(s);
}
}
else
{
return reBalance(node->parent);
}
return false;
}
Node * AVLTree::remove(int val)
{
//二叉查找树的删除
Node* node = BinarySearchTree::remove(val);
//修复平衡
reBalance(node);
return node;
}
按照树的格式输出
cpp
void AVLTree::printTree()
{
if (rootNode == nullptr)
{
return;
}
std::vector<Node*> vec;
vec.push_back(rootNode);
int count = 0;
int index = 0;
int h = 5 * std::pow(2, treeHeight(rootNode));
int s = 1;
while (true)
{
count = vec.size();
if (index == count)
{
break;
}
std::string str = "";
char spli = '_';
bool isFlag = false;
for (int i = index; i < count; i++)
{
Node* node = vec[i];
str.append(h / s, spli);
if (node)
{
str += std::to_string(node->val);
isFlag = true;
}
else
{
str += "*";
}
str.append(h / s, spli);
if (node)
{
vec.push_back(node->left);
vec.push_back(node->right);
}
}
if (isFlag)
{
std::cout << str << std::endl;
std::cout << std::endl;
}
index = count;
s *= 2;
}
}
4.查找后继(中序后继)
要找节点 x 的后继,分两种情况:
情况一:节点 x 有右子树
策略:往右走一步,然后一路向左到底。
因为右子树里的所有节点都比 x 大,其中最小的那个肯定在右子树的最左下角。
步骤:x.right -> 然后不断往左走(node.left),直到没有左孩子为止。
情况二:节点 x 没有右子树
策略:向上找祖先,直到"拐弯"为止。
因为没有右子树,说明比 x 大的节点只能在它的祖先里。我们要找到第一个向左拐的祖先(即,该祖先的左子树包含了节点 x)。
步骤:让指针指向 x,不断向上找父节点,当发现当前节点是其父节点的左孩子时,那个父节点就是后继。
源码实现
cpp
bool AVLTree::successor(int val, int & res)
{
//当前AVL树为空
if (rootNode == nullptr)
{
return false;
}
Node* node;
//未找到当前结点
if (!find(val, &node))
{
return false;
}
if (node->right)
{
//在右子树中找最小的
if (node->right->left)
{
node = node->right->left;
}
else
{
node = node->right;
}
while (node->left)
{
node = node->left;
}
res = node->val;
return true;
}
//当前结点没有右孩子
if (node == rootNode) //当前是根结点
{
return false;
}
//当前结点是左孩子,父节点就是它的后继
//当前结点是右孩子,找第一个是左孩子的结点的父节点
while (node && node->parent && !node->isLeftTree)//当前结点不为空,结点的父节点不为空,当前结点是右节点,继续查找第一个左节点
{
node = node->parent;
}
if (node->isLeftTree)
{
res = node->parent->val;
return true;
}
return false;
}
查找前驱(中序前驱)
逻辑和后继完全对称,把"左右"反过来即可。
情况一:节点 x 有左子树
策略:往左走一步,然后一路向右到底。
左子树都比 x 小,其中最大的那个肯定在左子树的最右下角。
步骤:x.left -> 然后不断往右走(node.right),直到没有右孩子为止。
情况二:节点 x 没有左子树
策略:向上找祖先,直到"拐弯"为止。
向上找第一个向右拐的祖先(即,该祖先的右子树包含了节点 x)。
步骤:让指针指向 x,不断向上找父节点,当发现当前节点是其父节点的右孩子时,那个父节点就是前驱。
源码实现
cpp
bool AVLTree::predecessor(int val, int & res)
{
Node* node = nullptr;
if (!find(val, &node))
{
return false;
}
//当前结点有左结点
if (node->left)
{
//当前结点的左孩子没有右孩子,左孩子最大值就是左子树的根节点
if (node->left->right == nullptr)
{
res = node->left->val;
return true;
}
//当前结点的左孩子有右孩子,在右孩子中找最大值
node = node->left->right;
while (node->right)
{
node = node->right;
}
res = node->val;
return true;
}
//当前结点没有左孩子
if (node == rootNode)//当前结点是根结点
{
return false;
}
//如果当前结点是右孩子,则父结点为前驱
//如果当前结点是左孩子,则找到第一个为右孩子的父节点,其祖父结点为前驱
while (node && node->parent && node->isLeftTree)//当前结点不为空,结点的父节点不为空,当前结点是左节点,继续查找第一个右节点
{
node = node->parent;
}
if (!node->isLeftTree)
{
res = node->parent->val;
return true;
}
return false;
}
平衡二叉树的测试程序
cpp
//AVL树测试
void BSTTest()
{
AVLTree bst;
//创建树
std::cout << "插入节点:7" << std::endl;
bst.create(7);
bst.printTree();
std::cout << std::endl;
std::vector<int> vec{ 5,8,3,4,10 };
for (size_t i = 0; i < vec.size(); i++)
{
//向树中插入结点
bst.insert(vec[i]);
std::cout << "插入节点:" << vec[i] << std::endl;
bst.printTree();
std::cout << std::endl;
}
bst.remove(8);
std::cout << "删除节点:8" << std::endl;
bst.printTree();
std::cout << std::endl;
bst.remove(3);
std::cout << "删除节点:3" << std::endl;
bst.printTree();
std::cout << std::endl;
bst.remove(10);
std::cout << "删除节点:10" << std::endl;
bst.printTree();
std::cout << std::endl;
bst.remove(4);
std::cout << "删除节点:4" << std::endl;
bst.printTree();
std::cout << std::endl;
bst.remove(7);
std::cout << "删除节点:7" << std::endl;
bst.printTree();
std::cout << std::endl;
}
结果截图



复杂度总结对比
操作 时间复杂度 旋转次数
查找 O(log n) 0
插入 O(log n) 最多 2 次(常数级)
删除 O(log n) 最坏情况 O(log n) 次