1. 从静态到动态:为什么需要树表查找
1.1 前面算法的局限性
| 算法 | 时间复杂度 | 核心局限 |
|---|---|---|
| 顺序查找 | O(n) | 太慢 |
| 二分查找 | O(log n) | 静态数据,插入删除O(n) |
| 插值/斐波那契 | O(log n) | 静态数据,插入删除O(n) |
| 分块查找 | O(√n) | 块内仍需顺序查找 |
| 哈希查找 | O(1) | 无序,不支持范围查询 |
关键问题 :当数据频繁插入、删除、修改时,如何保持高效的查找性能?
1.2 树表查找的核心思想
解决方案:用树形结构维护有序数据
二叉搜索树
│
┌──────┴──────┐
▼ ▼
严格平衡 近似平衡
(AVL树) (红黑树)
│ │
▼ ▼
高度平衡 颜色标记
旋转调整 旋转+变色
多路平衡树
(B树/B+树)
│
┌────┴────┐
▼ ▼
B树 B+树
数据分散 数据集中在叶子
所有节点 叶子节点链表连接
存数据 支持高效范围查询
1.3 树表查找的优势
| 特性 | 说明 |
|---|---|
| 动态维护 | 插入删除后自动调整,保持平衡 |
| 有序性 | 中序遍历即有序序列 |
| 范围查询 | 支持 find_range(min, max) |
| 前驱后继 | 快速查找相邻元素 |
| 可扩展性 | B+树是数据库索引的标准实现 |
2. 二叉搜索树:动态查找的基础
2.1 定义与性质
二叉搜索树(Binary Search Tree, BST):一棵二叉树,其中每个节点的值大于其左子树所有节点的值,小于其右子树所有节点的值。
BST示例:
50
/ \
30 70
/ \ / \
20 40 60 80
BST性质验证:
- 节点30:左子树[20]都<30,右子树[40]都>30 ✓
- 节点70:左子树[60]都<70,右子树[80]都>70 ✓
- 中序遍历:20, 30, 40, 50, 60, 70, 80(升序!)
2.2 完整BST实现
python
class TreeNode:
"""BST节点"""
def __init__(self, key, value=None):
self.key = key # 键(用于比较)
self.value = value # 值(存储数据)
self.left = None
self.right = None
self.size = 1 # 以该节点为根的子树节点数
def __repr__(self):
return f"Node({self.key})"
class BST:
"""
二叉搜索树实现
核心操作:
- 查找:O(h),h为树高
- 插入:O(h)
- 删除:O(h)
平衡时 h = log(n),退化时 h = n
"""
def __init__(self):
self.root = None
# ─── 辅助方法 ───
def _size(self, node):
return node.size if node else 0
def _update_size(self, node):
if node:
node.size = 1 + self._size(node.left) + self._size(node.right)
return node
# ─── 查找 ───
def search(self, key):
"""查找键对应的值"""
return self._search(self.root, key)
def _search(self, node, key):
if not node:
return None
if key == node.key:
return node.value
elif key < node.key:
return self._search(node.left, key)
else:
return self._search(node.right, key)
def contains(self, key):
return self.search(key) is not None
# ─── 插入 ───
def insert(self, key, value=None):
"""插入键值对"""
self.root = self._insert(self.root, key, value)
def _insert(self, node, key, value):
if not node:
return TreeNode(key, value)
if key == node.key:
node.value = value # 更新
elif key < node.key:
node.left = self._insert(node.left, key, value)
else:
node.right = self._insert(node.right, key, value)
return self._update_size(node)
# ─── 删除(最复杂)───
def delete(self, key):
"""删除指定键"""
self.root = self._delete(self.root, key)
def _delete(self, node, key):
if not node:
return None
if key < node.key:
node.left = self._delete(node.left, key)
elif key > node.key:
node.right = self._delete(node.right, key)
else:
# 找到要删除的节点
# 情况1:无左子树
if not node.left:
return node.right
# 情况2:无右子树
if not node.right:
return node.left
# 情况3:有两个子树
# 找到右子树的最小节点(后继)
successor = self._min(node.right)
node.key = successor.key
node.value = successor.value
node.right = self._delete_min(node.right)
return self._update_size(node)
def _min(self, node):
"""找到最小节点"""
while node.left:
node = node.left
return node
def _delete_min(self, node):
"""删除最小节点"""
if not node.left:
return node.right
node.left = self._delete_min(node.left)
return self._update_size(node)
# ─── 遍历 ───
def inorder(self):
"""中序遍历(升序输出)"""
result = []
self._inorder(self.root, result)
return result
def _inorder(self, node, result):
if node:
self._inorder(node.left, result)
result.append((node.key, node.value))
self._inorder(node.right, result)
# ─── 排名相关 ───
def rank(self, key):
"""
返回小于key的键的数量
即key在有序序列中的排名(从0开始)
"""
return self._rank(self.root, key)
def _rank(self, node, key):
if not node:
return 0
if key == node.key:
return self._size(node.left)
elif key < node.key:
return self._rank(node.left, key)
else:
return 1 + self._size(node.left) + self._rank(node.right, key)
def select(self, k):
"""
返回排名为k的键(第k小的键,从0开始)
"""
return self._select(self.root, k)
def _select(self, node, k):
if not node:
return None
left_size = self._size(node.left)
if k < left_size:
return self._select(node.left, k)
elif k == left_size:
return node.key
else:
return self._select(node.right, k - left_size - 1)
# ─── 范围查询 ───
def range_search(self, low, high):
"""查找[low, high]范围内的所有键"""
result = []
self._range_search(self.root, low, high, result)
return result
def _range_search(self, node, low, high, result):
if not node:
return
if low < node.key:
self._range_search(node.left, low, high, result)
if low <= node.key <= high:
result.append((node.key, node.value))
if node.key < high:
self._range_search(node.right, low, high, result)
# 测试
bst = BST()
# 插入
keys = [50, 30, 70, 20, 40, 60, 80]
for k in keys:
bst.insert(k, f"value_{k}")
print("中序遍历:", bst.inorder())
# [(20, 'value_20'), (30, 'value_30'), ..., (80, 'value_80')]
print("查找 40:", bst.search(40)) # value_40
print("查找 100:", bst.search(100)) # None
print("排名 of 40:", bst.rank(40)) # 3(20,30,40前面有3个)
print("第3小的键:", bst.select(3)) # 40
print("范围查询 [30, 70]:", bst.range_search(30, 70))
# [(30, 'value_30'), (40, 'value_40'), (50, 'value_50'), (60, 'value_60'), (70, 'value_70')]
# 删除
bst.delete(30)
print("删除30后:", bst.inorder())
2.3 BST的退化问题
BST的最大问题:可能退化成链表!
最坏情况(插入有序数据):
10
\
20
\
30
\
40
\
50
树高 = n = 5,查找退化到 O(n)!
对比平衡BST:
30
/ \
20 40
/ \
10 50
树高 = log(n) ≈ 2.3,查找 O(log n)
⚠️ 关键问题:如何在插入删除后保持树的平衡?
3. AVL树:严格平衡的艺术
3.1 AVL树的定义
AVL树:一棵BST,其中每个节点的左右子树高度差(平衡因子)的绝对值不超过1。
AVL树示例:
30 (平衡因子=0)
/ \
20 40 (BF=0)
/ \ / \
10 25 35 50 (BF=0)
所有节点的 |BF| <= 1
平衡因子计算:
BF(node) = height(left) - height(right)
节点30:h(left)=2, h(right)=2, BF=0 ✓
节点20:h(left)=1, h(right)=1, BF=0 ✓
节点40:h(left)=1, h(right)=1, BF=0 ✓
3.2 旋转操作:恢复平衡的核心
四种旋转情况:
1. 左旋(LL旋转):右子树太高
y x
/ \ / \
T1 x → y z
/ \ / \ / \
T2 z T1 T2 T3 T4
/ \
T3 T4
2. 右旋(RR旋转):左子树太高
x y
/ \ / \
y T4 → z x
/ \ / \ / \
z T3 T1 T2 T3 T4
/ \
T1 T2
3. 左右旋(LR旋转):左子树的右子树太高
z z y
/ \ / \ / \
x T4 → y T4 → x z
/ \ / \ / \ / \
T1 y x T3 T1 T2 T3 T4
/ \ / \
T2 T3 T1 T2
4. 右左旋(RL旋转):右子树的左子树太高
x x y
/ \ / \ / \
T1 z → T1 y → x z
/ \ / \ / \ / \
y T4 T2 z T1 T2 T3 T4
/ \ / \
T2 T3 T3 T4
3.3 完整AVL树实现
python
class AVLNode:
"""AVL树节点"""
def __init__(self, key, value=None):
self.key = key
self.value = value
self.left = None
self.right = None
self.height = 1 # 新增:节点高度
def __repr__(self):
return f"AVLNode({self.key}, h={self.height})"
class AVLTree:
"""
AVL树:严格平衡的二叉搜索树
性质:任意节点的 |BF| <= 1
查找、插入、删除:O(log n)
"""
def __init__(self):
self.root = None
# ─── 高度和平衡因子 ───
def _height(self, node):
return node.height if node else 0
def _balance_factor(self, node):
return self._height(node.left) - self._height(node.right)
def _update_height(self, node):
if node:
node.height = 1 + max(
self._height(node.left),
self._height(node.right)
)
return node
# ─── 旋转操作 ───
def _rotate_right(self, y):
"""
右旋(RR旋转)
y x
/ \ / \
x T3 → T1 y
/ \ / \
T1 T2 T2 T3
"""
x = y.left
T2 = x.right
# 旋转
x.right = y
y.left = T2
# 更新高度(先更新y,再更新x)
self._update_height(y)
self._update_height(x)
return x
def _rotate_left(self, x):
"""
左旋(LL旋转)
x y
/ \ / \
T1 y → x T3
/ \ / \
T2 T3 T1 T2
"""
y = x.right
T2 = y.left
# 旋转
y.left = x
x.right = T2
# 更新高度
self._update_height(x)
self._update_height(y)
return y
# ─── 平衡修复 ───
def _rebalance(self, node):
"""检查并修复平衡"""
if not node:
return node
self._update_height(node)
bf = self._balance_factor(node)
# 左子树太高
if bf > 1:
if self._balance_factor(node.left) < 0:
# LR情况:先左旋左子树
node.left = self._rotate_left(node.left)
# LL情况:右旋
return self._rotate_right(node)
# 右子树太高
if bf < -1:
if self._balance_factor(node.right) > 0:
# RL情况:先右旋右子树
node.right = self._rotate_right(node.right)
# RR情况:左旋
return self._rotate_left(node)
return node # 已经平衡
# ─── 插入 ───
def insert(self, key, value=None):
self.root = self._insert(self.root, key, value)
def _insert(self, node, key, value):
if not node:
return AVLNode(key, value)
if key < node.key:
node.left = self._insert(node.left, key, value)
elif key > node.key:
node.right = self._insert(node.right, key, value)
else:
node.value = value # 更新
# 插入后重新平衡
return self._rebalance(node)
# ─── 删除 ───
def delete(self, key):
self.root = self._delete(self.root, key)
def _delete(self, node, key):
if not node:
return None
if key < node.key:
node.left = self._delete(node.left, key)
elif key > node.key:
node.right = self._delete(node.right, key)
else:
# 找到要删除的节点
if not node.left:
return node.right
if not node.right:
return node.left
# 有两个子树:找后继(右子树最小)
successor = self._min(node.right)
node.key = successor.key
node.value = successor.value
node.right = self._delete(node.right, successor.key)
# 删除后重新平衡
return self._rebalance(node)
def _min(self, node):
while node.left:
node = node.left
return node
# ─── 查找 ───
def search(self, key):
return self._search(self.root, key)
def _search(self, node, key):
if not node:
return None
if key == node.key:
return node.value
elif key < node.key:
return self._search(node.left, key)
else:
return self._search(node.right, key)
# ─── 验证AVL性质 ───
def is_valid_avl(self):
"""验证是否为合法AVL树"""
return self._is_valid_avl(self.root)
def _is_valid_avl(self, node):
if not node:
return True
bf = self._balance_factor(node)
if abs(bf) > 1:
return False
return (self._is_valid_avl(node.left) and
self._is_valid_avl(node.right))
# 测试AVL树
avl = AVLTree()
# 插入有序数据(BST会退化,AVL保持平衡)
for i in range(1, 8):
avl.insert(i)
print("AVL验证:", avl.is_valid_avl()) # True
# 验证高度
print("根节点高度:", avl.root.height) # 3(log2(7)≈2.8,向上取整为3)
# 对比BST退化情况
bst = BST()
for i in range(1, 8):
bst.insert(i)
# bst.root.height 会是 7(退化链表)
3.4 AVL树的性能分析
AVL树的高度证明:
设 N(h) 为高度为h的AVL树的最少节点数
N(0) = 0
N(1) = 1
N(h) = N(h-1) + N(h-2) + 1 (类似斐波那契)
可以证明:N(h) ≈ φ^h / √5,其中 φ ≈ 1.618
因此:h ≈ 1.44 * log₂(n)
结论:
- AVL树高最多为 1.44 * log₂(n)
- 查找、插入、删除都是严格的 O(log n)
- 但旋转操作频繁,插入删除的常数因子较大
4. 红黑树:实用主义的平衡
4.1 红黑树的定义
红黑树(Red-Black Tree):一棵BST,每个节点标记为红色或黑色,满足以下性质:
- 根节点是黑色
- 红色节点的子节点必须是黑色(不能有连续红色)
- 从任意节点到其每个叶子节点的路径上,黑色节点数相同(黑高相等)
红黑树示例(B=黑,R=红):
30(B)
/ \
20(B) 40(B)
/ \ / \
10(R) 25(R) 35(R) 50(R)
验证性质:
1. 根30是黑色 ✓
2. 无连续红色(红节点的子都是黑) ✓
3. 到各叶子路径的黑节点数:
30→20→10:2个黑(30,20)
30→20→25:2个黑(30,20)
30→40→35:2个黑(30,40)
30→40→50:2个黑(30,40) ✓
4.2 红黑树 vs AVL树
| 特性 | AVL树 | 红黑树 |
|---|---|---|
| 平衡条件 | |BF| <= 1(严格) | 黑高相等(宽松) |
| 树高 | <= 1.44 log₂(n) | <= 2 log₂(n) |
| 查找速度 | 更快(树更矮) | 稍慢(树稍高) |
| 插入旋转 | 最多2次 | 最多2次 |
| 删除旋转 | 最多O(log n)次 | 最多3次 |
| 实现复杂度 | 较复杂 | 更复杂(但常数更优) |
| 实际应用 | 数据库理论 | Linux内核、C++ map、Java TreeMap |
💡 核心洞察:红黑树用"近似平衡"换取更少的旋转操作,在实际应用中(尤其是删除频繁时)性能更优。
4.3 红黑树的关键操作
python
class Color:
RED = 0
BLACK = 1
class RBNode:
"""红黑树节点"""
def __init__(self, key, value=None):
self.key = key
self.value = value
self.color = Color.RED # 新节点默认红色
self.left = None
self.right = None
self.parent = None # 需要父指针
class RedBlackTree:
"""
红黑树实现(简化版,展示核心逻辑)
实际应用:C++ std::map/set, Java TreeMap, Linux内核
"""
def __init__(self):
self.NIL = RBNode(0) # 哨兵叶子节点
self.NIL.color = Color.BLACK
self.root = self.NIL
def _rotate_left(self, x):
"""左旋"""
y = x.right
x.right = y.left
if y.left != self.NIL:
y.left.parent = x
y.parent = x.parent
if x.parent == self.NIL:
self.root = y
elif x == x.parent.left:
x.parent.left = y
else:
x.parent.right = y
y.left = x
x.parent = y
def _rotate_right(self, y):
"""右旋"""
x = y.left
y.left = x.right
if x.right != self.NIL:
x.right.parent = y
x.parent = y.parent
if y.parent == self.NIL:
self.root = x
elif y == y.parent.right:
y.parent.right = x
else:
y.parent.left = x
x.right = y
y.parent = x
def _fix_insert(self, k):
"""插入后修复红黑性质"""
while k.parent.color == Color.RED:
if k.parent == k.parent.parent.right:
u = k.parent.parent.left # 叔叔节点
if u.color == Color.RED:
# 情况1:叔叔是红色
u.color = Color.BLACK
k.parent.color = Color.BLACK
k.parent.parent.color = Color.RED
k = k.parent.parent
else:
if k == k.parent.left:
# 情况2:叔叔黑,当前是左子
k = k.parent
self._rotate_right(k)
# 情况3:叔叔黑,当前是右子
k.parent.color = Color.BLACK
k.parent.parent.color = Color.RED
self._rotate_left(k.parent.parent)
else:
# 对称情况
u = k.parent.parent.right
if u.color == Color.RED:
u.color = Color.BLACK
k.parent.color = Color.BLACK
k.parent.parent.color = Color.RED
k = k.parent.parent
else:
if k == k.parent.right:
k = k.parent
self._rotate_left(k)
k.parent.color = Color.BLACK
k.parent.parent.color = Color.RED
self._rotate_right(k.parent.parent)
if k == self.root:
break
self.root.color = Color.BLACK
def insert(self, key, value=None):
"""插入"""
node = RBNode(key, value)
node.parent = self.NIL
node.left = self.NIL
node.right = self.NIL
y = self.NIL
x = self.root
while x != self.NIL:
y = x
if node.key < x.key:
x = x.left
else:
x = x.right
node.parent = y
if y == self.NIL:
self.root = node
elif node.key < y.key:
y.left = node
else:
y.right = node
if node.parent == self.NIL:
node.color = Color.BLACK
return
if node.parent.parent == self.NIL:
return
self._fix_insert(node)
def search(self, key):
"""查找"""
current = self.root
while current != self.NIL and key != current.key:
if key < current.key:
current = current.left
else:
current = current.right
return current if current != self.NIL else None
# 红黑树在实际中更复杂,这里展示核心逻辑
# 实际使用建议直接用语言内置实现:
# Python: 无内置红黑树,可用 sortedcontainers 库
# C++: std::map, std::set
# Java: TreeMap, TreeSet
5. B树与B+树:为磁盘而生
5.1 为什么需要B树?
内存 vs 磁盘访问时间:
内存访问:约 100 ns
磁盘访问:约 10 ms = 10,000,000 ns
差距:10万倍!
问题:如果BST存储在磁盘上,每次节点访问都需要磁盘IO!
BST(高度=20):
查找可能需要 20 次磁盘IO = 200 ms
需要:减少树高,减少磁盘访问次数!
解决方案:B树(多路平衡树)
- 每个节点存多个键
- 分支因子大,树高小
- 一次磁盘读可以读一个节点(多个键)
5.2 B树的定义
B树(B-Tree):一棵m阶B树满足:
- 每个节点最多有 m 个子节点
- 除根外,每个节点至少有 ⌈m/2⌉ 个子节点
- 所有叶子节点在同一层
- 节点内的键有序排列
3阶B树(2-3树)示例:
[50]
/ \
[20,30] [60,70]
/ | \ / | \
10 25 40 55 65 80
特点:
- 每个节点2-3个子节点
- 节点内键有序:[20,30] 表示 20和30之间的值在它们中间子树
- 所有叶子在同一层
5.3 B+树的改进
B+树:B树的变种,所有数据都存储在叶子节点,内部节点只存键作为索引。
B+树示例:
内部节点(仅存键,不存数据):
[50]
/ \
[20,30] [60,70]
叶子节点(存实际数据,链表连接):
[10]→[20]→[25]→[30]→[40]→[50]→[55]→[60]→[65]→[70]→[80]
↑_____________________________________________________↓
双向链表
B+树优势:
1. 内部节点更小,一个磁盘页可以存更多键
2. 叶子节点链表连接,范围查询极快
3. 所有查询都到叶子,性能稳定
5.4 B+树在数据库中的应用
MySQL InnoDB索引结构:
磁盘页大小:16KB
假设:
- 主键类型:BIGINT (8 bytes)
- 指针大小:6 bytes
- 每个内部节点可存键数:16KB / (8+6) ≈ 1170 个
树高计算:
- 高1(根):1170 个键 → 1170 个叶子
- 高2:1170 × 1170 = 136万 个叶子
- 高3:1170 × 1170 × 1170 = 16亿 个叶子
结论:
- 16亿条记录,树高仅3!
- 查找最多3次磁盘IO
- 加上缓存,通常1-2次IO即可
5.5 B+树简化实现
python
class BPlusTreeNode:
"""B+树节点"""
def __init__(self, leaf=False):
self.leaf = leaf # 是否为叶子节点
self.keys = [] # 键列表
self.children = [] # 子节点指针(内部节点)
self.next = None # 叶子节点的下一个指针(链表)
self.values = [] # 叶子节点的值列表
class BPlusTree:
"""
B+树简化实现
实际数据库中的B+树更复杂,包含:
- 节点分裂与合并
- 延迟分裂优化
- 前缀压缩
- 并发控制
"""
def __init__(self, order=4):
self.order = order # 阶数(最大子节点数)
self.min_keys = (order - 1) // 2 # 最小键数
self.root = BPlusTreeNode(leaf=True)
def search(self, key):
"""查找"""
node = self.root
# 下降到叶子
while not node.leaf:
i = 0
while i < len(node.keys) and key >= node.keys[i]:
i += 1
node = node.children[i]
# 在叶子中查找
for i, k in enumerate(node.keys):
if k == key:
return node.values[i]
return None
def range_query(self, start, end):
"""范围查询(B+树的优势!)"""
results = []
# 找到起始叶子
node = self._find_leaf(start)
# 沿链表遍历
while node:
for i, key in enumerate(node.keys):
if start <= key <= end:
results.append((key, node.values[i]))
elif key > end:
return results
node = node.next
return results
def _find_leaf(self, key):
"""找到key应该在的叶子节点"""
node = self.root
while not node.leaf:
i = 0
while i < len(node.keys) and key >= node.keys[i]:
i += 1
node = node.children[i]
return node
def insert(self, key, value):
"""插入(简化版,省略分裂逻辑)"""
leaf = self._find_leaf(key)
# 找到插入位置
i = 0
while i < len(leaf.keys) and leaf.keys[i] < key:
i += 1
leaf.keys.insert(i, key)
leaf.values.insert(i, value)
# 如果溢出,需要分裂(省略完整实现)
if len(leaf.keys) >= self.order:
self._split_leaf(leaf)
def _split_leaf(self, node):
"""分裂叶子节点(简化)"""
mid = len(node.keys) // 2
# 创建新节点
new_node = BPlusTreeNode(leaf=True)
new_node.keys = node.keys[mid:]
new_node.values = node.values[mid:]
# 原节点保留前半
node.keys = node.keys[:mid]
node.values = node.values[:mid]
# 维护链表
new_node.next = node.next
node.next = new_node
# 将中间键提升到父节点(省略父节点处理)
# B+树实际应用演示
print("B+树在数据库中的应用:")
print("- MySQL InnoDB主键索引")
print("- MongoDB的 WiredTiger 存储引擎")
print("- 文件系统的目录索引")
print("- 范围查询性能优异(叶子链表)")
6. 树表查找对比总结
6.1 核心特性对比
| 特性 | BST | AVL树 | 红黑树 | B树 | B+树 |
|---|---|---|---|---|---|
| 平衡性 | 不平衡 | 严格平衡 | 近似平衡 | 多路平衡 | 多路平衡 |
| 树高 | O(n)~O(log n) | 1.44 log₂n | ≤ 2 log₂n | logₘn | logₘn |
| 查找 | O(n)~O(log n) | O(log n) | O(log n) | O(log n) | O(log n) |
| 插入 | O(n)~O(log n) | O(log n) | O(log n) | O(log n) | O(log n) |
| 删除 | O(n)~O(log n) | O(log n) | O(log n) | O(log n) | O(log n) |
| 旋转次数 | - | 较多 | 较少 | 节点分裂/合并 | 节点分裂/合并 |
| 范围查询 | 中序遍历 | 中序遍历 | 中序遍历 | 中序遍历 | 叶子链表(最优) |
| 适用场景 | 教学/简单应用 | 查找频繁 | 通用(语言内置) | 文件系统 | 数据库索引 |
6.2 实际应用选择指南
选择决策树:
需要磁盘存储或大数据量? ──是──→ 需要范围查询?
│ │
否 是
│ ▼
▼ B+树(数据库标准)
数据量小,内存操作 (MySQL、MongoDB)
│
▼
需要严格的最坏情况性能? ──是──→ AVL树
│
否
▼
需要语言内置/广泛支持? ──是──→ 红黑树
│ (C++ map, Java TreeMap)
否
▼
简单实现,数据随机? ──是──→ 普通BST + 随机化
│
否
▼
Treap / Splay树等高级结构