二叉搜索树基础回顾
二叉搜索树(BST)是树形数据结构的基础形态,满足以下性质:
- 任意节点的左子树所有节点值小于该节点值
- 任意节点的右子树所有节点值大于该节点值
- 左右子树各自也是 BST
普通 BST 在理想情况下具有 O(log n) 的查询效率,但在极端情况下(如顺序插入)会退化为链表,导致时间复杂度恶化至O(n)。这就是需要自平衡树的根本原因。
在计算机科学中,树形数据结构被广泛应用于各种领域,从数据库索引到操作系统调度,再到网络路由算法等。其中,自平衡二叉搜索树(Self-balancing Binary Search Tree)是一类特别重要的树形结构,它们通过自我调整机制保持高度平衡,从而确保操作的高效性。本文将介绍两种最著名的自平衡二叉搜索树:AVL 树和红黑树。
AVL 树
AVL 树是以其发明者 Adelson-Velsky 和 Landis 命名的一种高度平衡的二叉搜索树。它于 1962 年首次提出,是最早的自平衡二叉搜索树之一。AVL 树的基本思想在于通过旋转操作来保持任意节点左右子树的高度差不超过 1,这使得其查找、插入和删除操作的时间复杂度均为 O(log n)。AVL 树的特点在于它的严格平衡性质,使其在频繁进行查找操作的应用场景中表现优异。
插入操作
插入操作是 AVL 树中最复杂的操作之一,因为它不仅需要遵循二叉搜索树的插入规则,还要确保插入后树仍然保持平衡。
通过平衡因子
维护平衡:
平衡因子 = 左子树高度 - 右子树高度
要求每个节点的平衡因子绝对值不超过 1,当超过即表示平衡被破坏了,需要进行重平衡调整,一共有 4 种情况,对应着 4 种旋转策略,分别是 左旋(LL型)、右旋(RR型)、左右旋(LR型)、右左旋(RL型)。
插入流程:
- 标准BST插入:首先按照普通二叉搜索树的方式插入新节点。
- 回溯更新高度:插入完成后,从插入点向上回溯,更新沿途所有节点的高度。
- 检查平衡因子:通过平衡因子检查是否失衡
- 执行必要旋转:根据情况进行旋转调整
删除操作
删除操作同样需要考虑树的平衡性。删除一个节点可能会导致其祖先节点失衡,因此也需要通过旋转操作来恢复平衡。删除操作的主要步骤如下:
- 标准 BST 删除:找到要删除的节点并执行删除操作,遵循二叉搜索树的删除规则。
- 更新高度:从删除点向上回溯,更新沿途所有节点的高度。
- 检查平衡因子:计算每个节点的平衡因子,若发现失衡则进行旋转调整。
在执行删除时:
- 若删除节点有存在 null 的子树,就直接把不为 null 的子树替换到当前删除节点位置即可,也有可能左右子树均为空,这时直接替换为 null 即可;
- 若删除节点的左右子树均不为空,这时候可以找到右子树中的最小节点,把该节点替换到当前删除节点位置,并且递归执行在右子树中删除最小节点。
查找操作
查找操作在 AVL 树中非常高效,因为其高度接近 log(n),所以查找操作的时间复杂度为 O(log n)。查找过程类似于普通二叉搜索树,从根节点开始逐层比较目标键值与当前节点的键值,直到找到匹配的节点或遍历完整棵树。
代码实现
package AVL;
public class AVLTree {
private Node root;
private static class Node {
int value;
int height;
Node left;
Node right;
Node(int value) {
this.value = value;
this.height = 1;
}
}
private void insert(int value) {
root = insert(root, value);
}
private Node insert(Node node, int value) {
if (node == null) {
return new Node(value);
}
if (value < node.value) {
node.left = insert(node.left, value);
} else if (value > node.value) {
node.right = insert(node.right, value);
} else {
// 不允许插入相同的值
return node;
}
return rebalance(node);
}
private Node rebalance(Node node) {
// 更新高度
updateHeight(node);
// 获取平衡因子
int balanceFactor = getBalanceFactor(node);
// 左-左
if (balanceFactor > 1 && getBalanceFactor(node.left) >= 0) {
return rotateRight(node);
}
// 右-右
if (balanceFactor < -1 && getBalanceFactor(node.right) <= 0) {
return rotateLeft(node);
}
// 左-右
if (balanceFactor > 1 && getBalanceFactor(node.left) < 0) {
node.left = rotateLeft(node.left);
return rotateRight(node);
}
// 右-左
if (balanceFactor < -1 && getBalanceFactor(node.right) > 0) {
node.right = rotateRight(node.right);
return rotateLeft(node);
}
return node;
}
// 删除节点
public void delete(int value) {
root = delete(root, value);
}
private Node delete(Node node, int value) {
if (node == null) {
return null;
}
if (value < node.value) {
node.left = delete(node.left, value);
} else if (value > node.value) {
node.right = delete(node.right, value);
} else {
if (node.left == null || node.right == null) {
// 只要有一个节点为空,直接替换掉当前节点即可
node = (node.left != null) ? node.left : node.right;
} else {
// 两个子节点都不为空, 就找到右子树的最小节点,替换掉当前节点,再删除右子树的最小节点
Node minNode = findMin(node.right);
node.value = minNode.value;
node.right = delete(node.right, minNode.value);
}
}
if (node == null) {
return null;
}
return rebalance(node);
}
private Node findMin(Node node) {
while (node.left != null) {
node = node.left;
}
return node;
}
// 右旋转
private Node rotateRight(Node node) {
Node x = node.left;
Node y = x.right;
x.right = node;
node.left = y;
updateHeight(node);
updateHeight(x);
return x;
}
// 左旋转
private Node rotateLeft(Node node) {
Node x = node.right;
Node y = x.left;
x.left = node;
node.right = y;
updateHeight(node);
updateHeight(x);
return x;
}
private int getBalanceFactor(Node node) {
return node == null ? 0 : height(node.left) - height(node.right);
}
// 更新高度
private void updateHeight(Node node) {
node.height = 1 + Math.max(height(node.left), height(node.right));
}
// 获取指定节点的高度
private int height(Node node) {
if (node == null) {
return 0;
}
return node.height;
}
// 打印树结构
public void printTree() {
printTree(root, "", true);
}
private void printTree(Node node, String prefix, boolean isLeft) {
if (node != null) {
System.out.println(prefix + (isLeft ? "├── " : "└── ") + node.value);
printTree(node.left, prefix + (isLeft ? "│ " : " "), true);
printTree(node.right, prefix + (isLeft ? "│ " : " "), false);
}
}
// 中序遍历打印
public void inorderPrint() {
inorder(root);
}
private void inorder(Node node) {
if (node == null) {
return;
}
inorder(node.left);
System.out.print(node.value + " ");
inorder(node.right);
}
public static void main(String[] args) {
AVLTree tree = new AVLTree();
int[] values = {10, 20, 30, 40, 50, 25};
for (int value : values) {
tree.insert(value);
}
System.out.println("完成节点插入");
// 打印树结构
System.out.println("树的结构:");
tree.printTree();
System.out.println();
// 中序遍历打印
System.out.println("中序遍历:");
tree.inorderPrint();
System.out.println();
// 删除节点
System.out.println("删除节点 40");
tree.delete(40);
// 打印树结构
System.out.println("树的结构:");
tree.printTree();
System.out.println();
// 中序遍历打印
System.out.println("中序遍历:");
tree.inorderPrint();
System.out.println();
}
}
时间复杂度分析
操作类型 | 平均复杂度 | 最坏复杂度 |
---|---|---|
查询 | O(log n) | O(log n) |
插入 | O(log n) | O(log n) |
删除 | O(log n) | O(log n) |
优点与缺点
AVL 树的优点在于其严格的平衡特性,这使得查找操作极其高效,尤其适用于那些查找操作远多于插入和删除操作的应用场景。然而,AVL 树的插入和删除操作相对较复杂,每次操作可能需要多次旋转来恢复平衡,导致这些操作的开销较大。此外,频繁的旋转操作也可能增加内存消耗。
综上所述,AVL 树以其高效的查找性能和严格的平衡要求,在某些特定应用场景中表现出色。然而,对于需要频繁进行插入和删除操作的情况,红黑树可能是更合适的选择。接下来,我们将介绍红黑树。
红黑树
红黑树则由 Rudolf Bayer 和 Ed McCreight 在 1972 年提出,是一种较为宽松的自平衡二叉搜索树。红黑树通过引入颜色属性(红色或黑色)以及一系列规则来约束树的高度,从而保证了操作时间复杂度为 O(log n)。
尽管 AVL 树和红黑树都属于自平衡二叉搜索树,但它们在实际应用中的表现各有千秋。AVL 树由于其严格的平衡特性,在查找密集型的应用中表现尤为突出;而红黑树则因为其灵活性和较低的操作开销,在动态数据集的管理中更具优势。
五大约束规则
红黑树通过五个约束保持近似平衡:
- 节点非红即黑
- 根节点为黑
- 子节点(NIL)为黑
- 节点的子节点必须为黑
- 路径的黑节点数相同
插入操作
红黑树的插入操作需要遵循二叉搜索树的插入规则,同时还要通过一系列调整操作来恢复树的平衡。插入过程可以分为以下步骤:
- 标准插入:首先按照普通二叉搜索树的方式插入新节点,并将其初始颜色设为红色。
- 修复红黑性质:插入后,检查是否违反了红黑树的性质,并通过重新着色或旋转来修复。
具体的修复过程主要包括三种情况:
- Case 1: 新节点的父节点是黑色,无需任何调整。
- Case 2: 新节点的父节点是红色,且叔节点(uncle)也是红色。此时需要重新着色父节点、叔节点和祖父节点,然后继续向上检查。
- Case 3: 新节点的父节点是红色,且叔节点是黑色。此时需要通过旋转操作来修复树的平衡。
删除操作
红黑树的删除操作同样需要考虑树的平衡性。删除一个节点可能导致树不再满足红黑树的性质,因此需要通过重新着色和旋转操作来恢复平衡。删除操作的主要步骤如下:
- 标准删除:找到要删除的节点并执行删除操作,遵循二叉搜索树的删除规则。
- 修复红黑性质:删除后,检查是否违反了红黑树的性质,并通过重新着色或旋转来修复。
具体的修复过程包括多种情况,例如删除红色节点、删除黑色节点等情况,每种情况都需要单独处理。
红黑树的插入和删除流程相对复杂,这里篇幅原因介绍不到位,有兴趣深入了解可以前往 红黑树 - 维基百科,自由的百科全书
代码实现
public class RedBlackTree<K extends Comparable<K>, V> {
private static final boolean RED = true;
private static final boolean BLACK = false;
private class Node {
K key;
V value;
Node left, right;
boolean color;
int size; // 子树节点总数
Node(K key, V value, boolean color, int size) {
this.key = key;
this.value = value;
this.color = color;
this.size = size;
}
}
private Node root;
// 基本查询操作
public boolean contains(K key) {
return get(key) != null;
}
public V get(K key) {
return get(root, key);
}
private V get(Node x, K key) {
while (x != null) {
int cmp = key.compareTo(x.key);
if (cmp < 0) x = x.left;
else if (cmp > 0) x = x.right;
else return x.value;
}
return null;
}
// 插入操作
public void put(K key, V value) {
root = put(root, key, value);
root.color = BLACK; // 确保根节点始终为黑
}
private Node put(Node h, K key, V value) {
if (h == null) return new Node(key, value, RED, 1);
int cmp = key.compareTo(h.key);
if (cmp < 0) h.left = put(h.left, key, value);
else if (cmp > 0) h.right = put(h.right, key, value);
else h.value = value;
// 修复红黑树性质
if (isRed(h.right) && !isRed(h.left)) h = rotateLeft(h);
if (isRed(h.left) && isRed(h.left.left)) h = rotateRight(h);
if (isRed(h.left) && isRed(h.right)) flipColors(h);
h.size = size(h.left) + size(h.right) + 1;
return h;
}
// 删除操作
public void delete(K key) {
if (!contains(key)) return;
if (!isRed(root.left) && !isRed(root.right))
root.color = RED;
root = delete(root, key);
if (root != null) root.color = BLACK;
}
private Node delete(Node h, K key) {
if (key.compareTo(h.key) < 0) {
if (!isRed(h.left) && !isRed(h.left.left))
h = moveRedLeft(h);
h.left = delete(h.left, key);
}
else {
if (isRed(h.left))
h = rotateRight(h);
if (key.compareTo(h.key) == 0 && (h.right == null))
return null;
if (!isRed(h.right) && !isRed(h.right.left))
h = moveRedRight(h);
if (key.compareTo(h.key) == 0) {
Node x = min(h.right);
h.key = x.key;
h.value = x.value;
h.right = deleteMin(h.right);
}
else h.right = delete(h.right, key);
}
return balance(h);
}
// 核心平衡操作
private Node rotateLeft(Node h) {
Node x = h.right;
h.right = x.left;
x.left = h;
x.color = h.color;
h.color = RED;
x.size = h.size;
h.size = size(h.left) + size(h.right) + 1;
return x;
}
private Node rotateRight(Node h) {
Node x = h.left;
h.left = x.right;
x.right = h;
x.color = h.color;
h.color = RED;
x.size = h.size;
h.size = size(h.left) + size(h.right) + 1;
return x;
}
private void flipColors(Node h) {
h.color = !h.color;
h.left.color = !h.left.color;
h.right.color = !h.right.color;
}
// 辅助方法
private boolean isRed(Node x) {
return x != null && x.color == RED;
}
private int size(Node x) {
return x == null ? 0 : x.size;
}
// 删除辅助操作
private Node moveRedLeft(Node h) {
flipColors(h);
if (isRed(h.right.left)) {
h.right = rotateRight(h.right);
h = rotateLeft(h);
flipColors(h);
}
return h;
}
private Node moveRedRight(Node h) {
flipColors(h);
if (isRed(h.left.left)) {
h = rotateRight(h);
flipColors(h);
}
return h;
}
private Node balance(Node h) {
if (isRed(h.right)) h = rotateLeft(h);
if (isRed(h.left) && isRed(h.left.left)) h = rotateRight(h);
if (isRed(h.left) && isRed(h.right)) flipColors(h);
h.size = size(h.left) + size(h.right) + 1;
return h;
}
// 测试用例
public static void main(String[] args) {
RedBlackTree<Integer, String> rbt = new RedBlackTree<>();
// 插入测试
int[] keys = {10, 20, 5, 30, 15};
for (int key : keys) {
rbt.put(key, "Value_" + key);
}
// 删除测试
rbt.delete(20);
rbt.delete(5);
// 查询测试
System.out.println("Key 15 exists: " + rbt.contains(15));
System.out.println("Value for 30: " + rbt.get(30));
}
}
优点与缺点
红黑树的优点在于其实现相对简单,插入和删除操作的开销较小,尤其适用于动态数据集的管理。由于其宽松的平衡条件,红黑树在插入和删除操作时通常不需要像 AVL 树那样频繁地进行旋转调整,因此整体性能更加稳定。此外,红黑树的内存占用也相对较低。
然而,红黑树的查找性能略逊于 AVL 树,尤其是在高度平衡的极端情况下。这是因为红黑树允许一定的不平衡,导致其高度可能略高于 AVL 树。因此,在查找操作远多于插入和删除操作的应用场景中,AVL 树通常是更好的选择。
总结来说,红黑树以其灵活的平衡策略和较低的操作开销,在需要频繁进行插入和删除操作的应用场景中表现出色。而对于查找密集型的应用,AVL 树则更为合适。了解这两种树各自的优缺点,可以根据具体需求选择最适合的数据结构。
AVL 树与红黑树的比较
在深入理解了 AVL 树和红黑树的设计原理和操作细节之后,我们可以进一步比较它们之间的异同点,特别是在性能、实现复杂度和适用场景方面。
平衡策略差异
特征 | AVL树 | 红黑树 |
---|---|---|
平衡标准 | 严格高度平衡 | 颜色路径平衡 |
旋转次数 | 可能频繁旋转 | 最多三次旋转 |
树高度 | 1.44log n | ≤2log n |
更新代价 | 较高 | 较低 |
性能对比测试
在100万次操作测试中:
- 查询密集型:AVL快15-20%
- 更新密集型:红黑树快25-30%
- 内存占用:AVL多存储高度值(通常4字节),红黑树存储颜色位(1位)
典型应用场景及选择考量
AVL适用场景:
- 数据库索引
- 需要快速查询的字典
- 实时系统
红黑树适用场景:
- 语言标准库(如C++ STL map)
- 文件系统索引
- 内存分配器
选择考量:
- 读多写少:优先AVL
- 写操作频繁:选择红黑树
- 内存敏感:考虑红黑树
经典实现案例
Linux内核红黑树
- 实现文件:lib/rbtree.c
- 特性:内存高效,支持嵌入结构
- 应用场景:虚拟内存管理,调度器
Java TreeMap
- 基于红黑树实现
- 保证log(n)时间性能
- 支持范围查询
总结
在实际应用中,选择合适的自平衡二叉搜索树至关重要。例如,在数据库索引优化、文件系统目录结构管理、实时交易系统订单管理以及缓存系统数据管理等场景中,合理选用 AVL 树或红黑树可以显著提升系统的性能和用户体验。开发者应根据具体的应用需求,权衡查找、插入和删除操作的频率,以及内存使用等因素,做出明智的选择。
优质项目推荐
推荐一个可用于练手、毕业设计参考、增加简历亮点的项目。
lemon-puls/txing-oj-backend: Txing 在线编程学习平台,集在线做题、编程竞赛、即时通讯、文章创作、视频教程、技术论坛为一体