B树的分裂与合并操作详解
引言
B树是一种自平衡的树数据结构,广泛应用于数据库和文件系统中,以维持有序数据并允许高效的插入、删除和搜索操作。作为一种多路搜索树,B树的每个节点可以有多个子节点和多个键值。B树的关键特性在于其通过分裂和合并操作来保持平衡。本文将详细介绍B树的分裂与合并操作,并提供具体的源码示例。
B树基础知识
在讨论B树的分裂与合并操作之前,先简要回顾一下B树的基本概念和性质。
B树的定义
B树是一种平衡的多路搜索树,满足以下性质:
- 每个节点最多有
m
个子节点(称为m阶B树)。 - 除根节点外,每个节点至少有
⌈m/2⌉
个子节点。 - 根节点至少有两个子节点(如果非叶子节点)。
- 每个节点包含
k-1
个键值和k
个子节点指针,其中⌈m/2⌉ ≤ k ≤ m
。 - 所有叶子节点都位于同一层。
B树的结构
B树节点的结构如下所示:
Node:
keys: [k1, k2, ..., kn]
children: [c0, c1, ..., cn]
n: 当前节点中的键值数量
leaf: 是否为叶子节点
B树的分裂操作
分裂操作是B树在插入过程中用于保持平衡的关键操作。当一个节点中的键值数量达到最大值m-1
时,该节点需要分裂为两个节点,并将中间键提升到父节点中。
分裂操作的步骤
- 创建一个新的节点,作为分裂后的一半。
- 将中间键及其左侧的键值和子节点保留在原节点中。
- 将中间键右侧的键值和子节点移动到新节点中。
- 将中间键提升到父节点中。如果父节点也满了,递归执行分裂操作。
分裂操作的示例
假设我们有一个B树节点,包含以下键值:
Node:
keys: [10, 20, 30, 40, 50]
children: [c0, c1, c2, c3, c4, c5]
n: 5
leaf: False
在插入新键值60时,节点已满,需要进行分裂操作:
-
创建一个新节点:
New Node:
keys: []
children: []
n: 0
leaf: False -
将中间键及其左侧键值保留在原节点中:
Original Node (after split):
keys: [10, 20]
children: [c0, c1, c2]
n: 2
leaf: False -
将中间键右侧的键值和子节点移动到新节点中:
New Node (after split):
keys: [40, 50, 60]
children: [c3, c4, c5, c6]
n: 3
leaf: False -
将中间键30提升到父节点中:
Parent Node (after split):
keys: [30]
children: [Original Node, New Node]
n: 1
leaf: False
B树的合并操作
合并操作是B树在删除过程中用于保持平衡的关键操作。当一个节点中的键值数量低于最小值⌈m/2⌉-1
时,该节点需要与其相邻的兄弟节点合并,或者从父节点借一个键值。
合并操作的步骤
- 检查节点的左兄弟和右兄弟,选择一个键值数量大于最小值的兄弟节点。
- 将父节点中的键值下移到当前节点中。
- 将兄弟节点中的键值上移到父节点中。
- 如果兄弟节点的键值数量也低于最小值,合并当前节点和兄弟节点,并递归执行合并操作。
合并操作的示例
假设我们有一个B树节点,包含以下键值:
Parent Node:
keys: [20, 40]
children: [Node1, Node2, Node3]
n: 2
leaf: False
删除节点Node2中的键值30后,节点中的键值数量低于最小值,需要进行合并操作:
-
检查左兄弟Node1和右兄弟Node3,选择一个键值数量大于最小值的兄弟节点。假设选择右兄弟Node3:
Node3:
keys: [50, 60]
children: [c5, c6, c7]
n: 2
leaf: False -
将父节点中的键值40下移到当前节点Node2中:
Node2 (after borrow):
keys: [40]
children: [c3, c4]
n: 1
leaf: False -
将右兄弟Node3中的键值50上移到父节点中:
Parent Node (after borrow):
keys: [20, 50]
children: [Node1, Node2, Node3]
n: 2
leaf: False
如果右兄弟Node3的键值数量也低于最小值,则需要进行合并操作:
-
将当前节点Node2与右兄弟Node3合并:
Node2 (after merge):
keys: [40, 50, 60]
children: [c3, c4, c5, c6, c7]
n: 3
leaf: False -
将父节点中的键值50下移到当前节点Node2中,删除右兄弟Node3:
Parent Node (after merge):
keys: [20]
children: [Node1, Node2]
n: 1
leaf: False
源码示例
以下是B树的分裂与合并操作的完整源码示例,使用Python语言实现。
python
class BTreeNode:
def __init__(self, t, leaf=False):
self.t = t # B树的最小度数
self.keys = []
self.children = []
self.leaf = leaf
class BTree:
def __init__(self, t):
self.root = BTreeNode(t, True)
self.t = t
def insert(self, k):
root = self.root
if len(root.keys) == 2 * self.t - 1:
s = BTreeNode(self.t, False)
s.children.append(self.root)
self.split_child(s, 0)
self.root = s
self.insert_non_full(s, k)
else:
self.insert_non_full(root, k)
def insert_non_full(self, x, k):
i = len(x.keys) - 1
if x.leaf:
x.keys.append(None)
while i >= 0 and k < x.keys[i]:
x.keys[i + 1] = x.keys[i]
i -= 1
x.keys[i + 1] = k
else:
while i >= 0 and k < x.keys[i]:
i -= 1
i += 1
if len(x.children[i].keys) == 2 * self.t - 1:
self.split_child(x, i)
if k > x.keys[i]:
i += 1
self.insert_non_full(x.children[i], k)
def split_child(self, x, i):
t = self.t
y = x.children[i]
z = BTreeNode(t, y.leaf)
x.children.insert(i + 1, z)
x.keys.insert(i, y.keys[t - 1])
z.keys = y.keys[t:]
y.keys = y.keys[:t - 1]
if not y.leaf:
z.children = y.children[t:]
y.children = y.children[:t]
def delete(self, k):
self._delete(self.root, k)
if len(self.root.keys) == 0 and not self.root.leaf:
self.root = self.root.children[0]
def _delete(self, x, k):
t = self.t
if k in x.keys:
if x.leaf:
x.keys.remove(k)
else:
idx = x.keys.index(k)
if len(x.children[idx].keys) >= t:
predecessor = self.get_predecessor(x, idx)
x.keys[idx] = predecessor
self._delete(x.children[idx], predecessor)
elif len(x.children[idx + 1].keys) >= t:
successor = self.get_successor(x, idx)
x.keys[idx] = successor
self._delete(x.children[idx + 1], successor)
else:
self.merge(x, idx)
self._delete(x.children[idx], k)
else:
if x.leaf:
return
idx = self.find_key(x, k)
if len(x.children[idx].keys) < t:
if idx > 0 and len(x.children[idx - 1].keys) >= t:
self.borrow_from_prev(x, idx)
elif idx < len(x.children) - 1 and len(x.children[idx + 1].keys) >= t:
self.borrow_from_next(x, idx)
else:
if idx != len(x.children) - 1:
self.merge(x, idx)
else:
self.merge(x, idx - 1)
self._delete(x.children[idx], k)
def get_predecessor(self, x, idx):
current = x.children[idx]
while not current.leaf:
current = current.children[-1]
return current.keys[-1]
def get_successor(self, x, idx):
current = x.children[idx + 1]
while not current.leaf:
current = current.children[0]
return current.keys[0]
def merge(self, x, idx):
t = self.t
child = x.children[idx]
sibling = x.children[idx + 1]
child.keys.append(x.keys[idx])
child.keys.extend(sibling.keys)
if not child.leaf:
child.children.extend(sibling.children)
x.keys.pop(idx)
x.children.pop(idx + 1)
def borrow_from_prev(self, x, idx):
child = x.children[idx]
sibling = x.children[idx - 1]
child.keys.insert(0, x.keys[idx - 1])
x.keys[idx - 1] = sibling.keys.pop()
if not child.leaf:
child.children.insert(0, sibling.children.pop())
def borrow_from_next(self, x, idx):
child = x.children[idx]
sibling = x.children[idx + 1]
child.keys.append(x.keys[idx])
x.keys[idx] = sibling.keys.pop(0)
if not child.leaf:
child.children.append(sibling.children.pop(0))
def find_key(self, x, k):
idx = 0
while idx < len(x.keys) and x.keys[idx] < k:
idx += 1
return idx
def traverse(self, x=None, depth=0):
if x is None:
x = self.root
print(" " * depth, x.keys)
if not x.leaf:
for child in x.children:
self.traverse(child, depth + 4)
def search(self, k, x=None):
if x is None:
x = self.root
i = 0
while i < len(x.keys) and k > x.keys[i]:
i += 1
if i < len(x.keys) and k == x.keys[i]:
return True
if x.leaf:
return False
return self.search(k, x.children[i])
# 测试B树
btree = BTree(3)
for i in range(10):
btree.insert(i)
print("Initial tree:")
btree.traverse()
btree.delete(3)
print("\nTree after deleting 3:")
btree.traverse()
btree.delete(6)
print("\nTree after deleting 6:")
btree.traverse()
结论
通过本文的详细介绍,我们理解了B树的分裂与合并操作的原理和实现方式。分裂操作在插入过程中用于保持B树的平衡,而合并操作在删除过程中用于保持B树的平衡。这些操作确保了B树能够高效地执行插入、删除和搜索操作。通过源码示例,我们展示了如何在实际编程中实现这些操作,并测试了它们的正确性。希望本文对读者深入理解B树的工作机制有所帮助,并能够在实际项目中应用B树。