在前面的文章《B+树插入操作的图形化展示,python调用graphviz自动生成svg图形》中用图形化的方式展示了B+树在插入时图形的变化。
本文用来展示B+树删除操作的过程。
在网上查到一篇文献《关于 B+tree (附 python 模拟代码)》。该文作者用python实现了B+树的基本操作。但是他的输出结果是文本形式的,不方便直观地查看B+树的动态变化效果。本文在原作者python代码的基础上做了一些改进,实现了图形化展示的效果。对原作者的分享精神,再次表示感谢。
图形化显示环境的搭建细节,还请参看以前的文章《平衡二叉树插入及删除操作的图形化展示,python调用graphviz自动生成svg图形》
对svg图形做美化处理的细节,还请参看以前的文章《用jquery对graphviz生成的svg图形做后处理,改变字体,颜色,连线形状等》
下面是插入一个随机选定的key序列[10, 17, 9, 33, 33, 50, 36, 41, 31, 30, 13, 6, 37, 45, 20, 4, 35, 11, 2, 40] 生成一棵B+树后再进行删除操作的图文演示
其它序列的插入过程的图形效果,可以将代码下载到本地后自已手动调整源码,然后在浏览器上查看图形效果。当然前提是你的电脑上已经安装好了python与graphviz。
BplusTree.py
# -*- coding:utf-8 -*-
# B+树的序列化
# 原作者:thursdayhawk http://wiki.jikexueyuan.com/project/python-actual-combat/tutorial-11.html
# 修改成用graphviz图形化显示,添加了序列化为json格式文件的功能,修改者:littleZhuHui
import os
import json
from random import randint,choice
from bisect import bisect_right,bisect_left
from collections import deque
#显示信息的级别,控制不同等级的信息的可见性
infoLevel = 1
# id值为1的节点是 superRoot,是所有操作的起点,
#因此固定为1,而0表示找不到有效的节点
idSeed = 1
#存放已加载节点的hash表,由节点id映射到节点对象
nodePool ={}
#生成一个全局ID
def getGID():
global idSeed
idSeed+=1
return idSeed
#------------------ node 定义开始 ----------------------------------------------
class InitError(Exception):
pass
class LoadNodeError(Exception):
pass
class ParaError(Exception):
pass
# 定义键值对(为了处理重复键值,对kv类做改进,使得一个key可以对应多个value)
class KeyValue(object):
__slots__=('key','valueList')
def __init__(self,key,value):
self.key=key
self.valueList=[value]
def __str__(self):
return '%s,%s'%(self.key,str([v for v in self.valueList]))
def __cmp__(self,key):
if self.key>key:
return 1
elif self.key==key:
return 0
else:
return -1
def appendValue(self,value):
self.valueList.insert(0,value)
@property
def value(self): #默认value就是list中的第0个value
return self.valueList[0]
@property
def vLen(self):
return len(self.valueList)
# 定义可按下标进行控制访问的childList类
# 不直接使用列表而用自定义对象表示childList ,主要原因有两点
# 按需加载,孩子节点序列化时存放的节点Id值,当实际访问时加载为对象
# 访问控制,当修改了孩子节点指针的值之后,可以有机会做更多的处理(可以实现事件钩子功能)
class ChildList(object):
def __init__(self,listVal=[]):
self._childList = listVal
def __getitem__(self,i):
if type(self._childList[i])==type(0): #需要加载
self._childList[i] = loadNode(self._childList[i])
return self._childList[i]
def __setitem__(self,i,value):
self._childList[i] = value
def index(self,value):
return self._childList.index(value)
def insert(self,i,value):
return self._childList.insert(i,value)
def remove(self,value):
return self._childList.remove(value)
def pop(self):
return self._childList.pop()
# 内部节点
class Bptree_InterNode(object):
def __init__(self,M,id=-1):
if not isinstance(M,int):
raise InitError,'M must be int'
if M<=3:
raise InitError,'M must be greater then 3'
else:
self.M=M
self.childList=ChildList()
self.indexList=[]
self._bro=-1
self._par=None
#每个节点有一个唯一的整数值做为id,方便用graphviz绘图
if id < 0:
self.id = getGID()
else:
self.id = id
def isleaf(self):
return False
def isfull(self):
return len(self.indexList)>=self.M-1
def isempty(self):
return len(self.indexList)<=(self.M+1)/2-1
@property
def bro(self):
if type(self._bro)==type(0): #需要加载
if self._bro > 1 : # 确实对应着一个节点,1是超级根节点,不算在内
self._bro = loadNode(self._bro)
return self._bro
@bro.setter
def bro(self,value):
#可在此处做一些额外的处理
self._bro=value
@property
def par(self):
if type(self._par)==type(0): #需要加载
if self._bro > 1 :
self._par = loadNode(self._par)
return self._par
@par.setter
def par(self,value):
#可在此处做一些额外的处理
self._par=value
#叶子节点
class Bptree_Leaf(object):
def __init__(self,L,id=-1):
if not isinstance(L,int):
raise InitError,'L must be int'
else:
self.L=L
#self.childList='
\nleaf has no child list
\n'
self.kvList=[]
self._bro=0
self._par=None
#每个节点有一个唯一的整数值做为id,方便用graphviz绘图
if id < 0:
self.id = getGID()
else:
self.id = id
def isleaf(self):
return True
def isfull(self):
return len(self.kvList)>self.L
def isempty(self):
return len(self.kvList)<=(self.L+1)/2
@property
def bro(self):
if type(self._bro)==type(0): #需要加载
if self._bro > 1 :
self._bro = loadNode(self._bro)
return self._bro
@bro.setter
def bro(self,value):
#可在此处做一些额外的处理
self._bro=value
@property
def par(self):
if type(self._par)==type(0): #需要加载
if self._bro > 1 :
self._par = loadNode(self._par)
return self._par
@par.setter
def par(self,value):
#可在此处做一些额外的处理
self._par=value
#------------------ node 定义结束 ----------------------------------------------
#------------------ B+ 树 定义开始 ----------------------------------------
#B+树类
class Bptree(object):
def __init__(self,M,L):
if L>M:
raise InitError,'L must be less or equal then M'
else:
self.M=M
self.L=L
self.__root=Bptree_Leaf(L)
self.__leaf=self.__root
#在树上查找
def search(self,mi=None,ma=None):
result=[]
node=self.__root
leaf=self.__leaf
if mi is None and ma is None:
raise ParaError,'you need to setup searching range'
elif mi is not None and ma is not None and mi>ma:
raise ParaError,'upper bound must be greater or equal than lower bound'
def search_key(n,k):
if n.isleaf():
p=bisect_left(n.kvList,k)
return (p,n)
else:
p=bisect_right(n.indexList,k)
return search_key(n.childList[p],k)
if mi is None:
while True:
for kv in leaf.kvList:
if kv<=ma:
result.append(kv)
else:
return result
if leaf.bro==None:
return result
else:
leaf=leaf.bro
elif ma is None:
index,leaf=search_key(node,mi)
result.extend(leaf.kvList[index:])
while True:
if leaf.bro==None:
return result
else:
leaf=leaf.bro
result.extend(leaf.kvList)
else:
if mi==ma:
i,l=search_key(node,mi)
try:
if l.kvList[i]==mi:
result.append(l.kvList[i])
return result
else:
return result
except IndexError:
return result
else:
i1,l1=search_key(node,mi)
i2,l2=search_key(node,ma)
if l1 is l2:
if i1==i2:
return result
else:
result.extend(l.kvList[i1:i2])
return result
else:
result.extend(l1.kvList[i1:])
l=l1
while True:
if l.bro==l2:
result.extend(l2.kvList[:i2+1])
return result
else:
result.extend(l.bro.kvList)
l=l.bro
#遍历B+树的所有叶子节点
def traversal(self):
result=[]
l=self.__leaf
while True:
result.extend(l.kvList)
if l.bro==None:
return result
else:
l=l.bro
#显示B+树
def show(self):
def dotShow(tree):
q=deque()
h=0
q.append([self.__root,h])
#生成childList对应的dot格式的文本串
def childListDotStr(n):
dotStr ='{'
if n.childList==[]:
return '{}'
else:
for i,k in enumerate(n.indexList):
dotStr +='#%s|' %(n.childList[i].id,n.childList[i].id)
#childList比indexList多一个,要处理一下最右孩子指针
dotStr +='#%s}' %(n.childList[-1].id,n.childList[-1].id)
return dotStr
#生成childList对应的dot格式的文本串
def childListEdgeStr(n):
dotStr =''
if n.childList==[]:
return ''
else:
for i,k in enumerate(n.indexList):
dotStr +='node%s:f%s:s--node%s:e:n;\n'% (n.id,n.childList[i].id,n.childList[i].id)
#childList比indexList多一个,要处理一下最右孩子指针
dotStr +='node%s:f%s:s--node%s:e:n;\n'% (n.id,n.childList[-1].id,n.childList[-1].id)
return dotStr
while True:
try:
node,height=q.popleft()
except IndexError:
return
else:
if not node.isleaf(): #内部节点
#print node.indexList,'the height is',height
nodeText = str([k for k in node.indexList])
tree.dotStr += 'node%s [label = "{ #%s|%s| %s}" ];\n' % (node.id,node.id,nodeText,childListDotStr(node))
tree.dotStr += childListEdgeStr(node)
if height==h:
h+=1
q.extend([[n,h] for n in node.childList])
else: #叶节点
#print [v.key for v in node.kvList],'the leaf is,',height
nodeText = str([i.key if i.vLen <=1 else '%s^%s'%(i.key,i.vLen) for i in node.kvList])
tree.dotStr += 'node%s [label = "{ #%s|%s}" ];\n' % (node.id,node.id,nodeText)
self.dotStr=''
dotShow(self)
print(self.svgStr())
#导出B+树
def dump(self):
def doDump():
q=deque()
h=0
q.append([self.__root,h])
while True:
try:
node,height=q.popleft()
dumpNode(node)
except IndexError:
return
else:
if not node.isleaf(): #内部节点
if height==h:
h+=1
q.extend([[n,h] for n in node.childList])
else: #叶节点
pass
doDump()
#保存根节点的id信息到node1.json
with open('nodes/node1.json', 'w') as f:
f.write(json.dumps({'id':self.__root.id}))
#加载根节点,而子节点是在真实访问时,发现如果存放的是整数id值,才真正加载进来(按需加载)
def loadRoot(self,nodeId):
self.__root = loadNode(nodeId)
self.__leaf=self.__root
#debug(self.__root,'__root')
#加载B+树,假定根节点信息总是存话在node1.json文件中
def load(self):
rawNode=None
with open('nodes/node1.json', 'r') as f:
rawNode = json.load(f)
#debug(rawNode)
if rawNode :
nodeId = rawNode['id']
self.loadRoot(nodeId)
else:
raise LoadNodeError,'can not load node1.json '
# 生成svg图形对应的文本串
def svgStr(self):
dotHeader ='''
graph G
{
rankdir = TB;
node [shape=record];
'''
dotStr = dotHeader + self.dotStr +'}'
dotFile =open('BplusTree.dot','w')
dotFile.write(dotStr)
dotFile.close()
#调用dot命令生成svg文件
os.system('dot -Tsvg BplusTree.dot -o BplusTree.html')
#取出svg图形文件的内容
svgFile =open('BplusTree.html','r')
svgStr = svgFile.read()
svgFile.close()
return svgStr
#插入操作
def insert(self,key_value):
#内部节点分裂
def split_node(n1):
mid=self.M/2 #分裂点为度数的中点
newnode=Bptree_InterNode(self.M)
#新节点的数据是原节点的后半部分
newnode.indexList=n1.indexList[mid:]
newnode.childList=ChildList(n1.childList[mid:])
newnode.par=n1.par
for c in newnode.childList:
c.par=newnode
if n1.par is None: #如果当前节点是根节点,则创建一个新的根节点
newroot=Bptree_InterNode(self.M)
#tipInfo(' #%s 号内部节点分裂,键 %s 将复制(上升)到新的根节点 #%s 中'%(n1.id,n1.indexList[mid-1],newroot.id))
newroot.indexList=[n1.indexList[mid-1]]
newroot.childList=ChildList([n1,newnode])
n1.par=newnode.par=newroot
self.__root=newroot
else: #如果当前节点不是根节点
#tipInfo(' #%s 号内部节点分裂,键 %s 将复制(上升)到父节点 #%s 中'%(n1.id,n1.indexList[mid-1],n1.par.id))
i=n1.par.childList.index(n1)
n1.par.indexList.insert(i,n1.indexList[mid-1])
n1.par.childList.insert(i+1,newnode)
n1.indexList=n1.indexList[:mid-1]
n1.childList=ChildList(n1.childList[:mid])
return n1.par
#叶子节点分裂
def split_leaf(n2):
mid=(self.L+1)/2 #分裂点为叶子节点度数+1的中点
newleaf=Bptree_Leaf(self.L)
newleaf.kvList=n2.kvList[mid:]
if n2.par==None: #如果当前节点是既是叶子节点又是根节点,则创建一个新的内部节点
newroot=Bptree_InterNode(self.M)
#tipInfo(' #%s 号叶子节点分裂,键 %s 将复制(上升)到新的根节点 #%s 中'%(n2.id,n2.kvList[mid].key,newroot.id))
newroot.indexList=[n2.kvList[mid].key]
newroot.childList=ChildList([n2,newleaf])
n2.par=newroot
newleaf.par=newroot
self.__root=newroot
else:
#tipInfo(' #%s 号叶子节点分裂,键 %s 将复制(上升)到父节点 #%s 中'%(n2.id,n2.kvList[mid].key,n2.par.id))
i=n2.par.childList.index(n2)
n2.par.indexList.insert(i,n2.kvList[mid].key)
n2.par.childList.insert(i+1,newleaf)
newleaf.par=n2.par
n2.kvList=n2.kvList[:mid]
n2.bro=newleaf
#插入节点
def insert_node(n):
#tipInfo('对 #%s 号节点进行检查 '%(n.id))
if not n.isleaf():
#tipInfo(' #%s 号节点是内部节点 '%(n.id))
if n.isfull():
#tipInfo(' #%s 号节点已满,分裂后再做插入操作 '%(n.id))
insert_node(split_node(n))
else:
p=bisect_right(n.indexList,key_value)
#tipInfo(' 插入位置:%s '%p)
# pp = 0 if p == 0 else p - 1
# if p > 0:
# tipInfo(' #%s 号节点未满,找到稍小于 %s 的键值 %s ,在 %s 的右孩子 #%s 号节点上执行插入操作'%(n.id,key_value.key,n.indexList[pp],n.indexList[pp],n.childList[p].id))
# else:
# tipInfo(' #%s 号节点未满,只能找到比 %s 稍大的键值 %s ,在 %s 的左孩子 #%s 号节点上执行插入操作'%(n.id,key_value.key,n.indexList[pp],n.indexList[pp],n.childList[p].id))
insert_node(n.childList[p])
else:
#tipInfo(' #%s 号节点是叶子节点, 实际插入键值与卫星数据 '%(n.id))
#p=bisect_right(n.kvList,key_value)
p=bisect_left(n.kvList,key_value)
if len(n.kvList) > p and n.kvList[p]==key_value:
#发现了重复键值
n.kvList[p].appendValue(key_value.value)
else:
n.kvList.insert(p,key_value)
if n.isfull():
#tipInfo(' #%s 号叶子节点已满, 分裂该节点 '%(n.id))
split_leaf(n)
else:
return
insert_node(self.__root)
#删除操作
def delete(self,key_value):
def merge(n,i):
if n.childList[i].isleaf():
n.childList[i].kvList=n.childList[i].kvList+n.childList[i+1].kvList
n.childList[i].bro=n.childList[i+1].bro
else:
n.childList[i].indexList=n.childList[i].indexList+[n.indexList[i]]+n.childList[i+1].indexList
n.childList[i].childList=mergeChildList(n.childList[i].childList,n.childList[i+1].childList)
n.childList.remove(n.childList[i+1])
n.indexList.remove(n.indexList[i])
if n.indexList==[]:
n.childList[0].par=None
self.__root=n.childList[0]
del n
return self.__root
else:
return n
def mergeChildList(c1,c2):
return ChildList(c1._childList+c2._childList)
def tran_l2r(n,i):
if not n.childList[i].isleaf():
n.childList[i+1].childList.insert(0,n.childList[i].childList[-1])
n.childList[i].childList[-1].par=n.childList[i+1]
n.childList[i+1].indexList.insert(0,n.indexList[i])
n.indexList[i]=n.childList[i].indexList[-1]
n.childList[i].childList.pop()
n.childList[i].indexList.pop()
else:
n.childList[i+1].kvList.insert(0,n.childList[i].kvList[-1])
n.childList[i].kvList.pop()
n.indexList[i]=n.childList[i+1].kvList[0].key
def tran_r2l(n,i):
if not n.childList[i].isleaf():
n.childList[i].childList.append(n.childList[i+1].childList[0])
n.childList[i+1].childList[0].par=n.childList[i]
n.childList[i].indexList.append(n.indexList[i])
n.indexList[i]=n.childList[i+1].indexList[0]
n.childList[i+1].childList.remove(n.childList[i+1].childList[0])
n.childList[i+1].indexList.remove(n.childList[i+1].indexList[0])
else:
n.childList[i].kvList.append(n.childList[i+1].kvList[0])
n.childList[i+1].kvList.remove(n.childList[i+1].kvList[0])
n.indexList[i]=n.childList[i+1].kvList[0].key
def del_node(n,kv):
tipInfo('对 #%s 号节点进行检查 '%(n.id))
if not n.isleaf():
#tipInfo(' #%s 号节点不是叶子节点 '%(n.id))
p=bisect_right(n.indexList,kv)
if p==len(n.indexList):
tipInfo(' #%s 号节点中的键值都小于 %s ,%s 只可能位于最右孩子中'%(n.id,kv.key,kv.key))
if not n.childList[p].isempty():
tipInfo(' #%s 号节点中的最右孩子丰满 '%(n.id))
return del_node(n.childList[p],kv)
elif not n.childList[p-1].isempty():
tipInfo(' #%s 号节点中的最右孩子不丰满,但最右孩子的左邻兄弟丰满 向右转移数据 '%(n.id))
tran_l2r(n,p-1)
return del_node(n.childList[p],kv)
else:
tipInfo(' #%s 号节点中的最右孩子不丰满,最右孩子的左邻兄弟也不丰满,先合并再做删除操作 '%(n.id))
return del_node(merge(n,p-1),kv)
else:
tipInfo(' #%s 号节点中有键值 %s 稍大于 %s 接下来检查 %s 的左孩子'%(n.id,n.indexList[p],kv.key,n.indexList[p]))
if not n.childList[p].isempty():
tipInfo(' %s 的左孩子丰满 '%(n.indexList[p]))
return del_node(n.childList[p],kv)
elif not n.childList[p+1].isempty():
tipInfo(' %s 的左孩子不丰满,右孩子丰满 向左转移数据'%(n.indexList[p]))
tran_r2l(n,p)
return del_node(n.childList[p],kv)
else:
tipInfo(' %s 的左孩子不丰满,右孩子也不丰满 先合并再做删除操'%(n.indexList[p]))
return del_node(merge(n,p),kv)
else:
#tipInfo(' #%s 号节点是叶子节点 '%(n.id))
p=bisect_left(n.kvList,kv)
try:
pp=n.kvList[p]
except IndexError:
return -1
else:
if pp!=kv:
tipInfo('在 #%s 号节点中没有发现 %s 删除结束 '%(n.id,kv.key))
return -1
else:
tipInfo('在 #%s 号节点中发现了 %s ,该节点是叶子节点,直接删除 '%(n.id,kv.key))
n.kvList.remove(kv)
return 0
del_node(self.__root,key_value)
#------------------ B+ 树 定义结束 ----------------------------------------
def tipInfo(str):
global infoLevel
if infoLevel and infoLevel < 2:
println(str)
def debug(value,tip=''):
global infoLevel
if infoLevel and infoLevel < 1:
print('\n
****** debug ****** %s *******'%tip)
print(value)
print('\n
')
def println(str):
print('\n
')
print(str)
print('\n
')
#固定序列
def fixTestList():
testlist=[]
keyList =[10, 17, 9, 33, 33, 50, 36, 41, 31, 30, 13, 6, 37, 45, 20, 4, 35, 11, 2, 40]
#keyList =[2, 2, 2, 2, 2, 2]
#keyList =[10, 17, 9, 33, 33, 50]
#通常情况下树高为3,下面这个序列会生成树高为4的B+树,可以分析一下为什么会这样?
#keyList =[3, 33, 25, 30, 15, 27, 16, 35, 28, 39, 44, 2, 47, 45, 14, 42, 18, 3, 9, 18, 34, 19, 33, 46, 24, 45, 48, 20, 10, 8, 35, 3, 49, 48, 50, 9, 46, 1, 31, 6, 37, 34, 33, 37, 6, 48, 39, 24, 17]
for key in keyList:
value=choice('abcdefghijklmn')
testlist.append(KeyValue(key,value))
tipInfo(str([k.key for k in testlist]))
return testlist;
#随机序列,用50个数的随机序列生成B+树时,观察生成的图形,大部分时候树高都是3,
#但偶尔出现树高为4,是因为B+树在面对特定数据时树高会高一些吗?
def randTestList():
testlist=[]
for i in range(1,100):
key=randint(1,100)
value=choice('abcdefghijklmn')
testlist.append(KeyValue(key,value))
tipInfo(str([k.key for k in testlist]))
return testlist;
#测试插入操作
def testInsert():
M=4
L=4
#构造一个空的B+树
mybptree=Bptree(M,L)
println('B+树的插入过程, 内部%s阶,叶子%s阶 '%(M,L))
tipInfo('插入序列')
testlist = fixTestList()
#testlist = randTestList()
for kv in testlist:
tipInfo('
------------ 准备插入 %s : -------------------------------'%kv.key)
mybptree.insert(kv)
tipInfo('插入 %s 后的B+树'%kv.key)
mybptree.show()
#导出B+树
mybptree.dump()
#测试删除操作
def testDelete():
M=4
L=4
# M=6
# L=6
#构造一个空的B+树
mybptree=Bptree(M,L)
println('插入B+树, 内部%s阶,叶子%s阶 '%(M,L))
tipInfo('插入序列')
testlist = fixTestList()
#testlist = randTestList()
for kv in testlist:
mybptree.insert(kv)
mybptree.show()
println('删除序列')
testlist = fixTestList()
println('提示:以下所说的位置,序号等等皆从0开始计数,请特别留意!!!')
for x in testlist:
println('
------------ 准备删除 %s : -------------------------------'%x.key)
mybptree.delete(x)
println('删除 %s 后的B树:'%x.key)
mybptree.show()
if __name__=='__main__':
testDelete()