平衡二叉树是一种特殊的二叉搜索树(BST),通过保持树的高度平衡,确保在最坏情况下操作的时间复杂度维持在 (O(log n))。本文将深入探讨平衡二叉树的基本概念、节点添加和查找、旋转机制及其触发时机,并详细解释四种基本旋转操作:左左(LL)、左右(LR)、右右(RR)和右左(RL)。
前言
平衡二叉树(Balanced Binary Tree)是一种二叉树,其特点是在插入或删除节点时,通过一定的机制保持树的平衡。常见的平衡二叉树包括 AVL 树和红黑树。本文主要以 AVL 树为例,讲解其工作原理和实现细节。
添加节点
在 AVL 树中,添加节点的过程与普通的二叉搜索树类似,但在每次插入后,需要检查树的平衡因子并进行必要的旋转操作以恢复平衡。以下是添加节点的具体步骤:
1. 普通的 BST 插入:
- 比较要插入的值与当前节点的值。
- 如果小于当前节点的值,递归地插入到左子树。
- 如果大于当前节点的值,递归地插入到右子树。
- 如果等于当前节点的值,则忽略(不允许重复)。2. 更新高度:
- 插入节点后,更新沿路径的每个节点的高度。3. 检查平衡因子:
- 计算每个节点的平衡因子(左子树高度减去右子树高度)。4. 执行旋转操作:
- 根据平衡因子的值,确定是否需要旋转及旋转类型。
插入节点示例代码:
public class AVLTree {
private class TreeNode {
int key, height;
TreeNode left, right;
TreeNode(int d) {
key = d;
height = 1;
}
}
private TreeNode root;
public void insert(int key) {
root = insertRec(root, key);
}
private TreeNode insertRec(TreeNode node, int key) {
// 如果当前节点为空,则插入新节点
if (node == null) {
return new TreeNode(key);
}
// 根据键值大小选择插入到左子树或右子树
if (key < node.key) {
node.left = insertRec(node.left, key);
} else if (key > node.key) {
node.right = insertRec(node.right, key);
} else {
return node; // 不允许插入重复键值
}
// 更新节点的高度
node.height = 1 + Math.max(height(node.left), height(node.right));
// 计算平衡因子
int balance = getBalance(node);
// 根据平衡因子进行相应的旋转操作
// 左左情况
if (balance > 1 && key < node.left.key) {
return rightRotate(node);
}
// 右右情况
if (balance < -1 && key > node.right.key) {
return leftRotate(node);
}
// 左右情况
if (balance > 1 && key > node.left.key) {
node.left = leftRotate(node.left);
return rightRotate(node);
}
// 右左情况
if (balance < -1 && key < node.right.key) {
node.right = rightRotate(node.right);
return leftRotate(node);
}
return node;
}
// 获取节点的高度
private int height(TreeNode node) {
return node == null ? 0 : node.height;
}
// 计算节点的平衡因子
private int getBalance(TreeNode node) {
return node == null ? 0 : height(node.left) - height(node.right);
}
// 右旋操作
private TreeNode rightRotate(TreeNode y) {
TreeNode x = y.left;
TreeNode T2 = x.right;
// 执行旋转操作
x.right = y;
y.left = T2;
// 更新旋转节点的高度
y.height = Math.max(height(y.left), height(y.right)) + 1;
x.height = Math.max(height(x.left), height(x.right)) + 1;
// 返回新的根节点
return x;
}
// 左旋操作
private TreeNode leftRotate(TreeNode x) {
TreeNode y = x.right;
TreeNode T2 = y.left;
// 执行旋转操作
y.left = x;
x.right = T2;
// 更新旋转节点的高度
x.height = Math.max(height(x.left), height(x.right)) + 1;
y.height = Math.max(height(y.left), height(y.right)) + 1;
// 返回新的根节点
return y;
}
}
在平衡二叉树中,如何查找单个节点?
查找节点的过程与普通的二叉搜索树相同。通过比较要查找的值与当前节点的值,递归地在左子树或右子树中查找。
public boolean search(int key) {
return searchRec(root, key);
}
private boolean searchRec(TreeNode node, int key) {
if (node == null) {
return false;
}
if (key < node.key) {
return searchRec(node.left, key);
} else if (key > node.key) {
return searchRec(node.right, key);
} else {
return true;
}
}
为什么要旋转?
在 AVL 树中,每个节点的平衡因子是其左子树高度减去右子树高度的值。AVL 树通过旋转操作保持平衡因子在 -1、0 和 1 之间。当插入或删除节点后,某个节点的平衡因子的绝对值超过 1 时,树就需要进行旋转操作来恢复平衡。
旋转的触发时机
旋转操作在以下情况下触发:
1. 插入节点后:如果插入节点导致某个节点的平衡因子变为 2 或 -2。
2. 删除节点后:如果删除节点导致某个节点的平衡因子变为 2 或 -2。
左左(LL)旋转
左左情况发生在一个节点的左子树过高(平衡因子为 2),且左子树的左子树导致不平衡。这种情况需要进行一次右旋操作。
z
/ \
y T4
/ \
x T3
/ \
T1 T2
右旋后:
y
/ \
x z
/ \ / \
T1 T2 T3 T4
右旋操作步骤
1. 识别旋转节点:找到需要进行右旋的节点 z。
2. 左子树提取:提取 z 的左子树 y,并将 y 的右子树 T2 暂存。
3. 旋转操作:
- 将 y 的右子树指向 z。
- 将 z 的左子树指向 T2。
4. 更新高度:更新 z 和 y 的高度。
5. 返回新的根节点:返回旋转后的新根节点 y。
右旋代码实现:
private TreeNode rightRotate(TreeNode y) {
TreeNode x = y.left;
TreeNode T2 = x.right;
// 执行旋转操作
x.right = y;
y.left = T2;
// 更新旋转节点的高度
y.height = Math.max(height(y.left), height(y.right)) + 1;
x.height = Math.max(height(x.left), height(x.right)) + 1;
// 返回新的根节点
return x;
}
左右(LR)旋转
左右情况发生在一个节点的左子树过高(平衡因子为 2),且左子树的右子树导致不平衡。这种情况需要先对左子树进行左旋,然后对根节点进行右旋。
z
/ \
y T4
/ \
T1 x
/ \
T2 T3
左旋后:
z
/ \
x T4
/ \
y T3
/ \
T1 T2
右旋后:
x
/ \
y z
/ \ / \
T1 T2 T3 T4
左右旋操作步骤
1. 先对左子树进行左旋:
- 提取 y 的右子树 x,并将 x 的左子树 T2 暂存。
- 将 y 的右子树指向 T2,将 x 的左子树指向 y。
- 更新 y 和 x 的高度。
2. 再对根节点进行右旋:
- 提取 z 的左子树 x,并将 x 的右子树 T3 暂存。
- 将 z 的左子树指向 T3,将 x 的右子树指向 z。
- 更新 z 和 x 的高度。
3. 返回新的根节点:返回旋转后的新根节点 x。
左右旋代码实现:
private TreeNode leftRightRotate(TreeNode z) {
// 先对左子树进行左旋
z.left = leftRotate(z.left);
// 然后对根节点进行右旋
return rightRotate(z);
}
右右(RR)旋转
右右情况发生在一个节点的右子树过高(平衡因子为 -2),且右子树的右子树导致不平衡。这种情况需要进行一次左旋操作。
z
/ \
T1 y
/ \
T2 x
/ \
T3 T4
左旋后:
y
/ \
z x
/ \ / \
T1 T2 T3 T4
左旋操作步骤
1. 识别旋转节点:找到需要进行左旋的节点 z。
2. 右子树提取:提取 z 的右子树 y,并将 y 的左子树 T2 暂存。
3. 旋转操作:
- 将 y 的左子树指向 z。
- 将 z 的右子树指向 T2。
4. 更新高度:更新 z 和 y 的高度。
5. 返回新的根节点:返回旋转后的新根节点 y。
左旋代码实现:
private TreeNode leftRotate(TreeNode x) {
TreeNode y = x.right;
TreeNode T2 = y.left;
// 执行旋转操作
y.left = x;
x.right = T2;
// 更新旋转节点的高度
x.height = Math.max(height(x.left), height(x.right)) + 1;
y.height = Math.max(height(y.left), height(y.right)) + 1;
// 返回新的根节点
return y;
}
右左(RL)旋转
右左情况发生在一个节点的右子树过高(平衡因子为 -2),且右子树的左子树导致不平衡。这种情况需要先对右子树进行右旋,然后对根节点进行左旋。
z
/ \
T1 y
/ \
x T4
/ \
T2 T3
右旋后:
z
/ \
T1 x
/ \
T2 y
/ \
T3 T4
左旋后:
x
/ \
z y
/ \ / \
T1 T2 T3 T4
右左旋操作步骤
1. 先对右子树进行右旋:
- 提取 y 的左子树 x,并将 x 的右子树 T3 暂存。
- 将 y 的左子树指向 T3,将 x 的右子树指向 y。
- 更新 y 和 x 的高度。
2. 再对根节点进行左旋:
- 提取 z 的右子树 x,并将 x 的左子树 T2 暂存。
- 将 z 的右子树指向 T2,将 x 的左子树指向 z。
- 更新 z 和 x 的高度。
3. 返回新的根节点:返回旋转后的新根节点 x。
右左旋代码实现:
private TreeNode rightLeftRotate(TreeNode z) {
// 先对右子树进行右旋
z.right = rightRotate(z.right);
// 然后对根节点进行左旋
return leftRotate(z);
}
总结
平衡二叉树通过旋转操作保持树的平衡,从而确保操作的时间复杂度维持在 \(O(\log n)\)。四种基本的旋转操作(左左、左右、右右和右左)是维护树平衡的核心手段。理解这些旋转操作及其应用,可以帮助我们设计和实现高效的数据结构,确保在各种应用场景中提供稳定的性能。