【学习】《算法图解》第八章学习笔记:平衡树
前言
在上一章中,我们学习了二叉搜索树(BST)的基本概念和操作。虽然BST在平均情况下提供了O(log n)的搜索、插入和删除效率,但在最坏情况下(如按顺序插入数据),它可能退化为链表,导致操作效率降为O(n)。为了解决这个问题,《算法图解》第八章介绍了平衡树的概念和几种主要的平衡树结构,这些结构能够在各种情况下保持较好的平衡性,确保操作的高效性。
一、平衡树的基本概念
(一)什么是平衡树
平衡树是一种特殊的二叉搜索树,它通过某些机制来保持树的平衡,避免树向一侧过度生长。所谓"平衡",通常是指树的左右子树高度大致相同,或者满足某些特定的平衡条件。
平衡树的目标是确保树的高度接近于log n(其中n是节点数),这样可以保证基本操作(查找、插入、删除)的时间复杂度为O(log n),即使在最坏情况下也是如此。
(二)为什么需要平衡树
回顾上一章中提到的问题:当按顺序(如升序或降序)向BST中插入数据时,树会变得极度不平衡,形成一个链表状结构:
1
\
2
\
3
\
4
\
5
在这种情况下,查找、插入和删除操作的时间复杂度都退化为O(n),失去了BST的效率优势。平衡树通过在插入和删除操作中自动调整树的结构,确保树始终保持相对平衡,从而避免这种性能退化。
二、AVL树
(一)基本特性
AVL树是最早被发明的自平衡二叉搜索树之一,由G.M. Adelson-Velsky和E.M. Landis在1962年提出(AVL就是取自他们名字的首字母)。
AVL树的关键特性是:
-
它是一个二叉搜索树 -
对于树中的每个节点,其左右子树的高度差(平衡因子)不超过1 -
当进行插入或删除操作导致树失去平衡时,通过旋转操作重新平衡树
(二)平衡因子
在AVL树中,每个节点的平衡因子定义为其左子树高度减去右子树高度的差值:
-
平衡因子 = 左子树高度 - 右子树高度 -
平衡因子必须是 -1, 0 或 1(平衡条件)
如果平衡因子的绝对值超过1,则树处于不平衡状态,需要通过旋转来恢复平衡。
(三)旋转操作
AVL树通过四种基本的旋转操作来维持平衡:
-
左旋转(Left Rotation):当右子树过高时使用 -
右旋转(Right Rotation):当左子树过高时使用 -
左-右旋转(Left-Right Rotation):先对左子节点进行左旋转,然后对当前节点进行右旋转 -
右-左旋转(Right-Left Rotation):先对右子节点进行右旋转,然后对当前节点进行左旋转
这些旋转操作可以在O(1)时间内完成,因此不会显著影响插入和删除操作的效率。
(四)AVL树的优缺点
优点:
-
保证了最坏情况下O(log n)的性能 -
高度平衡,查找效率稳定
缺点:
-
插入和删除操作可能需要多次旋转,开销较大 -
每个节点需要存储额外的平衡因子或高度信息
三、红黑树
(一)基本特性
红黑树是另一种广泛使用的自平衡二叉搜索树,由Rudolf Bayer在1972年首次提出,后来由Leo J. Guibas和Robert Sedgewick完善并命名。红黑树不像AVL树那样严格平衡,但仍能保证O(log n)的操作效率,且插入和删除操作所需的旋转次数通常少于AVL树。
红黑树的每个节点都有一个颜色属性,可以是红色或黑色。红黑树必须满足以下五个性质:
-
每个节点要么是红色,要么是黑色 -
根节点必须是黑色 -
所有叶节点(NIL节点,空节点)都是黑色 -
如果一个节点是红色,则其两个子节点都必须是黑色(即不能有两个连续的红节点) -
对于每个节点,从该节点到其所有后代叶节点的简单路径上,包含相同数量的黑色节点
这些性质共同确保了红黑树的关键特性:从根到最远叶节点的路径长度不会超过从根到最近叶节点的路径长度的两倍。这保证了红黑树的高度大致上是log n,因此所有基本操作的时间复杂度都是O(log n)。
(二)红黑树的平衡操作
红黑树通过颜色变换和旋转操作来维持平衡:
-
重新着色(Recoloring):改变节点的颜色,这是一种简单且常用的操作 -
旋转(Rotation):与AVL树类似,包括左旋转和右旋转,用于调整树的结构
插入或删除节点后,如果违反了红黑树的任何性质,就会触发重新着色和/或旋转操作,以恢复这些性质。
(三)红黑树的优缺点
优点:
-
插入和删除操作所需的平衡操作(旋转)次数少于AVL树 -
实际应用中,性能表现优异 -
内存占用相对较小,只需要一个额外的比特来存储颜色
缺点:
-
实现复杂度高,特别是删除操作 -
对于频繁查找但较少插入删除的场景,性能可能不如AVL树
(四)红黑树的应用
红黑树在实际系统和库中应用广泛:
-
Java中的TreeMap和TreeSet -
C++ STL中的map、set、multimap和multiset -
Linux内核中的完全公平调度器 -
许多数据库系统的索引结构
四、B树和B+树
虽然AVL树和红黑树主要用于内存中的数据结构,但当数据量大到无法全部加载到内存中时,我们需要考虑磁盘IO操作的效率。B树和B+树是专为外部存储(如磁盘)设计的平衡树结构。
(一)B树
B树(或B-树)是一种自平衡的多路搜索树,由Rudolf Bayer和Edward McCreight在1972年提出。与二叉搜索树不同,B树的每个节点可以有多个子节点,通常是设计成与磁盘页或系统块大小相匹配的结构。
B树的主要特性:
-
所有叶节点都在同一层(完美平衡) -
每个节点最多可以有m个子节点(m称为B树的阶) -
除根节点和叶节点外,每个节点至少有⌈m/2⌉个子节点 -
如果根节点不是叶节点,则至少有2个子节点 -
一个有k个子节点的内部节点包含k-1个键(有序) -
键将节点的子树分开:在子树i中的所有键都小于节点中的第i个键,所有大于第i个键的键都在子树i+1中
B树的设计使得即使处理大量数据,树的高度也能保持在较低水平,从而减少磁盘IO操作次数。
(二)B+树
B+树是B树的一种变体,广泛用于数据库和文件系统中。B+树与B树的主要区别在于:
-
所有的数据记录都存储在叶子节点中 -
内部节点只存储键值,不存储数据,这使得内部节点可以存储更多的键 -
叶子节点之间通过指针连接,形成一个有序链表,便于范围查询 -
每个叶子节点包含了所有的索引字段
这些特性使B+树特别适合于数据库索引:
-
由于内部节点不存储数据,所以同样大小的节点可以存储更多的键,从而降低树的高度 -
叶节点链表使得范围查询非常高效 -
所有查询都会访问叶节点,查询路径长度相同,性能更稳定
(三)应用场景
B树和B+树在大型数据存储系统中有广泛应用:
-
MySQL的InnoDB存储引擎使用B+树实现索引 -
Oracle、PostgreSQL等数据库也使用B+树索引 -
许多文件系统,如NTFS、XFS等,使用B树或其变体来组织文件
五、平衡树的比较与选择
(一)不同平衡树的特点比较
特性 | AVL树 | 红黑树 | B树 | B+树 |
---|---|---|---|---|
平衡条件 | 严格(高度差≤1) | 适中(黑色节点平衡) | 多路平衡 | 多路平衡+叶节点链表 |
查找性能 | O(log n),常数因子小 | O(log n),常数因子略大 | O(log n),基于块访问 | O(log n),基于块访问 |
插入/删除平衡开销 | 较高(可能多次旋转) | 适中(最多三次旋转) | 较低(分裂/合并操作) | 较低(分裂/合并操作) |
内存开销 | 每节点存储平衡因子 | 每节点存储颜色位 | 多键多指针 | 多键多指针+叶节点链表 |
主要应用 | 查询密集型内存数据结构 | 通用内存数据结构 | 外存数据组织 | 数据库索引 |
(二)如何选择适合的平衡树
选择合适的平衡树结构应考虑以下因素:
-
数据规模:
-
内存中数据:考虑AVL树或红黑树 -
磁盘上数据:考虑B树或B+树
-
-
操作频率:
-
查询频繁,修改较少:AVL树 -
查询和修改都频繁:红黑树 -
需要高效范围查询:B+树
-
-
实现复杂度:
-
红黑树实现复杂度高于AVL树 -
B树和B+树实现更为复杂
-
-
具体应用场景:
-
数据库索引:通常选择B+树 -
内存中的映射/集合:通常选择红黑树 -
需要严格平衡的场景:选择AVL树
-
六、Python实现平衡树示例
以下是一个简单的AVL树实现示例:
class AVLNode:
def __init__(self, key):
self.key = key
self.left = None
self.right = None
self.height = 1 # 新节点的初始高度为1
class AVLTree:
def get_height(self, node):
if not node:
return 0
return node.height
def get_balance(self, node):
if not node:
return 0
return self.get_height(node.left) - self.get_height(node.right)
def update_height(self, node):
if not node:
return
node.height = 1 + max(self.get_height(node.left), self.get_height(node.right))
def right_rotate(self, y):
x = y.left
T2 = x.right
# 执行旋转
x.right = y
y.left = T2
# 更新高度
self.update_height(y)
self.update_height(x)
return x
def left_rotate(self, x):
y = x.right
T2 = y.left
# 执行旋转
y.left = x
x.right = T2
# 更新高度
self.update_height(x)
self.update_height(y)
return y
def insert(self, root, key):
# 标准BST插入
if not root:
return AVLNode(key)
if key < root.key:
root.left = self.insert(root.left, key)
elif key > root.key:
root.right = self.insert(root.right, key)
else: # 相等的键不允许(或可以处理为计数器增加)
return root
# 更新高度
self.update_height(root)
# 获取平衡因子
balance = self.get_balance(root)
# 如果节点不平衡,则有四种情况
# 左左情况
if balance > 1 and key < root.left.key:
return self.right_rotate(root)
# 右右情况
if balance < -1 and key > root.right.key:
return self.left_rotate(root)
# 左右情况
if balance > 1 and key > root.left.key:
root.left = self.left_rotate(root.left)
return self.right_rotate(root)
# 右左情况
if balance < -1 and key < root.right.key:
root.right = self.right_rotate(root.right)
return self.left_rotate(root)
return root
def in_order_traversal(self, root):
result = []
if root:
result = self.in_order_traversal(root.left)
result.append(root.key)
result += self.in_order_traversal(root.right)
return result
# 使用示例
avl_tree = AVLTree()
root = None
# 插入节点
keys = [9, 5, 10, 0, 6, 11, -1, 1, 2]
for key in keys:
root = avl_tree.insert(root, key)
print("AVL树的中序遍历结果:", avl_tree.in_order_traversal(root))
七、总结
平衡树是解决普通二叉搜索树在特定情况下性能退化问题的关键数据结构。不同类型的平衡树各有特点,适用于不同的应用场景:
-
AVL树:提供严格平衡,适合查询密集型应用 -
红黑树:平衡性适中,插入删除效率高,实际应用广泛 -
B树和B+树:专为外部存储设计,广泛应用于数据库和文件系统
掌握这些平衡树结构及其特性,对于理解和设计高效的数据处理系统至关重要。平衡树体现了计算机科学中平衡"理论最优"和"实际可行"的智慧,是算法设计中的经典案例。
八、参考资料
-
《算法图解》(Grokking Algorithms)by Aditya Y. Bhargava -
AVL树 - 维基百科 -
红黑树 - 维基百科 -
B树 - 维基百科 -
B+树 - 维基百科 -
Chapter 8. Balanced Trees · Grokking Algorithms - Manning Publications
本文由 mdnice 多平台发布