B树
B树是为磁盘或其他直接存取的辅助存储设备而设计的一种平衡搜索树。B树类似于红黑树,但它们在降低磁盘I/O操作数方面要更好些。许多数据库系统使用B树或者B树的变种来存储信息。
B树与红黑树的不同之处在于B树的节点可以有很多孩子,从数个到数千个。
B树的定义
一棵B树T是具有以下性质的有根树(根为 T . r o o t T.root T.root):
-
每个节点 x x x有下面的属性
a. x . n x.n x.n,当前存储在 x x x中的关键字个数
b. x . n x.n x.n个关键字本身 , x . k e y 1 , x . k e y 2 , . . . , x . k e y x . n ,x.key_1, x.key_2, ...,x.key_{x.n} ,x.key1,x.key2,...,x.keyx.n,以非降序存放,使得 x . k e y 1 ≤ x . k e y 2 ≤ . . . ≤ x . k e y x . n x.key_1 \le x.key_2 \le ... \le x.key_{x.n} x.key1≤x.key2≤...≤x.keyx.n
c. x . l e a f x.leaf x.leaf,一个布尔值,如果 x x x是叶节点,则为TRUE; 如果 x x x为内部节点,则为FALSE -
每个内部节点 x x x还包含 x . n + 1 x.n+1 x.n+1个指向其孩子的指针 x . c 1 , x . c 2 , . . . , x . c x . n + 1 x.c_1,x.c_2,..., x.c_{x.n+1} x.c1,x.c2,...,x.cx.n+1,叶节点没有孩子,所以他们的 c i c_i ci属性没有定义
-
关键字 x . k e y i x.key_i x.keyi对存储在各子树中的关键字范围加以分割:如果 k i k_i ki为任意一个存储在以 x . c i x.c_i x.ci为根的子树中的关键字,那么 k 1 ≤ x . k e y 1 ≤ k 2 ≤ x . k e y 2 ≤ . . . ≤ x . k e y x . n ≤ k x . n + 1 k_1 \le x.key_1 \le k_2 \le x.key_2 \le ... \le x.key_{x.n} \le k_{x.n+1} k1≤x.key1≤k2≤x.key2≤...≤x.keyx.n≤kx.n+1
-
每个叶节点具有相同的深度,即树的高度 h h h
-
每个节点所包含的关键字个数有上界和下界。用一个被称为B树的最小度数的固定整数 t ≥ 2 t \ge 2 t≥2来表示这些界:
a. 除了根节点以外的每个节点必须至少有 t − 1 t-1 t−1个关键字,因此。除了根节点以外的每个内部节点至少有 t t t个孩子。如果树非空,根节点至少有一个关键字
b. 每个节点至多可包含 2 t − 1 2t-1 2t−1个关键字,因此,一个内部节点至多可有 2 t 2t 2t个孩子。当一个节点恰好有 2 t − 1 2t-1 2t−1个关键字时,称该节点都是满的
B树的高度
如果
n
≤
1
n \le 1
n≤1,那么对任意一棵包含
n
n
n个关键字、高度为
h
h
h、最小度数
t
≥
2
t \ge 2
t≥2的B树
T
T
T,有
h
≤
l
o
g
t
n
+
1
2
h \le log_t \frac{n+1}{ 2 }
h≤logt2n+1
如下图所示,一棵高度为3的B树可以包含的最小可能关键字个数,每个节点
x
x
x内部显示的是
x
.
n
x.n
x.n
与红黑树对比,这里我们看到了B树的能力,尽管二者的高度都以
O
(
l
g
n
)
O(lgn)
O(lgn)的速度增长,但对B数来说,对数的底可以大很多倍,因此,对大多数的树操作来说,要检查的节点数在B树中要比红黑树中少大约
l
g
t
lgt
lgt的因子。由于在一棵树中检查任意一个节点都需要一次磁盘访问,所以B树避免了大量的磁盘访问。
B树上的基本操作
搜索B树
搜索一棵B树和搜索一棵二叉搜索树很相似,只是在每个节点做所做的不是二叉或者“两路”分支选择,而是根据节点的孩子数做多路分支选择。对每个内部节点
x
x
x,做的是一个
x
.
n
+
1
x.n+1
x.n+1路的分支选择。
它的输入是一个指向某子树根节点
x
x
x的指针,以及要在该子树中搜索的一个关键字
k
k
k,如果
k
k
k在B树中,那么返回的是由节点
y
y
y和使得
y
.
k
e
y
i
=
k
y.key_i=k
y.keyi=k的下标
i
i
i组成的有序对
(
y
,
i
)
(y,i)
(y,i),否则,返回
N
I
L
NIL
NIL。
下图所示,显示了搜索的操作过程,浅阴影的节点是搜索关键字
R
R
R的过程中被检查的节点。
python实现函数如下:
# search
def btree_search(self,x, k):
i = 0
while i<=x.n and k > x.keys[i]:
i+=1
# 检查是否已经找到关键字
if i < x.n and k == x.keys[i]:
return (x,i)
#没找到,若是叶子节点,则查找不成功
elif x.leaf:
return None
#非叶子节点,继续递归查找孩子节点
else:
return self.btree_search(x.childs[i],k)
B树插入
B树中插入一个关键字要比二叉搜索树中插入一个关键字复杂得多,在B树中,不能简单地创建一个新的叶节点,然后将其插入,因为这样得到的树将不再是合法的B树。相反,我们是将新的关键字插入到一个已经存在的叶节点上,由于不能将关键字插入一个满的叶节点,故引入一个操作,将一个满的节点
y
y
y(有
2
t
−
1
2t-1
2t−1个关键字)按其中关键字
y
.
k
e
y
t
y.key_t
y.keyt分裂为两个各含
t
−
1
t-1
t−1个关键字节点。中间关键字被提升到
y
y
y的父节点,以标识两棵树的划分点。但是如果
y
y
y的父节点也是满的,就必须在插入新的关键字之前将其分裂,最终满节点的分裂会沿着树向上传播。
与一棵二叉搜索树一样,可以从树根到叶子这个单程向下过程中将一个新的关键字插入B树中。为了做到这一点,我们并不是等到找出插入过程中实际要分裂的满节点时才做分裂,相反,当沿着树往下查找新的关键字所属位置时,就分裂沿途遇到的每个满节点(包括叶节点本身)。因此,每当要分裂一个满节点
y
y
y时,就能确保它的父节点不是满的。
分裂B树中的节点
输入时一个非满的内部节点
x
x
x和一个使
x
.
c
i
x.c_i
x.ci为
x
x
x的满子节点的下标
i
i
i。该过程把这个子节点分裂成两个,并调整
x
x
x,使之包含多出来的孩子。要分裂一个满的根,首先要让根成为一个新的空根节点的孩子,树的高度因此增加1,分裂是树长高的唯一途径。如下图所示,满节点
y
=
x
.
c
i
y=x.c_i
y=x.ci按照其中关键字
S
S
S进行分裂,
S
S
S提升到
y
y
y的父节点
x
x
x。
y
y
y中的那些大于中间关键字的关键字都置于一个新的节点
z
z
z中,它成为
x
x
x的一个新的孩子。
python实现函数:
'''
输入一个非满的内部节点x,和一个使x.child[i]为x的满子节点的下标i
把子节点分裂成两个,并调整x
'''
def btree_split_child(self, x, i):
#分配一个新节点
#z = Node()
z = self.__new_node()
#获取x的第i个孩子节点
y = x.childs[i]
#更新新生成的节点z
z.leaf = y.leaf
#分裂, y关键字2t-1变成t-1,z获取y中最右边的t-1个关键字
z.n = self.t-1
#把y的t-1个关键字以及相应的t个孩子赋值z
for j in range(self.t-1):
z.keys[j] = y.keys[j+self.t]
if not y.leaf:
for j in range(self.t):
z.childs[j] = y.childs[j+self.t]
#调整y的关键字个数
y.n = self.t - 1
# z插入为x的一个孩子
for j in range(x.n+1, i, -1):
x.childs[j] = x.childs[j-1]
x.childs[i+1] = z
#提升y的中间关键字到x来分割y和z
for j in range(x.n, i-1, -1):
x.keys[j] = x.keys[j-1]
x.keys[i] = y.keys[self.t-1]
#调整x的关键字个数
x.n = x.n+1
以沿树单程下行方式向B树插入关键字
在一棵高度为
h
h
h的B树
T
T
T中,以沿树单程下行方式插入一个关键字
k
k
k的操作需要
O
(
h
)
O(h)
O(h)次磁盘存取,所需要的CPU时间为
O
(
t
h
)
=
O
(
t
l
o
g
t
n
)
O(th)=O(tlog_tn)
O(th)=O(tlogtn),利用上述的 btree_split_child来保证递归始终不会将至一个满节点。对根进行分裂是增加B树高度的唯一途径,如下图所示,B树高度的增加发生在顶部而不是底部。
分裂
t
=
4
t=4
t=4的根,根节点
r
r
r一分为二,并创建了一个新节点
s
s
s。树的根包含了
r
r
r的中间关键字。且以
r
r
r的两半作为孩子。当根被分裂时,B树的高度增加1。以下是将关键字
k
k
k插入节点
x
x
x,要求假定调用该过程时
x
x
x是非满的。
python实现函数如下:
'''
将关键字k插入到节点x中,假定在调用过程中x是非满的
'''
def btree_insert_nonfull(self, x, k):
i = x.n
#x是叶子节点,直接插入
if x.leaf:
while i>=1 and k<x.keys[i-1]:
x.keys[i] = x.keys[i-1]
i-=1
x.keys[i] = k
#更新节点数
x.n+=1
#非叶节点
else:
while i>=1 and k<x.keys[i-1]:
i-=1
i+=1
#判断是否递归降至一个满子节点
if x.childs[i-1].n == 2*self.t-1:
self.btree_split_child(x,i-1)
#确定向两个孩子中哪个下降是正确的
if k>x.keys[i-1]:
i+=1
#递归地将k插入合适的子树中
self.btree_insert_nonfull(x.childs[i-1],k)
因为btree_insert_nonfull是尾递归的,所以它也可以用一个while循环来实现,从而说明了在任何时刻,需要留在主存中的页数为
O
(
1
)
O(1)
O(1)。如下图所示,向B树中插入关键字,这棵B树的最小度数
t
t
t为3,所以一个节点至多包含5个关键字。在插入过程中被修改的节点由浅阴影标记。
从B树中删除关键字
B树上的删除操作与插入操作类似,但更复杂,因为可以从任意一个节点中删除一个关键字,而不仅仅是叶节点,而且当从一个内部节点删除一个关键字时,还要重新安排这个节点的孩子,防止因删除操作而导致树的结构违反B树的性质。如下图所示,从一棵B树中删除关键字的各种情况。
-
如果关键字 k k k在节点 x x x中,并且 x x x是叶节点,则从 x x x中删除 k k k
-
如果关键字 k k k在节点 x x x中,并且 x x x是内部节点,则做如下操作:
a. 如果节点 x x x前于 k k k的子节点 y y y至少包含 t t t个关键字,则找出 k k k在以 y y y为根的子树中的前驱 k ′ k^{'} k′,递归地删除 k ′ k^{'} k′,并在 x x x中用 k ′ k^{'} k′替代 k k k
b. 对称地,如果 y y y有少于 t t t个关键字,则检查节点 x x x中后于 k k k的子节点 z z z,如果 z z z至少有 t t t个关键字,则找出 k k k在以 z z z为根的子树中的后继 k ′ k^{'} k′。递归地删除 k ′ k^{'} k′,并在 x x x中用 k ′ k{'} k′代替 k k k。
c. 否则,如果 y y y和 z z z都只含有 t − 1 t-1 t−1个关键字,则将 k k k和 z z z的全部合并进 y y y,这样 x x x就失去了 k k k和指向 x x x的指针,并且 y y y现在包含 2 t − 1 2t-1 2t−1个关键字。然后释放 z z z并递归地从 y y y中删除 k k k。 -
如果关键字 k k k当前不在内部节点 x x x中,则确定必包含 k k k的子树的根 x . c i x.c_i x.ci。如果 x . c i x.c_i x.ci只有 t − 1 t-1 t−1个关键字,必须执行步骤3a或3b来保证降至一个至少包含 t t t个关键字的节点,然后,通过对 x x x的某个合适的子节点进行递归而结束。
a. 如果 x . c i x.c_i x.ci只含有 t − 1 t-1 t−1个关键字,但是它的一个相邻的兄弟至少包含 t t t个关键字,则将 x x x中的某个关键字降至 x . c i x.c_i x.ci中,将 x . c i x.c_i x.ci的相邻左兄弟或右兄弟的一个关键字提升至 x x x,将该兄弟中相应的孩子指针移到 x . c i x.c_i x.ci中,这样使得 x . c i x.c_i x.ci增加了一个额外的关键字。
b. 如果 x . c i x.c_i x.ci以及 x . c i x.c_i x.ci的所有相邻兄弟都只含有 t − 1 t-1 t−1个关键字,则将 x . c i x.c_i x.ci与一个兄弟合并,即将 x x x的一个关键字移至新合并的节点,使之成为该节点的中间关键字。
B树整个python代码如下:
# -*- coding:utf8 -*-
import sys
#构造节点
class Node(object):
def __init__(self, n=0,isleaf = True):
#节点关键字数量n
self.n = n
#关键字keys值
self.keys = []
# 孩子节点
self.childs = []
#是否是叶子节点
self.leaf = isleaf
@classmethod
def allocate_node(self, key_max):
node = Node()
child_max = key_max+1
#初始化key and child
for i in range(key_max):
node.keys.append(None)
for i in range(child_max):
node.childs.append(None)
return node
class BTree(object):
def __init__(self, t, root=None):
# B数的最小度数
self.t = t
#节点包含的关键字的最大个数
self.max_key = 2*self.t-1
#节点包含的最大孩子个数
self.max_child = self.max_key+1
#跟节点
self.root = root
'''
输入一个非满的内部节点x,和一个使x.child[i]为x的满子节点的下标i
把子节点分裂成两个,并调整x
'''
def btree_split_child(self, x, i):
#分配一个新节点
#z = Node()
z = self.__new_node()
#获取x的第i个孩子节点
y = x.childs[i]
#更新新生成的节点z
z.leaf = y.leaf
#分裂, y关键字2t-1变成t-1,z获取y中最右边的t-1个关键字
z.n = self.t-1
#把y的t-1个关键字以及相应的t个孩子赋值z
for j in range(self.t-1):
z.keys[j] = y.keys[j+self.t]
if not y.leaf:
for j in range(self.t):
z.childs[j] = y.childs[j+self.t]
#调整y的关键字个数
y.n = self.t - 1
# z插入为x的一个孩子
for j in range(x.n+1, i, -1):
x.childs[j] = x.childs[j-1]
x.childs[i+1] = z
#提升y的中间关键字到x来分割y和z
for j in range(x.n, i-1, -1):
x.keys[j] = x.keys[j-1]
x.keys[i] = y.keys[self.t-1]
#调整x的关键字个数
x.n = x.n+1
'''
将关键字k插入到节点x中,假定在调用过程中x是非满的
'''
def btree_insert_nonfull(self, x, k):
i = x.n
#x是叶子节点,直接插入
if x.leaf:
while i>=1 and k<x.keys[i-1]:
x.keys[i] = x.keys[i-1]
i-=1
x.keys[i] = k
#更新节点数
x.n+=1
#非叶节点
else:
while i>=1 and k<x.keys[i-1]:
i-=1
i+=1
#判断是否递归降至一个满子节点
if x.childs[i-1].n == 2*self.t-1:
self.btree_split_child(x,i-1)
#确定向两个孩子中哪个下降是正确的
if k>x.keys[i-1]:
i+=1
#递归地将k插入合适的子树中
self.btree_insert_nonfull(x.childs[i-1],k)
def __new_node(self):
'''
创建新的B树节点
'''
return Node().allocate_node(self.max_key)
'''
插入,利用btree_insert_child保证递归始终不会降至一个满节点
'''
def btree_insert(self, k):
# 检查是否为空树
if self.root is None:
node = self.__new_node()
self.root = node
r = self.root
#根节点是满节点
if r.n == 2*self.t - 1:
#s = Node()
s = self.__new_node()
# s成为新的根节点
self.root = s
s.leaf = False
s.n = 0
s.childs[0] = r
#分裂根节点,对根进行分裂是增加b树高度的唯一途径
self.btree_split_child(s,0)
self.btree_insert_nonfull(s,k)
else:
self.btree_insert_nonfull(r,k)
#遍历,逐层遍历
def btree_walk(self):
current = [self.root]
while current:
next_current = []
output = ""
for node in current:
if node !=None and node.childs:
next_current.extend(node.childs)
if node !=None:
output+=''.join(node.keys[0:node.n]) + " "
print(output)
current = next_current
#中序遍历,从小到大的顺序输出key
def btree_order(self, tree):
if tree is not None:
for i in range(tree.n):
self.btree_order(tree.childs[i])
print(tree.keys[i],end=" ")
self.btree_order(tree.childs[i+1])
# search
def btree_search(self,x, k):
i = 0
while i<=x.n and k > x.keys[i]:
i+=1
# 检查是否已经找到关键字
if i < x.n and k == x.keys[i]:
return (x,i)
#没找到,若是叶子节点,则查找不成功
elif x.leaf:
return None
#非叶子节点,继续递归查找孩子节点
else:
return self.btree_search(x.childs[i],k)
if __name__=='__main__':
tree = BTree(3)
for x in ['G','M','P','X', 'A','C','D','E','J','K','N','O','R','S','T','U','V','Y','Z', 'B','Q','L', 'F']:
tree.btree_insert(x)
#逐层遍历和从小到大遍历输出
tree.btree_walk()
tree.btree_order(tree.root)
print('\n')
#search
result = tree.btree_search(tree.root, '1')
if result!=None:
print("find key :"+result[0].keys[result[1]])
else:
print("not find key")
具体代码,可参考github地址算法导论各章节python实现