引言
在前一篇文章《平衡二叉树插入及删除操作的图形化展示,python调用graphviz自动生成svg图形》用图形化的方式展示了平衡二叉树在插入或删除时图形的变化。现在来直观演示一下B树的插入过程
B树节点中的元素有序排列,并且个数有上限,当超过上限时,节点将会发生分裂,如果B树的根节点发生分烈,B树的高度就会加1。
B树通过分裂的方式,保证所有的叶子节点都在同一个层次上,实际上保证了的B树的平衡性。这是一个很巧妙的设计,我们接下来将用图形化演示的方式来揭示其奥妙所在。
在网上查到一篇文献《关于Btree》。该文作者用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] 生成一棵3度B树的图示,请留意对节点分裂的提示
其它序列的插入过程的图形效果,可以将代码下载到本地后自已手动调整源码,然后在浏览器上查看图形效果。当然前提是你的电脑上已经安装好了python与graphviz。
完整的python代码
# -*- coding:utf-8 -*-
# B树的图形化展示代码,
# 原作者:thursdayhawk https://blog.51cto.com/thuhak/1261783
# 修改成用graphviz图形化显示,修改者:littleZhuHui
import os
from random import randint,choice
from bisect import bisect_left
from collections import deque
idSeed = 0
#生成一个全局ID
def getGID():
global idSeed
idSeed+=1
return idSeed
class InitError(Exception):
pass
class ParaError(Exception):
pass
# 定义键值对
class KeyValue(object):
__slots__=('key','value')
def __init__(self,key,value):
self.key=key
self.value=value
def __str__(self):
return str((self.key,self.value))
def __cmp__(self,key):
if self.key>key:
return 1
elif self.key==key:
return 0
else:
return -1
#B树节点
class BtreeNode(object):
def __init__(self,t,parent=None):
if not isinstance(t,int):
raise InitError,'degree of Btree must be int type'
if t<2:
raise InitError,'degree of Btree must be equal or greater then 2'
else:
self.keyList=[]
self.childList=[]
self.parent=parent
self.__degree=t
#每个节点有一个唯一的整数值做为id,方便用graphviz绘图
self.id = getGID()
def __str__(self):
return str([k.key for k in self.keyList])
@property
def degree(self):
return self.__degree
def isleaf(self):
return len(self.childList)==0
#遍历节点对应的子树,深度优先遍历,返回树上所有节点上的所有(key,value)组成的列表
# 注意 keyList 中存放的不是整数,而是 KeyValue 对象,但是这个KeyValue对象是可以与整数比较大小的,见KeyValue的定义
def traversal(self):
result=[]
def get_value(n):
if n.childList==[]:
result.extend(n.keyList)
else:
for i,k in enumerate(n.keyList):
get_value(n.childList[i])
result.append(k)
#childList比keyList多一个,要处理一下最右孩子指针
get_value(n.childList[-1])
get_value(self)
return result
#显示树上全部的节点,广度优先遍历
def show(self,tree):
q=deque()
h=0
q.append([self,h])
#生成childList对应的dot格式的文本串
def childListDotStr(n):
dotStr ='{'
if n.childList==[]:
return '{}'
else:
for i,k in enumerate(n.keyList):
dotStr +='<f%s>#%s|'%(n.childList[i].id,n.childList[i].id)
#childList比keyList多一个,要处理一下最右孩子指针
dotStr +='<f%s>#%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.keyList):
dotStr +='node%s:f%s:s--node%s:e:n;\n'% (n.id,n.childList[i].id,n.childList[i].id)
#childList比keyList多一个,要处理一下最右孩子指针
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:
#print 'id:',node.id,[k.key for k in node.keyList],'level:',height
#print 'id:',node.id,str(node),'level:',height
tree.dotStr += 'node%s [label = "{<e> #%s|%s| %s}" ];\n'% (node.id,node.id,str(node),childListDotStr(node))
tree.dotStr += childListEdgeStr(node)
if node.childList==[]:
continue
else:
if height==h:
h+=1
q.extend([[n,h] for n in node.childList])
#取得节点对应的子树上的最大值(key的最大值)
def getmax(self):
n=self
while not n.isleaf():
n=n.childList[-1]
return (n.keyList[-1],n)
#取得节点对应的子树上的最小值(key的最小值)
def getmin(self):
n=self
while not n.isleaf():
n=n.childList[0]
return (n.keyList[0],n)
#------------------ node 定义结束 ----------------------------------------
#B树类
class Btree(object):
def __init__(self,t):
self.__degree=t
self.__root=BtreeNode(t)
@property
def degree(self):
return self.__degree
def traversal(self):
"""
use dfs to search a btree's node
"""
return self.__root.traversal()
def show(self):
"""
use bfs to show a btree's node and its height
"""
self.dotStr=''
self.__root.show(self)
print(self.svgStr())
# 生成svg图形对应的文本串
def svgStr(self):
dotHeader ='''
graph G
{
rankdir = TB;
node [shape=record];
'''
dotStr = dotHeader + self.dotStr +'}'
dotFile =open('BTree.dot','w')
dotFile.write(dotStr)
dotFile.close()
#调用dot命令生成svg文件
os.system('dot -Tsvg BTree.dot -o BTree.html')
#取出svg图形文件的内容
svgFile =open('BTree.html','r')
svgStr = svgFile.read()
svgFile.close()
return svgStr
#将一个key_value对象插入B树
def insert(self,key_value):
#节点中元素个数不能超过2*度数-1
full=self.degree*2-1
mid=full/2+1
#将节点n分裂
def split(n):
new_node=BtreeNode(self.degree,parent=n.parent)
#新节点是原节点列表的后半部分
new_node.keyList=n.keyList[mid:]
new_node.childList=n.childList[mid:]
#重置新节点的所有孩子节点的父指针
for c in new_node.childList:
c.parent=new_node
if n.parent is None:
#如果n是B树的根节点
#构造一个新的根节点
newroot=BtreeNode(self.degree)
#新的根节点的keyList中添加原节点的"中点"
newroot.keyList=[n.keyList[mid-1]]
#新的根节点将拥有n与n分裂出来的兄弟,做为其两个孩子
newroot.childList=[n,new_node]
#重置n与n分裂出来的兄弟的父指针
n.parent=new_node.parent=newroot
#将新的根节点做为B树的根节点
self.__root=newroot
else:
#如果n并不是B树的根节点
#找到n节点在其父节点的孩子列表中的下标
i=n.parent.childList.index(n)
#n的父节点的keyList中添加原节点的"中点"
n.parent.keyList.insert(i,n.keyList[mid-1])
#n的父节点的孩子列表中增加对新节点的孩子指针
n.parent.childList.insert(i+1,new_node)
#原节点只保留原来的列表的前半部分
n.keyList=n.keyList[:mid-1]
n.childList=n.childList[:mid]
#返回新(旧)节点的共同的父节点
return n.parent
#在节点n所对应的子树上进行插入(此函数可能会递归调用)
def insert_node(n):
#如果节点n中的元素个数已经满了,将节点n分列,从新的子树的根节点开始进入插入(递归调用)
if len(n.keyList)==full:
println('#%s 号节点元素个数已经有 %s 个,应该分裂为两个节点'%(n.id,full))
insert_node(split(n))
else:
#如果节点n的keyList为空,则直接将key_value对象加入keyList(keyList不仅仅是存放了key,其实还存放了value)
#只有叶节点的keyList才有可能为空,
if n.keyList==[]:
n.keyList.append(key_value)
else:
if n.isleaf():
#如果是叶子节点,这时元素个数又未满,则有序插入keyList
# 实际的插入操作永远发生在叶节点,只有在叶节点未满或为空时才会发生实际的插入操作
p=bisect_left(n.keyList,key_value) #locate insert point in ordered list keyList
n.keyList.insert(p,key_value)
else:
#如果是非叶节点,找到应该插入的子树,对子树进行插入操作(递归调用)
p=bisect_left(n.keyList,key_value)
insert_node(n.childList[p])
#从B树的根节点开始插入操作
insert_node(self.__root)
#------------------ Btree 定义结束 ----------------------------------------
def println(str):
print('\n<br>')
print(str)
print('\n<br>')
#固定序列
def fixTestList():
testlist=[]
keyList =[10, 17, 9, 33, 33, 50, 36, 41, 31, 30, 13, 6, 37, 45, 20, 4, 35, 11, 2, 40]
for key in keyList:
value=choice('abcdefghijklmn')
testlist.append(KeyValue(key,value))
println('待插入序列')
println(str([k.key for k in testlist]))
return testlist;
#测试插入操作
def testInsert():
#B树阶数
degree = 3
#节点中元素个数上限
full=degree*2-1
#构造一棵空树
mybtree=Btree(degree)
println('%s度B树的插入过程,节点中元素不应超过 %s 个'%(degree,full))
testlist = fixTestList()
for x in testlist:
mybtree.insert(x)
println('插入键 %s 之后的B树'%x.key)
mybtree.show()
if __name__=='__main__':
testInsert()
由于还对graphviz生成了svg图形做美化处理,如果想得到与美化svg相关的代码,还请参看以前的文章《用jquery对graphviz生成的svg图形做后处理,改变字体,颜色,连线形状等》。
用本文提供的新的BTree.py文件替换该篇文章中的BTree.py按该篇文章所说的环境拱建过程进行操作,应该就可以看到完整的效果了。