树是一种重要的非线性数据结构,由节点(或顶点)和边组成,具有层次关系。
一、 链表-斜树(Skewed Tree)
链表是一种常见的基础数据结构,是一种线性表,但不像数组那样是连续的内存空间,而是通过指针将一组零散的内存块串联起来使用。
(一) 节点结构
1、 每个节点包含两部分:
数据域(Data): 存储实际数据(如整数、字符串等)。
指针域(Pointer): 指向下一个节点的地址(或前驱/后继节点,取决于链表类型)。
2、 动态内存分配
链表通过指针动态链接节点,内存空间在运行时按需分配,适合数据频繁增删的场景。
(二) 链表类型
1、 单向链表(Singly Linked List)
结构:每个节点仅指向下一个节点,末尾节点指向NULL。
插入:修改新节点和前驱节点的指针。
删除:修改前驱节点的指针指向待删除节点的后继。
2、 双向链表(Doubly Linked List)
结构:每个节点包含前驱和后继指针,支持双向遍历。
优势:可反向遍历,但每个节点多占用一个指针的内存。
3、 循环链表(Circular Linked List)
结构:尾节点指向头节点,形成环状结构。
应用:实现约瑟夫问题、环形缓冲区等。
4、 双向循环链表
结构:结合双向和循环特性,首尾节点互相指向。
(三)代码示例
class Node:
"""双向链表节点类"""
def __init__(self, data):
# 节点存储的数据
self.data = data
# 前驱节点指针
self.prev = None
# 后继节点指针
self.next = None
class DoublyCircularLinkedList:
"""双向循环链表类"""
def __init__(self):
# 头节点初始化
self.head = None
def is_empty(self):
"""检查链表是否为空"""
return self.head is None
def length(self):
"""获取链表长度"""
if self.is_empty():
return 0
count = 1
current = self.head.next
while current != self.head:
count += 1
current = current.next
return count
def append(self, data):
"""在链表尾部添加节点"""
new_node = Node(data)
if self.is_empty():
# 新节点作为头节点
self.head = new_node
# 下一个指向自己
new_node.next = new_node
# 上一个指向自己
new_node.prev = new_node
else:
# 获取尾节点
tail = self.head.prev
# 尾节点指向新节点
tail.next = new_node
# 新节点前驱指向尾节点
new_node.prev = tail
# 新节点后继指向头节点
new_node.next = self.head
# 头节点前驱指向新节点
self.head.prev = new_node
def prepend(self, data):
"""在链表头部添加节点"""
new_node = Node(data)
if self.is_empty():
self.head = new_node
new_node.next = new_node
new_node.prev = new_node
else:
# 获取尾节点
tail = self.head.prev
# 新节点指向原头节点
new_node.next = self.head
# 新节点前驱指向尾节点
new_node.prev = tail
# 原头节点前驱指向新节点
self.head.prev = new_node
# 尾节点指向新节点
tail.next = new_node
# 更新头节点为新节点
self.head = new_node
def insert(self, index, data):
"""在指定位置插入节点"""
if index <= 0:
self.prepend(data)
elif index >= self.length():
self.append(data)
else:
new_node = Node(data)
# 从头节点开始
current = self.head
# 移动到插入位置前一个节点
for _ in range(index - 1):
current = current.next
# 新节点指向当前节点的下一个
new_node.next = current.next
# 新节点前驱指向当前节点
new_node.prev = current
# 当前节点下一个的前驱指向新节点
current.next.prev = new_node
# 当前节点指向新节点
current.next = new_node
def delete(self, data):
"""删除指定数据的节点"""
if self.is_empty():
return
# 从头节点开始
current = self.head
if current.data == data:
if current.next == self.head:
self.head = None
else:
# 获取尾节点
tail = self.head.prev
# 更新头节点
self.head = current.next
# 新头节点前驱指向尾节点
self.head.prev = tail
# 尾节点指向新头节点
tail.next = self.head
return
current = current.next
while current != self.head:
if current.data == data:
# 前一个节点跳过当前节点
current.prev.next = current.next
# 后一个节点跳过当前节点
current.next.prev = current.prev
return
current = current.next
def display(self):
"""正向遍历打印链表"""
if self.is_empty():
print("链表为空")
return
elements = []
current = self.head
while True:
elements.append(str(current.data))
current = current.next
if current == self.head:
break
print(" <-> ".join(elements) + " <-> (循环)")
def display_reverse(self):
"""反向遍历打印链表"""
if self.is_empty():
print("链表为空")
return
elements = []
current = self.head.prev
while True:
elements.append(str(current.data))
current = current.prev
if current == self.head.prev:
break
print(" <-> ".join(elements) + " <-> (循环反向)")
if __name__ == "__main__":
dll = DoublyCircularLinkedList()
# 添加元素
dll.append(10)
dll.append(20)
dll.append(30)
dll.prepend(5)
dll.insert(2, 15)
print("正向遍历:")
dll.display()
print("\n反向遍历:")
dll.display_reverse()
# 删除元素
dll.delete(15)
dll.delete(5)
dll.delete(30)
print("\n删除后的链表:")
dll.display()
print("\n链表长度:", dll.length())
(四) 应用
1、实现栈和队列
2、哈希表的链地址法解决冲突
3、LRU缓存淘汰算法
4、多项式表示(每个节点存储系数和指数)
5、操作系统中的进程调度(就绪队列)
6、图的邻接表表示法
二、 二叉树(Binary Tree)
每个节点最多有两个子节点,分别称为左子节点和右子节点。
(一) 完全二叉树
1、 特点
除最后一层外,其他层的节点必须完全填满(即每一层从左到右无空缺)。
最后一层的节点必须连续集中在左侧(即允许右侧有空缺,但左侧不能有空缺)。
2、 应用
1)堆Heap数据结构:优先队列、Dijkstra算法等。
2)数组表示的二叉树:节省存储空间,快速访问子节点。
3)哈夫曼编码树:构建最优前缀码时使用。
(二) 满二叉树 ( Full Binary Tree )
所有非叶子节点都有两个子节点,且所有叶子节点在同一层。
1、 特点
1)每个节点要么是叶子节点(没有子节点),要么恰好有两个子节点(左子节点和右子节点)
2)所有叶子节点都在同一层上
3)高度为h的满二叉树有2^h - 1个节点
2、 性质
1)节点数量:高度为h的满二叉树共有2^h - 1个节点
2)叶子节点数量:总是有2^(h-1)个叶子节点
3)非叶子节点数量:有2^(h-1) - 1个非叶子节点
4)高度与节点关系:高度h = log₂(n+1),其中n是节点总数
3、代码示例
from collections import deque
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class JudgeBinaryTree:
def __init__(self, root=None):
self.root = root # 允许初始化时传入根节点
def is_full_binary_tree(self):
def _is_full(node):
if node is None:
return True
if node.left is None and node.right is None:
return True
if node.left is not None and node.right is not None:
return _is_full(node.left) and _is_full(node.right)
return False
return _is_full(self.root)
def is_complete_binary_tree(self):
if self.root is None:
return True
queue = deque([self.root])
has_missing = False
while queue:
node = queue.popleft()
if node.left is None and node.right is not None:
return False # 不能只有右子节点
if node.left is not None:
if has_missing:
return False # 之前有空缺,但还有左子节点
queue.append(node.left)
else:
has_missing = True # 左子节点缺失
if node.right is not None:
if has_missing:
return False # 之前有空缺,但还有右子节点
queue.append(node.right)
else:
has_missing = True # 右子节点缺失
return True
def classify_binary_tree(self):
if self.root is None:
return "空树(既是完全二叉树也是满二叉树)"
is_full = self.is_full_binary_tree()
is_complete = self.is_complete_binary_tree()
if is_full and is_complete:
return "满二叉树(同时也是完全二叉树)"
elif is_full:
return "满二叉树(但非完全二叉树)"
elif is_complete:
return "完全二叉树(但非满二叉树)"
else:
return "非完全二叉树且非满二叉树"
if __name__ == '__main__':
# 1. 满二叉树(同时也是完全二叉树)
root1 = TreeNode(1)
root1.left = TreeNode(2, TreeNode(4), TreeNode(5))
root1.right = TreeNode(3, TreeNode(6), TreeNode(7))
judge1 = JudgeBinaryTree(root1)
print("树1:", judge1.classify_binary_tree())
# 2. 完全二叉树(但非满二叉树)
root2 = TreeNode(1)
root2.left = TreeNode(2, TreeNode(4), TreeNode(5))
root2.right = TreeNode(3, TreeNode(6), None)
judge2 = JudgeBinaryTree(root2)
print("树2:", judge2.classify_binary_tree())
# 3. 非完全二叉树(且非满二叉树)
root3 = TreeNode(1)
root3.left = TreeNode(2, TreeNode(4), None)
root3.right = TreeNode(3, TreeNode(6), TreeNode(7))
judge3 = JudgeBinaryTree(root3)
print("树3:", judge3.classify_binary_tree())
# 4. 非完全二叉树且非满二叉树
root4 = TreeNode(1)
root4.left = TreeNode(2, TreeNode(4), None)
root4.right = TreeNode(3, None, TreeNode(7))
judge4 = JudgeBinaryTree(root4)
print("树4:", judge4.classify_binary_tree())
# 5. 空树
judge5 = JudgeBinaryTree(None)
print("树5:", judge5.classify_binary_tree())
4、 完全二叉树对比满二叉树
满二叉树:所有非叶子节点都有两个子节点,所有叶子节点都在同一层,所有层的节点数均达到最大值若深度为k,则节点数为 2^k - 1。
完全二叉树是满二叉树的子集,最后一层可以不满,但必须从左到右排列。
5、 应用
1) 表达式树: 用于表示数学表达式
2) 文件系统索引: 某些文件系统使用类似满二叉树的结构进行索引
(三) 二叉搜索树(BST)
左子树所有节点值 < 根节点值 < 右子树所有节点值。
1、 性质
1)每个节点都有一个键(key)和最多两个子节点(左子节点和右子节点)
2)对于任意节点:
左子树所有节点的值小于当前节点的值。
右子树所有节点的值大于当前节点的值。
3)左右子树也必须是二叉搜索树(递归定义)
2、 基本操作
1) 查找(Search)
步骤:
从根节点开始,比较目标值与当前节点值。
若目标值较小,进入左子树;否则进入右子树。
重复直到找到目标或到达空节点。
时间复杂度:平均 O(logn),最坏 O(n)(树退化为链表时)。
2) 插入(Insert)
步骤:
从根节点开始,比较插入值与当前节点值。
若插入值较小,进入左子树;否则进入右子树。
重复直到到达空节点,插入新节点。
时间复杂度:同查找,平均 O(logn),最坏 O(n)。
3) 删除(Delete)
三种情况:
叶子节点:直接删除。
单子树节点:用子树替代。
双子树节点:
找到右子树的最小节点(或左子树的最大节点)。
用该节点值替换当前节点,并删除原最小节点。
时间复杂度:同查找。
3、 遍历方式
1) 先序遍历(Pre-order Traversal)
访问顺序:根节点 → 左子树 → 右子树
访问根节点,适合用于复制树结构,可用于前缀表达式(波兰表达式)
前序遍历结果:8, 3, 1, 6, 4, 7, 10, 14, 13
2) 中序遍历(In-order Traversal)
访问顺序:左子树 → 根节点 → 右子树
对BST进行中序遍历会得到一个升序排列的节点序列,常用于获取有序数据
中序遍历结果:1, 3, 4, 6, 7, 8, 10, 13, 14
3) 后序遍历(Post-order Traversal)
访问顺序:左子树 → 右子树 → 根节点
最后访问根节点,适合用于删除树,可用于后缀表达式(逆波兰表达式)
后序遍历结果:1, 4, 7, 6, 3, 13, 14, 10, 8
4、代码示例
class TreeNode:
def __init__(self, val=0, left=None, right=None):
# 节点值
self.val = val
# 左子节点
self.left = left
# 右子节点
self.right = right
class BinarySearchTree:
def __init__(self, root=None):
self.root = root
def search(self, target):
return self._search(self.root, target)
def _search(self, node, target):
if not node:
# 未找到
return None
if target == node.val:
# 找到目标节点
return node
elif target < node.val:
# 在左子树查找
return self._search(node.left, target)
else:
# 在右子树查找
return self._search(node.right, target)
def insert(self, val):
self.root = self._insert(self.root, val)
return self.root
def _insert(self, node, val):
if not node:
# 树为空,直接插入
return TreeNode(val)
if val < node.val:
# 插入左子树
node.left = self._insert(node.left, val)
else:
# 插入右子树
node.right = self._insert(node.right, val)
return node
def delete(self, val):
self.root = self._delete(self.root, val)
return self.root
def _delete(self, node, val):
if not node:
return None
if val < node.val:
# 在左子树删除
node.left = self._delete(node.left, val)
elif val > node.val:
# 在右子树删除
node.right = self._delete(node.right, val)
else:
# 找到要删除的节点
# 只有右子节点或无子节点
if not node.left:
return node.right
# 只有左子节点
elif not node.right:
return node.left
else:
# 有两个子节点,找到右子树的最小值
min_node = self._find_min(node.right)
# 替换值
node.val = min_node.val
# 删除右子树的最小值节点
node.right = self._delete(node.right, min_node.val)
return node
def _find_min(self, node):
while node.left:
node = node.left
return node
# 先序遍历(根 → 左 → 右)
def preorder_traversal(self, result=None):
if result is None:
result = []
self._preorder(self.root, result)
return result
def _preorder(self, node, result):
if node:
# 访问根节点
result.append(node.val)
# 遍历左子树
self._preorder(node.left, result)
# 遍历右子树
self._preorder(node.right, result)
# 中序遍历(左 → 根 → 右)
def inorder_traversal(self, result=None):
if result is None:
result = []
self._inorder(self.root, result)
return result
def _inorder(self, node, result):
if node:
# 遍历左子树
self._inorder(node.left, result)
# 访问根节点
result.append(node.val)
# 遍历右子树
self._inorder(node.right, result)
# 后序遍历(左 → 右 → 根)
def postorder_traversal(self, result=None):
if result is None:
result = []
self._postorder(self.root, result)
return result
def _postorder(self, node, result):
if node:
# 遍历左子树
self._postorder(node.left, result)
# 遍历右子树
self._postorder(node.right, result)
# 访问根节点
result.append(node.val)
if __name__ == "__main__":
# 构建 BST
tree = BinarySearchTree()
values = [8, 3, 10, 1, 6, 14, 4, 7, 13]
for val in values:
tree.insert(val)
# 查找节点
target = 6
found_node = tree.search(target)
print(f"查找 {target}: {'找到' if found_node else '未找到'}")
# 遍历
print("先序遍历:", tree.preorder_traversal())
print("中序遍历:", tree.inorder_traversal())
print("后序遍历:", tree.postorder_traversal())
# 删除节点
tree.delete(6)
print("删除 6 后的中序遍历:", tree.inorder_traversal())
5、 优缺点
优点 : 高效查找、插入、删除(平均 O(logn)),中序遍历直接得到有序序列
缺点 : 最坏情况下退化为链表(如插入有序序列),导致操作效率降至 O(n)
(四) 平衡二叉树(AVL树)
AVL树是一种自平衡的二叉搜索树,得名于其发明者Adelson-Velsky和Landis。在AVL树中,任何节点的两个子树的高度差最多为1,这种平衡性质保证了树的查找、插入和删除操作的时间复杂度都能保持在O(log n)。
1、特性:
1)平衡因子:每个节点的平衡因子是其左子树高度减去右子树高度
平衡因子 ∈ {-1, 0, 1}(平衡状态)
若绝对值 > 1,则需要进行平衡操作
2)高度平衡:对于树中的每个节点,其左右子树的高度差不超过1
2、 旋转操作
当插入或删除节点导致树不平衡时,AVL树通过旋转操作来恢复平衡
1. 左旋(LL旋转)
场景:右子树比左子树高,且新节点插入在右子树的右子树
2. 右旋(RR旋转)
场景:左子树比右子树高,且新节点插入在左子树的左子树
3. 左右旋(LR旋转)
场景:左子树比右子树高,且新节点插入在左子树的右子树
4. 右左旋(RL旋转)
场景:右子树比左子树高,且新节点插入在右子树的左子树
3、 步骤 示例
插入
1)按照BST规则插入新节点
2)从插入点向上回溯,更新每个祖先节点的高度
3)检查每个节点的平衡因子
4)如果发现不平衡,执行相应的旋转操作
删除
1)按照BST规则删除节点
2)从删除点向上回溯,更新每个祖先节点的高度
3)检查每个节点的平衡因子
4)如果发现不平衡,执行相应的旋转操作(可能需要多次旋转)
| 旋转类型 | 触发条件 (平衡因子) | 插入位置 | 操作顺序 |
|---|---|---|---|
| 左旋 (LL) | A.bf=-2, B.bf=-1 | 右子树的右子树 | 直接左旋 |
| 右旋 (RR) | A.bf=+2, B.bf=+1 | 左子树的左子树 | 直接右旋 |
| 左右旋 (LR) | A.bf=+2, B.bf=-1 | 左子树的右子树 | 先左旋后右旋 |
| 右左旋 (RL) | A.bf=-2, B.bf=+1 | 右子树的左子树 | 先右旋后左旋 |
4、代码示例
class TreeNode:
def __init__(self, key):
self.key = key
self.left = None
self.right = None
# 节点高度,初始为1(叶子节点)
self.height = 1
class AVLTree:
def __init__(self):
self.root = None
def insert(self, key):
self.root = self._insert(self.root, key)
def _insert(self, node, key):
# 1. 执行标准BST插入
if not node:
return TreeNode(key)
elif key < node.key:
node.left = self._insert(node.left, key)
else:
node.right = self._insert(node.right, key)
# 2. 更新节点高度
node.height = 1 + max(self._get_height(node.left),
self._get_height(node.right))
# 3. 获取平衡因子
balance = self._get_balance(node)
# 4. 如果不平衡,进行旋转
# 左左情况
if balance > 1 and key < node.left.key:
return self._right_rotate(node)
# 右右情况
if balance < -1 and key > node.right.key:
return self._left_rotate(node)
# 左右情况
if balance > 1 and key > node.left.key:
node.left = self._left_rotate(node.left)
return self._right_rotate(node)
# 右左情况
if balance < -1 and key < node.right.key:
node.right = self._right_rotate(node.right)
return self._left_rotate(node)
return node
def delete(self, key):
self.root = self._delete(self.root, key)
def _delete(self, node, key):
# 1. 执行标准BST删除
if not node:
return node
elif key < node.key:
node.left = self._delete(node.left, key)
elif key > node.key:
node.right = self._delete(node.right, key)
else:
# 节点有一个或没有子节点
if node.left is None:
temp = node.right
return temp
elif node.right is None:
temp = node.left
return temp
# 节点有两个子节点:获取中序后继(右子树的最小值)
temp = self._get_min_value_node(node.right)
node.key = temp.key
node.right = self._delete(node.right, temp.key)
# 如果树只有一个节点,直接返回
if node is None:
return node
# 2. 更新节点高度
node.height = 1 + max(self._get_height(node.left),
self._get_height(node.right))
# 3. 获取平衡因子
balance = self._get_balance(node)
# 4. 如果不平衡,进行旋转
# 左左情况
if balance > 1 and self._get_balance(node.left) >= 0:
return self._right_rotate(node)
# 右右情况
if balance < -1 and self._get_balance(node.right) <= 0:
return self._left_rotate(node)
# 左右情况
if balance > 1 and self._get_balance(node.left) < 0:
node.left = self._left_rotate(node.left)
return self._right_rotate(node)
# 右左情况
if balance < -1 and self._get_balance(node.right) > 0:
node.right = self._right_rotate(node.right)
return self._left_rotate(node)
return node
def _left_rotate(self, z):
"""
左旋
z
y
x
"""
y = z.right
T2 = y.left
# 执行旋转
y.left = z
z.right = T2
# 更新高度
z.height = 1 + max(self._get_height(z.left),
self._get_height(z.right))
y.height = 1 + max(self._get_height(y.left),
self._get_height(y.right))
# 返回新的根节点
return y
def _right_rotate(self, z):
"""
右旋
z
y
x
"""
y = z.left
T3 = y.right
# 执行旋转
y.right = z
z.left = T3
# 更新高度
z.height = 1 + max(self._get_height(z.left),
self._get_height(z.right))
y.height = 1 + max(self._get_height(y.left),
self._get_height(y.right))
# 返回新的根节点
return y
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 _get_min_value_node(self, node):
if node is None or node.left is None:
return node
return self._get_min_value_node(node.left)
def pre_order(self, node):
if not node:
return
print("{0} ".format(node.key), end="")
self.pre_order(node.left)
self.pre_order(node.right)
def print_tree(self):
if not self.root:
print("空树")
return
self._print_tree([self.root], 1, self._get_height(self.root))
def _print_tree(self, nodes, level, max_level):
"""
画树
"""
if not nodes or all(node is None for node in nodes):
return
floor = max_level - level
edge_lines = 1 # 每层只需要一组连接线
first_spaces = 2 ** floor - 1
between_spaces = 2 ** (floor + 1) - 1
# 打印空格
print(" " * first_spaces, end="")
# 打印当前层的所有节点
new_nodes = []
for node in nodes:
if node:
print(node.key, end="")
new_nodes.append(node.left)
new_nodes.append(node.right)
else:
print(" ", end="")
new_nodes.append(None)
new_nodes.append(None)
print(" " * between_spaces, end="")
print()
# 如果不是最后一层,打印连接线
if level < max_level:
for i in range(1, edge_lines + 1):
for node in nodes:
print(" " * (first_spaces - i), end="")
if node and node.left:
print("/", end="")
else:
print(" ", end="")
print(" " * (2 * i - 1), end="")
if node and node.right:
print("\", end="")
else:
print(" ", end="")
print(" " * (between_spaces - 2 * i + 1), end="")
print()
# 递归打印下一层
self._print_tree(new_nodes, level + 1, max_level)
if __name__ == "__main__":
avl = AVLTree()
keys = [10, 20, 30, 40, 50, 25]
print("插入顺序:", keys)
for key in keys:
avl.insert(key)
print("\n树结构:")
avl.print_tree()
print("\n删除40和10")
avl.delete(40)
avl.delete(10)
print("\n删除后的树结构:")
avl.print_tree()
5、 优缺点
1) 优点
严格的平衡保证,查找效率高;
插入、删除、查找的时间复杂度均为O(log n);
适合查找密集型应用
2) 缺点
插入和删除操作可能需要多次旋转,开销较大;
相比红黑树,维护平衡的成本更高;
需要存储平衡因子或高度信息,额外空间开销
(五) 红黑树(Red-Black Tree)
通过颜色标记和旋转操作保持近似平衡的二叉搜索树
1、特性
1)节点颜色: 每个节点是红色或黑色。
2)根节点: 必须为黑色。
3)叶子节点(NIL节点): 所有叶子节点(NIL,表示空节点)视为黑色。
4)红色限制: 红色节点的子节点必须为黑色(即不能有连续的红色节点)。
5)黑高一致: 从任意节点到其所有叶子节点的路径上,黑色节点的数量(黑高)相同。
关键特性: 从根到最远叶子节点的路径不会超过从根到最近叶子节点路径的两倍
2、 旋转操作
3、 插入操作
插入新节点时总是红色(避免违反性质5)
1)按照二叉查找树的规则插入新节点(红色)
2)如果父节点是黑色,无需调整
3)如果父节点是红色:
如果叔叔节点是红色:重新着色(父、叔变黑,祖父变红),然后向上递归处理
如果叔叔节点是黑色:通过旋转和重新着色调整
4、 删除操作
1)执行标准BST删除
2)如果删除的是红色节点,树仍然保持平衡
3)如果删除的是黑色节点,需要通过旋转和重新着色来恢复平衡
5、代码示例
class Node:
"""红黑树的节点类"""
def __init__(self, key, color="R"):
"""
初始化节点
:param key: 节点的键值
:param color: 节点颜色,默认为红色("R"),因为新插入的节点总是红色
"""
# 节点存储的键值
self.key = key
# 左子节点指针
self.left = None
# 右子节点指针
self.right = None
# 父节点指针
self.parent = None
# 节点颜色,"R"表示红色,"B"表示黑色
self.color = color
# 节点高度,初始为1(叶子节点)
self.height = 1
class RedBlackTree:
"""红黑树实现类"""
def __init__(self):
"""
初始化红黑树
使用哨兵节点NIL表示空节点,简化边界条件处理
"""
self.NIL = Node(None, "B") # 哨兵节点,颜色为黑色
self.NIL.height = 0 # NIL节点的高度应为0
self.root = self.NIL # 根节点初始化为NIL
def update_height(self, node):
"""更新节点的高度"""
if node == self.NIL:
return
node.height = max(self.get_height(node.left), self.get_height(node.right)) + 1
def get_height(self, node):
"""获取节点的高度,处理NIL节点的情况"""
if node == self.NIL:
return 0
return node.height
def _get_height(self, node):
"""用于打印树的方法,使用get_height来处理NIL节点"""
return self.get_height(node)
def left_rotate(self, x):
r"""
左旋操作
x 旋转的支点节点
旋转前:
x y
/ \ / \
A y => x C
/ \ / \
B C A B
"""
y = x.right # 设置y为x的右孩子
x.right = y.left # 将y的左子树变为x的右子树
# 如果y的左子树不为空,更新其父节点
if y.left != self.NIL:
y.left.parent = x
# 更新y的父节点为x的父节点
y.parent = x.parent
# 如果x是根节点,更新根节点为y
if x.parent == self.NIL:
self.root = y
# 如果x是其父节点的左孩子,更新父节点的左孩子为y
elif x == x.parent.left:
x.parent.left = y
# 否则更新父节点的右孩子为y
else:
x.parent.right = y
# 将x设为y的左孩子
y.left = x
x.parent = y
# 更新高度(先更新x再更新y,因为y现在是x的父节点)
self.update_height(x)
self.update_height(y)
def right_rotate(self, y):
r"""
右旋操作
y 旋转的支点节点
旋转前:
y x
/ \ / \
x C => A y
/ \ / \
A B B C
"""
x = y.left # 设置x为y的左孩子
y.left = x.right # 将x的右子树变为y的左子树
# 如果x的右子树不为空,更新其父节点
if x.right != self.NIL:
x.right.parent = y
# 更新x的父节点为y的父节点
x.parent = y.parent
# 如果y是根节点,更新根节点为x
if y.parent == self.NIL:
self.root = x
# 如果y是其父节点的右孩子,更新父节点的右孩子为x
elif y == y.parent.right:
y.parent.right = x
# 否则更新父节点的左孩子为x
else:
y.parent.left = x
# 将y设为x的右孩子
x.right = y
y.parent = x
# 更新高度(先更新y再更新x,因为x现在是y的父节点)
self.update_height(y)
self.update_height(x)
def insert(self, key):
r"""
插入节点
key 要插入的键值
插入后调用insert_fixup修复可能违反的红黑树性质
情况1:新节点是根节点
操作:直接变黑
情况2:父节点是黑色
操作:无需调整
黑X
/ \
Z 黑U (Z为新插入的红节点)
情况3:父节点是红色
"""
# 创建新节点
z = Node(key)
# 初始化左子树为NIL
z.left = self.NIL
# 初始化右子树为NIL
z.right = self.NIL
# 用于记录父节点,初始为NIL
y = self.NIL
# 从根节点开始查找插入位置
x = self.root
# 查找合适的插入位置
while x != self.NIL:
y = x
if z.key < x.key:
x = x.left
else:
x = x.right
# 设置新节点的父节点
z.parent = y
# 根据父节点情况插入新节点
if y == self.NIL:
# 树为空的情况
self.root = z
elif z.key < y.key:
# 插入到左子树
y.left = z
else:
# 插入到右子树
y.right = z
# 新插入的节点总是红色,可能需要修复红黑树性质
self.insert_fixup(z)
# 从插入节点向上更新高度
self.update_height(z)
current = z.parent
while current != self.NIL:
old_height = current.height
self.update_height(current)
if current.height == old_height:
# 如果高度没有变化,可以提前终止
break
current = current.parent
def insert_fixup(self, z):
r"""
插入修复操作,维护红黑树的性质
z 新插入的节点
可能违反的性质:
1. 根节点必须是黑色
2. 红色节点不能有红色子节点(不能有连续红色节点)
情况1:叔叔节点是红色
调整步骤:
父节点和叔叔节点变黑;祖父节点变红;递归检查祖父节点
黑G 红G
/ \ / \
红P 红Y => 黑P 黑Y
/ /
红Z 红Z
情况2:叔叔节点是黑色/不存在,且新节点是右孩子
调整步骤:
对父节点进行一次旋转(左旋或右旋,方向与插入方向相反);转换为情况3.3
旋转前: 对P左旋后:
黑G 黑G
/ \ / \
红P 黑U => 红Z 黑U
\ /
红Z 红P
情况3:叔叔节点是黑色/不存在,且新节点是左孩子
调整步骤:
对祖父节点进行一次旋转(左旋或右旋,方向与插入方向相同);交换祖父和父节点的颜色
调整前: 对G右旋并变色后:
黑G 黑P
/ \ / \
红P 黑U => 红Z 红G
/ /
红Z 黑U
"""
while z.parent.color == "R":
# 当父节点是红色时需要修复(因为根节点是黑色,所以z.parent不会是NIL)
if z.parent == z.parent.parent.left:
# 父节点是祖父节点的左孩子
y = z.parent.parent.right # 叔叔节点
# 情况1:叔叔节点是红色
if y.color == "R":
# 调整颜色:父节点和叔叔节点变黑,祖父节点变红
z.parent.color = "B"
y.color = "B"
z.parent.parent.color = "R"
# 继续检查祖父节点
z = z.parent.parent
else:
# 情况2:叔叔是黑色,且当前节点是右孩子
if z == z.parent.right:
z = z.parent
# 左旋转换为情况3
self.left_rotate(z)
# 情况3:叔叔是黑色,且当前节点是左孩子
z.parent.color = "B"
z.parent.parent.color = "R"
self.right_rotate(z.parent.parent)
else:
# 对称的情况:父节点是祖父节点的右孩子
y = z.parent.parent.left # 叔叔节点
# 情况1:叔叔节点是红色
if y.color == "R":
z.parent.color = "B"
y.color = "B"
z.parent.parent.color = "R"
z = z.parent.parent
else:
# 情况2:叔叔是黑色,且当前节点是左孩子
if z == z.parent.left:
z = z.parent
# 右旋转换为情况3
self.right_rotate(z)
# 情况3:叔叔是黑色,且当前节点是右孩子
z.parent.color = "B"
z.parent.parent.color = "R"
self.left_rotate(z.parent.parent)
# 确保根节点是黑色
self.root.color = "B"
def delete(self, key):
"""
删除指定键值的节点
:param key: 要删除的键值
"""
# 查找要删除的节点
z = self.root
while z != self.NIL and z.key != key:
if key < z.key:
z = z.left
else:
z = z.right
if z == self.NIL:
# 没找到要删除的节点
return
# 记录要删除的节点及其原始颜色
y = z
y_original_color = y.color
# 分情况处理删除
if z.left == self.NIL:
# 只有右孩子或无孩子
x = z.right
self.transplant(z, z.right)
elif z.right == self.NIL:
# 只有左孩子
x = z.left
self.transplant(z, z.left)
else:
# 有两个孩子
# 找到后继节点(右子树的最小节点)
y = self.minimum(z.right)
y_original_color = y.color
x = y.right
# 如果y不是z的右孩子
if y.parent != z:
self.transplant(y, y.right)
y.right = z.right
y.right.parent = y
# 用y替换z
self.transplant(z, y)
y.left = z.left
y.left.parent = y
# 保持颜色一致
y.color = z.color
# 保持高度一致
y.height = z.height
# 从x的父节点向上更新高度
if y_original_color == "B":
self.delete_fixup(x)
# 更新高度
current = x.parent if x != self.NIL else self.root
while current != self.NIL:
old_height = current.height
self.update_height(current)
if current.height == old_height:
# 如果高度没有变化,可以提前终止
break
current = current.parent
def delete_fixup(self, x):
"""
删除修复操作,维护红黑树的性质
:param x: 实际删除节点的替代节点
"""
# 当x不是根节点且是黑色时需要修复
while x != self.root and x.color == "B":
# x是左孩子
if x == x.parent.left:
# 兄弟节点
w = x.parent.right
# 情况1:兄弟节点是红色
if w.color == "R":
w.color = "B"
x.parent.color = "R"
self.left_rotate(x.parent)
w = x.parent.right
# 情况2:兄弟节点是黑色,且兄弟的两个孩子都是黑色
if w.left.color == "B" and w.right.color == "B":
w.color = "R"
x = x.parent
else:
# 情况3:兄弟节点是黑色,兄弟的左孩子是红色,右孩子是黑色
if w.right.color == "B":
w.left.color = "B"
w.color = "R"
self.right_rotate(w)
w = x.parent.right
# 情况4:兄弟节点是黑色,兄弟的右孩子是红色
w.color = x.parent.color
x.parent.color = "B"
w.right.color = "B"
self.left_rotate(x.parent)
# 退出循环
x = self.root
else:
# 对称的情况:x是右孩子
w = x.parent.left # 兄弟节点
# 情况1:兄弟节点是红色
if w.color == "R":
w.color = "B"
x.parent.color = "R"
self.right_rotate(x.parent)
w = x.parent.left
# 情况2:兄弟节点是黑色,且兄弟的两个孩子都是黑色
if w.right.color == "B" and w.left.color == "B":
w.color = "R"
x = x.parent
else:
# 情况3:兄弟节点是黑色,兄弟的右孩子是红色,左孩子是黑色
if w.left.color == "B":
w.right.color = "B"
w.color = "R"
self.left_rotate(w)
w = x.parent.left
# 情况4:兄弟节点是黑色,兄弟的左孩子是红色
w.color = x.parent.color
x.parent.color = "B"
w.left.color = "B"
self.right_rotate(x.parent)
# 退出循环
x = self.root
# 确保x是黑色
x.color = "B"
def transplant(self, u, v):
"""
用v替换u的位置
:param u: 要被替换的节点
:param v: 替换后的节点
"""
if u.parent == self.NIL:
# u是根节点
self.root = v
elif u == u.parent.left:
# u是左孩子
u.parent.left = v
else:
# u是右孩子
u.parent.right = v
# 更新v的父节点(即使v是NIL也要更新)
v.parent = u.parent
def minimum(self, x):
"""
返回以x为根的子树中的最小节点
:param x: 子树根节点
:return: 最小节点
"""
while x.left != self.NIL:
x = x.left
return x
def inorder(self, node):
"""
中序遍历红黑树
:param node: 当前遍历的节点
"""
if node != self.NIL:
self.inorder(node.left)
print(f"{node.key}({node.color})", end=" ")
self.inorder(node.right)
def print_tree(self):
# 检查树是否为空
if self.root == self.NIL:
print("空树")
return
# 调用_print_tree方法,从根节点开始打印,初始层级为1,并传入树的总高度
self._print_tree([self.root], 1, self._get_height(self.root))
def _print_tree(self, nodes, level, max_level):
"""
递归打印红黑树结构,并显示节点颜色
nodes: 当前层的节点列表
level: 当前层级
max_level: 树的总高度
"""
# 如果节点列表为空或全是NIL节点,则返回
if level > max_level or not nodes or all(node == self.NIL for node in nodes):
return
# 计算当前层与底层的距离
floor = max_level - level
# 计算首节点前的空格数(根据二叉树的性质计算)
first_spaces = int(2 ** floor - 1)
# 计算节点之间的空格数
between_spaces = int(2 ** (floor + 1) - 2)
# 每层只需要一组连接线
edge_lines = 1
# 打印首节点前的空格
print(" " * first_spaces, end="")
# 打印当前层的所有节点,并收集下一层的节点
new_nodes = []
for node in nodes:
left = node.left if node != self.NIL else self.NIL
right = node.right if node != self.NIL else self.NIL
new_nodes.append(left)
new_nodes.append(right)
if node != self.NIL:
print(f"{node.key}({node.color})", end="")
else:
# NIL占位符
if level == max_level:
print("", end="")
else:
print(" ", end="")
# 减少占位
print(" " * (between_spaces - 3), end="")
print()
# 如果不是最后一层,打印连接线(/和\)
if level < max_level:
for i in range(1, edge_lines + 1):
for node in nodes:
print(" " * (first_spaces - i), end="")
if node != self.NIL and node.left != self.NIL:
# 左斜杠表示左子节点
print("/", end="")
else:
print("", end="")
print(" " * (2 * i - 1), end="")
if node != self.NIL and node.right != self.NIL:
# 右斜杠表示右子节点
print("\", end="")
else:
print("", end="")
print(" " * (between_spaces - 2 * i + 1), end="")
# 换行
print()
# 递归打印下一层节点
self._print_tree(new_nodes, level + 1, max_level)
if __name__ == "__main__":
rbt = RedBlackTree()
keys = [10, 5, 15, 3, 1, 7, 12, 9]
print("插入顺序:", keys)
for key in keys:
rbt.insert(key)
rbt.print_tree()
print("\n中序遍历结果:")
rbt.inorder(rbt.root)
print("\n\n删除18后:")
rbt.delete(18)
rbt.print_tree()
rbt.inorder(rbt.root)
| 特性 | 红黑树 | AVL树 |
|---|---|---|
| 平衡强度 | 近似平衡、黑高一致(最长路径≤2倍最短路径) | 严格平衡(左右子树高度差≤1) |
| 插入/删除 | 需要较少旋转O(log n) | 可能频繁旋转O(log n) |
| 查找效率 | 略低于AVL(因平衡较松) | 更高(严格平衡) |
| 适用场景 | 频繁插入删除的场景(如map、set) | 频繁查找(如数据库索引) |
三、 多叉树(n-ary Tree)
每个节点可以有任意数量(n ≥ 0)的子节点
(一) 三叉树(Ternary Tree)
三叉树是一种每个节点最多有三个子节点的树结构,子节点通常称为左子节点(Left)、中间子节点(Middle)和右子节点(Right)。与二叉树类似,但每个节点多了一个分支,增加了存储和搜索的灵活性。
1、 特点
1)每个节点最多有三个子节点
2)子节点通常被称为左子节点、中子节点和右子节点
3)可以是有序的(子节点有特定顺序)或无序的
4)可以是平衡的或不平衡的
2、 操作
2.1 插入
根据规则(如数值大小、哈希值等)将新节点插入到左、中或右子树
有序三叉树:根据值的大小决定插入位置
无序三叉树:通常按空缺位置插入
不同插入策略比较
1) 广度优先填充
优点:保持树相对平衡
缺点:可能导致搜索效率不高
2) 有序插入
根据值大小决定插入左、中、右子节点
优点:搜索效率高
缺点:可能不平衡
3) 随机选择子节点插入
随机选择可用的子节点
优点:实现简单
缺点:可能导致树不平衡
2.2 遍历
支持前序、中序、后序和层序遍历,顺序根据子节点定义调整
1) 深度优先遍历 (DFS)
前序遍历 (根→左→中→右)
顺序:根节点 → 左子树 → 中子节点 → 右子树
路径:A → B → E → F → G → C → D → H → I → J
中序遍历 (左→根→中→右)
顺序:左子树 → 根节点 → 中子节点 → 右子树
路径:E → B → F → G → A → C → H → D → I → J
后序遍历 (左→中→右→根)
顺序:左子树 → 中子节点 → 右子树 → 根节点
路径:E → F → G → B → C → H → I → J → D → A
2) 广度优先遍历 (BFS /层序遍历 )
顺序:按层级从上到下、从左到右访问
路径:A → B → C → D → E → F → G → H → I → J
2.3 搜索
从根节点开始,根据比较结果递归搜索左、中或右子树
搜索可以深度优先(前序、中序、后序)或广度优先进行
2.4 删除
找到目标节点后,根据其子节点情况调整树结构(类似二叉树删除)
删除步骤
1)查找要删除的节点
2)判断节点类型:
叶子节点:直接删除
有一个子节点:用子节点替代
有多个子节点:找前驱/后继替代
3)整父节点指针
4)保持树的结构完整性
特殊情况
1)删除根节点:需要选择新的根节点,通常选择中序前驱或后继
2)所有子节点都存在:可以选择左子树最大或右子树最小替代
3)重复节点处理:需要定义如何处理重复值(全部删除或保留部分)
3、代码示例
class TernaryTreeNode:
def __init__(self, value):
"""
初始化三叉树节点
:param value: 节点存储的值
"""
# 节点值
self.value = value
# 左子节点(存储小于当前节点的值)
self.left = None
# 中子节点(存储等于当前节点的值,用于处理重复值)
self.middle = None
# 右子节点(存储大于当前节点的值)
self.right = None
class TernaryTree:
def __init__(self):
"""初始化空的三叉树"""
# 根节点初始化为None
self.root = None
def insert(self, value):
"""
公共插入方法
:param value: 要插入的值
"""
if self.root is None:
# 如果树为空,创建根节点
self.root = TernaryTreeNode(value)
else:
# 否则递归插入
self._insert_recursive(self.root, value)
def _insert_recursive(self, node, value):
"""
递归插入辅助方法
:param node: 当前节点
:param value: 要插入的值
"""
if value < node.value:
# 如果值小于当前节点,尝试插入左子树
if node.left is None:
# 左子树为空,直接创建新节点
node.left = TernaryTreeNode(value)
else:
# 否则递归插入左子树
self._insert_recursive(node.left, value)
elif value == node.value:
# 如果值等于当前节点,尝试插入中子树(处理重复值)
if node.middle is None:
# 中子树为空,直接创建新节点
node.middle = TernaryTreeNode(value)
else:
# 否则递归插入中子树
self._insert_recursive(node.middle, value)
else:
# 如果值大于当前节点,尝试插入右子树
if node.right is None:
# 右子树为空,直接创建新节点
node.right = TernaryTreeNode(value)
else:
# 否则递归插入右子树
self._insert_recursive(node.right, value)
def search(self, value):
"""
公共搜索方法
:param value: 要搜索的值
:return: 是否存在(True/False)
"""
return self._search_recursive(self.root, value)
def _search_recursive(self, node, value):
"""
递归搜索辅助方法
:param node: 当前节点
:param value: 要搜索的值
:return: 是否存在(True/False)
"""
if node is None:
# 到达叶子节点仍未找到,返回False
return False
if value == node.value:
# 找到值,返回True
return True
elif value < node.value:
# 值小于当前节点,搜索左子树
return self._search_recursive(node.left, value)
else:
# 值大于当前节点,搜索右子树
return self._search_recursive(node.right, value)
def inorder_traversal(self):
"""
:return: 遍历结果列表
"""
result = []
self._inorder_recursive(self.root, result)
return result
def _inorder_recursive(self, node, result):
"""
递归中序遍历辅助方法
:param node: 当前节点
:param result: 存储结果的列表
"""
if node:
# 递归遍历左子树
self._inorder_recursive(node.left, result)
# 访问当前节点
result.append(node.value)
# 递归遍历中子树(处理重复值)
self._inorder_recursive(node.middle, result)
# 递归遍历右子树
self._inorder_recursive(node.right, result)
def preorder_traversal(self):
"""
:return: 遍历结果列表
"""
result = []
self._preorder_recursive(self.root, result)
return result
def _preorder_recursive(self, node, result):
"""
递归前序遍历辅助方法
:param node: 当前节点
:param result: 存储结果的列表
"""
if node:
# 访问当前节点
result.append(node.value)
# 递归遍历左子树
self._preorder_recursive(node.left, result)
# 递归遍历中子树(处理重复值)
self._preorder_recursive(node.middle, result)
# 递归遍历右子树
self._preorder_recursive(node.right, result)
def postorder_traversal(self):
"""
:return: 遍历结果列表
"""
result = []
self._postorder_recursive(self.root, result)
return result
def _postorder_recursive(self, node, result):
"""
递归后序遍历辅助方法
:param node: 当前节点
:param result: 存储结果的列表
"""
if node:
# 递归遍历左子树
self._postorder_recursive(node.left, result)
# 递归遍历中子树(处理重复值)
self._postorder_recursive(node.middle, result)
# 递归遍历右子树
self._postorder_recursive(node.right, result)
# 访问当前节点
result.append(node.value)
def delete(self, value):
"""
公共删除方法
:param value: 要删除的值
"""
self.root = self._delete_recursive(self.root, value)
def _delete_recursive(self, node, value):
"""
递归删除辅助方法
:param node: 当前节点
:param value: 要删除的值
:return: 处理后的子树根节点
"""
if node is None:
# 未找到要删除的节点,返回None
return node
if value < node.value:
# 要删除的值在左子树
node.left = self._delete_recursive(node.left, value)
elif value > node.value:
# 要删除的值在右子树
node.right = self._delete_recursive(node.right, value)
else:
# 找到要删除的节点
if node.middle:
# 如果有中子节点(处理重复值的情况)
node.middle = self._delete_recursive(node.middle, value)
return node
# 处理没有中子节点或已处理完中子节点的情况
if node.left is None:
# 只有右子节点或没有子节点,返回右子节点
return node.right
elif node.right is None:
# 只有左子节点,返回左子节点
return node.left
# 有两个子节点的情况
# 获取右子树的最小值
node.value = self._min_value(node.right)
# 删除右子树中的最小值节点
node.right = self._delete_recursive(node.right, node.value)
return node
def _min_value(self, node):
"""
查找子树中的最小值
:param node: 子树根节点
:return: 最小值
"""
current = node
# 一直向左查找直到叶子节点
while current.left is not None:
current = current.left
return current.value
if __name__ == "__main__":
# 创建三叉树实例
tree = TernaryTree()
# 插入测试数据(包含重复值5)
values = [5, 3, 7, 2, 4, 6, 8, 5, 5]
for val in values:
tree.insert(val)
# 遍历
print("\n中序遍历:", tree.inorder_traversal())
print("前序遍历:", tree.preorder_traversal())
print("后序遍历:", tree.postorder_traversal())
# 搜索
print("\n搜索5:", tree.search(5))
print("搜索10:", tree.search(10))
# 删除
print("\n删除前中序遍历:", tree.inorder_traversal())
tree.delete(5)
print("删除5后中序遍历:", tree.inorder_traversal())
tree.delete(7)
print("删除7后中序遍历:", tree.inorder_traversal())
4、 应用
1)三元搜索树(Ternary Search Tree):用于字符串搜索,每个节点比较一个字符,分支到左(<)、中(=)或右(>)
2)空间分割:如3D场景管理,每个节点将空间分为三个部分
3)游戏开发:用于场景管理和AI决策树
4)文件系统:某些文件系统使用三叉结构组织目录
5)语法分析:编译器设计中的语法分析树
6)决策树:多分支决策场景
5、 变种
1)完全三叉树:除最后一层外,所有层都完全填满
2)平衡三叉树:任意节点的子树高度差不超过1
3)三元搜索树(TST):结合了二叉搜索树和字典树的优点
6、 三叉树与二叉树对比
| 特性 | 三叉树 | 二叉树 |
|---|---|---|
| 子节点数 | 最多3个 | 最多2个 |
| 高度 | 相对较低(相同节点数) | 相对较高 |
| 复杂度 | 插入/删除可能更复杂 | 相对简单 |
| 搜索复杂度 | O(log₃ n)(平衡时更快) | O(log n)(平衡) |
| 适用场景 | 需要更多分支的场景 | 大多数通用场景 |
(二) B树
B树(B-Tree)是一种自平衡的多路搜索树,广泛应用于数据库和文件系统中(如MySQL的InnoDB、Btrfs文件系统)。其核心设计目标是减少磁盘I/O次数,通过增加树的分支因子(每个节点的子节点数)来降低树的高度,从而将数据存储在更少的磁盘块中。
1、 特性
多路平衡: 每个节点最多有m个子节点(m为阶数),每个非叶子节点至少有⌈m/2⌉个子节点(根节点除外)
有序存储: 节点内的关键字按升序排列,且关键字将节点空间划分为多个子树范围
数据存储: 所有数据都存储在叶子节点(或非叶子节点,取决于变种如B+树)
高度平衡: 所有叶子节点在同一层,通过分裂和合并操作保持树的平衡
2、 操作
2.1 插入
1)搜索要插入的位置(总是插入到叶子节点)
2)如果叶子节点有空间(键数 < m-1)直接插入
3)如果叶子节点已满,节点键数超过m-1时,节点分裂:
a.将中间键提升到父节点
b.分裂后的两部分成为中间键的左右子节点
4)分裂可能导致父节点继续分裂(递归向上)
5)树高增长:只有根节点分裂时,树高才会增加
6)保持平衡:所有叶子节点始终在同一层级
2.2 搜索
从根节点开始,递归地在适当的子节点中搜索
1)在当前节点中找到第一个不小于搜索键的键
2)如果找到相等的键,则搜索成功
3)否则,进入对应的子节点继续搜索
4)如果到达叶子节点仍未找到,则搜索失败
2.3 删除
1、 规则
1)删除操作始终从叶子节点开始(即使删除内部节点的关键字,也会先转换为删除叶子节点)
2)删除后必须保持B树的平衡性(每个节点至少⌈m/2⌉-1个关键字,根节点至少1个关键字)
3)删除过程中可能需要合并或借用兄弟节点的关键字
2、 删除操作较为复杂,需要考虑多种情况
1)如果键在叶子节点中且删除后仍满足最小键数要求,直接删除
2)如果键在内部节点中,用前驱或后继替换,然后删除前驱或后继
3)如果删除导致节点下溢(键数 < ⌈m/2⌉-1),可以:
从兄弟节点借一个键(如果兄弟节点有富余)
与兄弟节点合并(如果兄弟节点也没有富余)
3、步骤
1)如果要删除的关键字在内部节点,找到前驱或后继并替换
2)从叶子节点删除关键字
3)检查关键字数是否满足要求:
a.满足:结束
b.不满足:尝试从左兄弟借;左兄弟不可借则尝试从右兄弟借;都不可借则合并
4)如果合并导致父节点关键字不足,递归向上处理
时间复杂度:O(log n)(需要从根遍历到叶子)
空间复杂度:O(1)(不需要额外存储)
3、代码示例
class BTreeNode:
def __init__(self, leaf=False):
"""
初始化B树节点
:param leaf: 是否为叶子节点,默认为False
"""
# 标记是否为叶子节点
self.leaf = leaf
# 存储节点中的键
self.keys = []
# 存储子节点指针
self.children = []
class BTree:
def __init__(self, degree):
"""
初始化B树
:param degree: B树的度(阶数)
"""
# 初始时根节点为叶子节点
self.root = BTreeNode(leaf=True)
# B树的度,决定节点的最大和最小子节点数
self.degree = degree
def search(self, key, node=None):
"""
在B树中搜索键
:param key: 要搜索的键
:param node: 当前搜索的节点,默认为根节点
:return: 如果找到返回(节点, 索引),否则返回None
"""
if node is None:
node = self.root
i = 0
# 找到第一个不小于key的键的位置
while i < len(node.keys) and key > node.keys[i]:
i += 1
# 如果找到相等的键,返回结果
if i < len(node.keys) and key == node.keys[i]:
return (node, i)
# 如果是叶子节点且没找到,返回None
if node.leaf:
return None
# 否则在子节点中继续搜索
return self.search(key, node.children[i])
def insert(self, key):
"""
向B树中插入键
:param key: 要插入的键
"""
root = self.root
# 如果根节点已满,需要分裂
if len(root.keys) == 2 * (self.degree - 1): # 节点键数达到2t-1时分裂
new_root = BTreeNode()
new_root.children.append(root)
self.root = new_root
self._split_child(new_root, 0) # 分裂子节点
self._insert_non_full(new_root, key) # 在非满节点中插入
else:
self._insert_non_full(root, key) # 直接在非满节点中插入
def _insert_non_full(self, node, key):
"""
在非满节点中插入键
:param node: 非满节点
:param key: 要插入的键
"""
i = len(node.keys) - 1
if node.leaf:
# 叶子节点直接插入
node.keys.append(None) # 扩展列表
# 找到合适位置并移动键
while i >= 0 and key < node.keys[i]:
node.keys[i + 1] = node.keys[i]
i -= 1
node.keys[i + 1] = key
else:
# 内部节点需要找到合适的子节点
while i >= 0 and key < node.keys[i]:
i -= 1
i += 1
# 检查子节点是否需要分裂
if len(node.children[i].keys) == 2 * (self.degree - 1):
self._split_child(node, i) # 分裂子节点
# 确定应该插入到哪个子节点
if key > node.keys[i]:
i += 1
# 递归插入到子节点
self._insert_non_full(node.children[i], key)
def _split_child(self, parent, index):
"""
分裂父节点的指定子节点
:param parent: 父节点
:param index: 要分裂的子节点索引
"""
degree = self.degree
child = parent.children[index]
new_node = BTreeNode(leaf=child.leaf) # 创建新节点,类型与子节点相同
mid_index = degree - 1 # 中位数位置(t-1)
mid_key = child.keys[mid_index] # 中间键提升到父节点
# 分裂键:左部分留在原节点,右部分移到新节点
left_keys = child.keys[:mid_index]
right_keys = child.keys[mid_index + 1:]
# 将中间键插入父节点
parent.keys.insert(index, mid_key)
# 更新原节点和新节点的键
child.keys = left_keys
new_node.keys = right_keys
# 如果不是叶子节点,需要分裂子节点指针
if not child.leaf:
left_children = child.children[:mid_index + 1]
right_children = child.children[mid_index + 1:]
child.children = left_children
new_node.children = right_children
# 将新节点添加到父节点的子节点列表
parent.children.insert(index + 1, new_node)
def delete(self, key, node=None):
"""
从B树中删除键
:param key: 要删除的键
:param node: 当前节点,默认为根节点
"""
if node is None:
node = self.root
i = 0
# 找到键的位置或应该存在的子节点位置
while i < len(node.keys) and key > node.keys[i]:
i += 1
# 如果键存在于当前节点
if i < len(node.keys) and node.keys[i] == key:
if node.leaf:
# 叶子节点直接删除
node.keys.pop(i)
else:
# 内部节点需要特殊处理
self._delete_internal_node(node, key, i)
else:
if node.leaf:
# 叶子节点且没找到
print(f"Key {key} not found in the tree")
return
# 标记是否是最后一个子节点
flag = (i == len(node.keys))
# 如果子节点键数不足,先填充
if len(node.children[i].keys) < self.degree:
self._fill(node, i)
# 根据flag调整递归路径
if flag and i > len(node.keys):
self.delete(key, node.children[i - 1])
else:
self.delete(key, node.children[i])
def _delete_internal_node(self, node, key, i):
"""
从内部节点删除键
:param node: 内部节点
:param key: 要删除的键
:param i: 键在节点中的索引
"""
degree = self.degree
# 如果左子节点有足够键,用前驱替换
if len(node.children[i].keys) >= degree:
predecessor = self._get_predecessor(node, i)
node.keys[i] = predecessor
self.delete(predecessor, node.children[i])
# 如果右子节点有足够键,用后继替换
elif len(node.children[i + 1].keys) >= degree:
successor = self._get_successor(node, i)
node.keys[i] = successor
self.delete(successor, node.children[i + 1])
# 否则合并子节点
else:
self._merge(node, i)
self.delete(key, node.children[i])
def _get_predecessor(self, node, i):
"""
获取键的前驱(左子树的最大键)
"""
current = node.children[i]
while not current.leaf:
current = current.children[-1]
return current.keys[-1]
def _get_successor(self, node, i):
"""
获取键的后继(右子树的最小键)
"""
current = node.children[i + 1]
while not current.leaf:
current = current.children[0]
return current.keys[0]
def _fill(self, node, i):
"""
填充键数不足的子节点
:param node: 父节点
:param i: 需要填充的子节点索引
"""
degree = self.degree
# 尝试从前一个兄弟节点借键
if i != 0 and len(node.children[i - 1].keys) >= degree:
self._borrow_from_prev(node, i)
# 尝试从后一个兄弟节点借键
elif i != len(node.children) - 1 and len(node.children[i + 1].keys) >= degree:
self._borrow_from_next(node, i)
else:
# 必须合并
if i != len(node.children) - 1:
self._merge(node, i)
else:
self._merge(node, i - 1)
def _borrow_from_prev(self, node, index):
"""
从前一个兄弟节点借一个键
"""
child = node.children[index]
sibling = node.children[index - 1]
# 将父节点的键下移到子节点
child.keys.insert(0, node.keys[index - 1])
# 如果不是叶子节点,还需要移动子节点指针
if not child.leaf:
child.children.insert(0, sibling.children[-1])
sibling.children.pop()
# 将兄弟节点的最大键提升到父节点
node.keys[index - 1] = sibling.keys[-1]
sibling.keys.pop()
def _borrow_from_next(self, node, index):
"""
从后一个兄弟节点借一个键
"""
child = node.children[index]
sibling = node.children[index + 1]
# 将父节点的键下移到子节点
child.keys.append(node.keys[index])
# 如果不是叶子节点,还需要移动子节点指针
if not child.leaf:
child.children.append(sibling.children[0])
sibling.children.pop(0)
# 将兄弟节点的最小键提升到父节点
node.keys[index] = sibling.keys[0]
sibling.keys.pop(0)
def _merge(self, node, i):
"""
合并两个子节点
:param node: 父节点
:param i: 第一个子节点的索引
"""
child = node.children[i]
sibling = node.children[i + 1]
# 将父节点的键下移到子节点
child.keys.append(node.keys[i])
# 合并兄弟节点的键
child.keys += sibling.keys
# 如果不是叶子节点,还需要合并子节点指针
if not child.leaf:
child.children += sibling.children
# 从父节点移除被合并的键和子节点指针
node.keys.pop(i)
node.children.pop(i + 1)
# 如果合并后根节点为空,降低树的高度
if node == self.root and not node.keys:
self.root = child
def print_tree(self, node=None, level=0, indent=" "):
"""
打印B树结构
:param node: 当前节点,默认为根节点
:param level: 当前层级
:param indent: 缩进字符串
"""
if node is None:
node = self.root
print(f"{indent * level}Level {level}: {node.keys}")
if not node.leaf:
for child in node.children:
self.print_tree(child, level + 1, indent)
if __name__ == "__main__":
b_tree = BTree(degree=2) # 创建阶数为2的B树
# 插入关键字
keys = [5, 8, 10, 15, 20, 25, 30, 4, 6]
print("插入顺序:", keys)
for key in keys:
b_tree.insert(key)
print("\nB树结构:")
b_tree.print_tree()
# 测试搜索
print("\n搜索测试:")
for key in [6, 15, 18, 99]:
result = b_tree.search(key)
if result:
print(f"关键字 {key} 存在于树中")
else:
print(f"关键字 {key} 不存在于树中")
# 测试删除
print("\n删除测试:")
to_delete = [8, 15, 10]
for key in to_delete:
print(f"删除关键字 {key} 后:")
b_tree.delete(key)
b_tree.print_tree()
4、 优势
1)适合外存存储:B树设计考虑了磁盘I/O,每个节点大小通常设置为磁盘页大小
2)高度平衡:所有叶子节点在同一层级,保证了稳定的查询性能
3)高效操作:搜索、插入和删除的时间复杂度都是O(log n)
4)范围查询高效:由于数据有序存储,范围查询非常高效
(三) B+树
B+树是一种高效的多路平衡搜索树,广泛应用于数据库和文件系统的索引结构(如MySQL的InnoDB引擎)。它是B树的变种,具有更高的查询效率和更适合磁盘存储的特性。
1、 特性
多路平衡: 每个节点可以有多个子节点(通常远大于2),保持树的高度平衡。
所有数据存储在叶子节点: 非叶子节点仅存储键(key)作为导航信息,不存储实际数据。
叶子节点链表化:叶子节点通过指针连接,形成有序链表,支持高效的范围查询。
高扇出(Fan-out): 单个节点可存储大量键,减少树的高度(通常3-4层即可存储海量数据)。
2、 规则
节点键数量: 非叶子节点至少有⌈m/2⌉-1个键,最多m-1个键。
子节点数量: 非叶子节点的子节点数 = 键数 + 1。
叶子节点: 至少包含⌈m/2⌉个键,且通过指针双向连接。
3、 操作
3.1 插入
1)查找适当的叶子节点
2)将新键值插入叶子节点
a.如果叶子节点未满,直接插入
b.如果叶子节点已满,进行分裂:将节点分裂为两个;将中间键值提升到父节点;如果父节 点已满,递归分裂
3)更新叶子节点的链表指针
3.2 查找
1)从根节点开始,通过比较键值确定子节点指针
2)递归向下查找,直到叶子节点
3)在叶子节点中进行精确查找或范围查找
3.3 删除
1)查找包含要删除键值的叶子节点
2)从叶子节点中删除键值
如果删除后节点不低于最小键值数,完成
否则尝试从兄弟节点借键值
如果无法借键值,与兄弟节点合并
2)递归向上调整父节点的键值
删除原则
1)删除操作始终从叶子节点开始
2)删除后需要保持B+树的性质:
a、节点元素数量 ≥ ⌈m/2⌉-1(根节点除外)
b、所有叶子节点在同一层次
c、叶子节点通过指针链接形成有序链表
特殊情况
1)删除根节点唯一元素:当根节点只剩一个元素且被删除时,树高减1
2)重复键处理:如果B+树允许重复键,删除时需要明确删除哪个实例
3)并发控制:在实际数据库系统中,删除操作通常需要加锁以避免并发问题
时间复杂度:O(log n)(需要遍历树高)
空间复杂度:O(1)(原地操作)
4、代码示例
import bisect
class BPlusTreeNode:
def __init__(self, is_leaf=False):
# 存储节点的键
self.keys = []
# 存储子节点指针(内部节点使用)或数据指针(叶子节点使用)
self.children = []
# 标记是否为叶子节点
self.is_leaf = is_leaf
# 叶子节点间的链表指针,用于范围查询
self.next = None
class BPlusTree:
def __init__(self, order=3):
# 初始化时根节点为叶子节点
self.root = BPlusTreeNode(is_leaf=True)
# B+树的阶(最大子节点数)
self.order = order
def search(self, key):
"""查找键是否存在"""
node = self.root
# 从根节点开始向下查找
while not node.is_leaf:
# 在内部节点中找到合适的子节点(找到第一个大于key的键的位置)
i = 0
while i < len(node.keys) and key >= node.keys[i]:
i += 1
node = node.children[i]
# 在叶子节点中线性查找键
for k in node.keys:
if k == key:
return True # 找到键
return False # 未找到
def insert(self, key):
"""插入键"""
# 1. 找到应该插入的叶子节点
leaf = self._find_leaf(key)
# 2. 插入键到叶子节点
self._insert_into_leaf(leaf, key)
# 3. 检查是否需要分裂
if len(leaf.keys) > self.order - 1: # 超过最大容量(order-1)
new_leaf = self._split_leaf_node(leaf)
# 将新节点的最小键插入到父节点
self._insert_into_parent(leaf, new_leaf.keys[0], new_leaf)
def _find_leaf(self, key):
"""查找应该包含指定键的叶子节点"""
node = self.root
while not node.is_leaf:
# 类似search的查找过程
i = 0
while i < len(node.keys) and key >= node.keys[i]:
i += 1
node = node.children[i]
return node
def _insert_into_leaf(self, leaf, key):
"""将键插入到叶子节点中(保持有序)"""
# 使用bisect找到插入位置(保持有序)
i = bisect.bisect_right(leaf.keys, key)
leaf.keys.insert(i, key)
def _split_leaf_node(self, leaf):
"""分裂叶子节点"""
new_leaf = BPlusTreeNode(is_leaf=True)
# 计算分裂点(中间位置)
split_point = len(leaf.keys) // 2
# 移动后半部分键到新节点
new_leaf.keys = leaf.keys[split_point:]
leaf.keys = leaf.keys[:split_point]
# 设置叶子节点的链接(用于范围查询)
new_leaf.next = leaf.next
leaf.next = new_leaf
return new_leaf
def _split_internal_node(self, node):
"""分裂内部节点"""
new_node = BPlusTreeNode(is_leaf=False)
split_point = len(node.keys) // 2 # 中间位置
split_key = node.keys[split_point] # 提升到父节点的键
# 新节点获取分裂点后的键和子节点
new_node.keys = node.keys[split_point + 1:]
new_node.children = node.children[split_point + 1:]
# 原节点保留分裂点前的键和子节点
node.keys = node.keys[:split_point]
node.children = node.children[:split_point + 1]
return split_key, new_node
def _insert_into_parent(self, old_node, key, new_node):
"""将分裂后的新节点插入到父节点中"""
parent = self._find_parent(self.root, old_node)
if parent is None:
# 如果没有父节点(即old_node是根节点),创建新的根节点
new_root = BPlusTreeNode(is_leaf=False)
new_root.keys = [key]
new_root.children = [old_node, new_node]
self.root = new_root
return
# 找到正确的插入位置
i = bisect.bisect_right(parent.keys, key)
parent.keys.insert(i, key)
parent.children.insert(i + 1, new_node)
# 检查父节点是否需要分裂
if len(parent.keys) > self.order - 1:
split_key, new_parent = self._split_internal_node(parent)
self._insert_into_parent(parent, split_key, new_parent)
def _find_parent(self, node, child):
"""查找指定子节点的父节点(递归实现)"""
if node.is_leaf or node == child:
return None
for i, c in enumerate(node.children):
if c == child:
return node
parent = self._find_parent(c, child)
if parent is not None:
return parent
return None
def delete(self, key):
"""删除键"""
# 1. 找到包含键的叶子节点
leaf = self._find_leaf(key)
if key not in leaf.keys:
return # 键不存在,直接返回
# 2. 从叶子节点中删除键
leaf.keys.remove(key)
# 3. 处理根节点特殊情况
if leaf == self.root:
if len(leaf.keys) == 0:
self.root = None # 树为空
return
# 4. 检查是否需要修复下溢(键数过少)
if len(leaf.keys) < (self.order - 1) // 2: # 少于最小填充度
self._fix_leaf_underflow(leaf)
def _fix_leaf_underflow(self, leaf):
"""修复叶子节点的下溢情况"""
parent = self._find_parent(self.root, leaf)
if not parent:
return # 没有父节点(可能是根节点)
# 找到叶子节点在父节点中的位置
idx = parent.children.index(leaf)
# 情况1:尝试从左兄弟借键
if idx > 0:
left_sibling = parent.children[idx - 1]
if len(left_sibling.keys) > (self.order - 1) // 2:
# 从左兄弟借最后一个键
borrowed_key = left_sibling.keys.pop()
leaf.keys.insert(0, borrowed_key)
# 更新父节点的分隔键
parent.keys[idx - 1] = borrowed_key
return
# 情况2:尝试从右兄弟借键
if idx < len(parent.children) - 1:
right_sibling = parent.children[idx + 1]
if len(right_sibling.keys) > (self.order - 1) // 2:
# 从右兄弟借第一个键
borrowed_key = right_sibling.keys.pop(0)
leaf.keys.append(borrowed_key)
# 更新父节点的分隔键
parent.keys[idx] = right_sibling.keys[0]
return
# 情况3:没有兄弟可以借,需要合并
if idx > 0:
# 与左兄弟合并
left_sibling = parent.children[idx - 1]
left_sibling.keys.extend(leaf.keys)
left_sibling.next = leaf.next
# 从父节点中删除对应的键和子节点
parent.keys.pop(idx - 1)
parent.children.pop(idx)
# 检查父节点是否下溢
if len(parent.keys) < (self.order - 1) // 2 and parent != self.root:
self._fix_internal_underflow(parent)
else:
# 与右兄弟合并
right_sibling = parent.children[idx + 1]
leaf.keys.extend(right_sibling.keys)
leaf.next = right_sibling.next
# 从父节点中删除对应的键和子节点
parent.keys.pop(idx)
parent.children.pop(idx + 1)
# 检查父节点是否下溢
if len(parent.keys) < (self.order - 1) // 2 and parent != self.root:
self._fix_internal_underflow(parent)
def _fix_internal_underflow(self, node):
"""修复内部节点的下溢情况"""
parent = self._find_parent(self.root, node)
if not parent:
# 处理根节点特殊情况
if len(node.keys) == 0 and not node.is_leaf:
# 根节点为空且有子节点,降低树的高度
self.root = node.children[0]
return
# 找到节点在父节点中的位置
idx = parent.children.index(node)
# 情况1:尝试从左兄弟借键
if idx > 0:
left_sibling = parent.children[idx - 1]
if len(left_sibling.keys) > (self.order - 1) // 2:
# 从左兄弟借最后一个键和对应的子节点
borrowed_key = parent.keys[idx - 1]
borrowed_child = left_sibling.children.pop()
node.keys.insert(0, borrowed_key)
node.children.insert(0, borrowed_child)
# 更新父节点的分隔键
parent.keys[idx - 1] = left_sibling.keys.pop()
return
# 情况2:尝试从右兄弟借键
if idx < len(parent.children) - 1:
right_sibling = parent.children[idx + 1]
if len(right_sibling.keys) > (self.order - 1) // 2:
# 从右兄弟借第一个键和对应的子节点
borrowed_key = parent.keys[idx]
borrowed_child = right_sibling.children.pop(0)
node.keys.append(borrowed_key)
node.children.append(borrowed_child)
# 更新父节点的分隔键
parent.keys[idx] = right_sibling.keys.pop(0)
return
# 情况3:没有兄弟可以借,需要合并
if idx > 0:
# 与左兄弟合并
left_sibling = parent.children[idx - 1]
left_sibling.keys.append(parent.keys.pop(idx - 1))
left_sibling.keys.extend(node.keys)
left_sibling.children.extend(node.children)
# 从父节点中删除对应的子节点
parent.children.pop(idx)
# 检查父节点是否下溢
if len(parent.keys) < (self.order - 1) // 2 and parent != self.root:
self._fix_internal_underflow(parent)
else:
# 与右兄弟合并
right_sibling = parent.children[idx + 1]
node.keys.append(parent.keys.pop(idx))
node.keys.extend(right_sibling.keys)
node.children.extend(right_sibling.children)
# 从父节点中删除对应的子节点
parent.children.pop(idx + 1)
# 检查父节点是否下溢
if len(parent.keys) < (self.order - 1) // 2 and parent != self.root:
self._fix_internal_underflow(parent)
def print_tree(self, node=None, level=0):
"""打印树结构(用于调试)"""
if node is None:
node = self.root
print("=== B+树结构 ===")
prefix = " " * level
if node.is_leaf:
print(f"{prefix}↳ 叶子[{level}]: {node.keys}")
else:
print(f"{prefix}■ 索引[{level}]: {node.keys}")
for child in node.children:
self.print_tree(child, level + 1)
if __name__ == "__main__":
bplustree = BPlusTree(order=4)
# 插入测试
keys = [5, 8, 1, 7, 3, 12, 9, 6, 4, 2, 10]
for key in keys:
bplustree.insert(key)
print(f"插入 {key}:")
bplustree.print_tree()
print()
# 查找测试
for key in [12, 4, 20]:
print(f"搜索 {key}: {'Found' if bplustree.search(key) else 'Not found'}")
# 删除测试
print()
delete_keys = [1, 9, 3, 7]
for key in delete_keys:
bplustree.print_tree()
print(f"删除 {key}:")
bplustree.delete(key)
bplustree.print_tree()
print()
5、 优势
1.更适合磁盘存储:节点大小通常设置为磁盘块大小,减少I/O次数
2.查询效率稳定:所有查询都要到叶子节点,路径长度相同
3.范围查询高效:叶子节点链表结构使范围查询非常高效
4.更高的扇出:内部节点不存储数据,可以包含更多键值,降低树高度
6、 应用
数据库索引(MySQL的InnoDB引擎)、文件系统(如NTFS、ReiserFS)、键值存储系统
B树 vs B+树
| 特性 | B树 | B+树 |
|---|---|---|
| 数据存储 | 所有节点均可存储数据 | 所有数据都存储在叶子节点,内部节点只存储键 |
| 叶子节点 | 无链表 | 叶子节点通过指针链接,便于范围查询 |
| 查询效率 | 范围查询需中序遍历整棵树 | 叶子节点直达,更稳定,内部节点可以存储更多的键,从而降低树的高度 |
| 空间利用率 | 非叶子节点占用更多空间 | 非叶子节点更小,扇出更高 |
| 应用场景 | 数据库索引MongoDB | 数据库索引MySQL InnoDB |
四、参考
《算法导论》
《数据结构与算法分析》
1、 可视化工具
USFCA Data Structure Visualization
AVLtree:www.cs.usfca.edu/~galles/vis…
RedBlack:www.cs.usfca.edu/~galles/vis…
BTree:www.cs.usfca.edu/~galles/vis…
BPlusTree:www.cs.usfca.edu/~galles/vis…
VisuAlgo
2、 BST
维基百科(Wikipedia)
地址:en.wikipedia.org/wiki/Binary…
GeeksforGeeks
地址:www.geeksforgeeks.org/binary-sear…
3、AVL
维基百科(AVL Tree)
GitHub: AVL Tree
4、 Red Black Tree
Wikipedia
5、 算法与数据结构,GitHub代码库
MySQL源码中的B+树实现,GitHub仓库
关键文件:storage/innobase/include/btr0btr.ic(B+树核心逻辑)
PostgreSQL的B+树模块,官网文档
www.postgresql.org/docs/curren…
源码路径: src/backend/access/nbtree/