引言
平衡二叉树的基本性质为许多人所熟知,简单说来就是“左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树”。
平衡二叉树为了保证其平衡性,需要在插入或删除节点时动态地调整树节点,使其一直保持平衡。在网上查到一篇文献《平衡二叉树(AVL树)》。该文作者用python实现了平衡二叉树的基本操作。唯一有点遗憾的是他的输出结果是文本形式的,不方便直观地查看插入和删除操作的效果。
本文在原作者python代码的基础上做了一些改进,实现了图形化展示的效果。
基本工作原理
实现步骤
1.在进行二叉树遍历时,动态地生成graphviz图形源文件dot语言格式的顶点与边
以下为遍历时生成dot片断的代码
def traversal(node,tree):
if not node:
return
traversal(node.lchild,tree)
#print(node.value, 'height=', node.height)
tree.dotStr += 'node%s [label = "%s" ];\n'% (node.value,node.value)
traversal(node.rchild,tree)
if node.lchild:
tree.dotStr +='node%s:s-> node%s:n;\n'% (node.value,node.lchild.value)
if node.rchild:
tree.dotStr +='node%s:s-> node%s:n;\n'% (node.value,node.rchild.value)
访问每一个节点时将生成类似于下面的dot文件格式的片断
node3 [label = "3" ];
node2:s-> node1:n;
node2:s-> node3:n;
对graphviz 的dot语言比较熟悉的人可以一眼看出。以上三句话定义了一个顶点,两条有向边。
2.将遍历得到的dot图形的片断与graph的基本定义片断组合成一个完整的dot格式的源文件
主要代码如下
dotHeader ='''
digraph G
{
rankdir = TB;
node [shape=circle];
'''
dotStr = dotHeader + self.dotStr +'}'
dotFile =open('avlTree.dot','w')
dotFile.write(dotStr)
dotFile.close()
以下是由python自动生成的11个节点的平衡二叉树的dot源文件:avlTree.dot
digraph G
{
rankdir = TB;
node [shape=circle];
node1 [label = "1" ];
node2 [label = "2" ];
node3 [label = "3" ];
node2:s-> node1:n;
node2:s-> node3:n;
node4 [label = "4" ];
node5 [label = "5" ];
node6 [label = "6" ];
node7 [label = "7" ];
node6:s-> node5:n;
node6:s-> node7:n;
node8 [label = "8" ];
node10 [label = "10" ];
node18 [label = "18" ];
node99 [label = "99" ];
node18:s-> node10:n;
node18:s-> node99:n;
node8:s-> node6:n;
node8:s-> node18:n;
node4:s-> node2:n;
node4:s-> node8:n;
}
- 通过dot命令将上一步生成的图形源文件转换为svg源文件
主要代码如下
#调用dot命令生成svg文件
os.system('dot -Tsvg avlTree.dot -o avlTree.html')
通过python的os模块执行命令行,由avlTree.dot文件生成了svg图形文件avlTree.html
4.在每插入或删除一个节点时,动态地生成代表平衡二叉树的svg图形文件,并在浏览器页面上显示。
实际效果如下:
完整的python代码
# -*- coding:utf-8 -*-
# 平衡二叉树的图形化展示代码,
# 原作者:zhipingChen https://www.jianshu.com/p/655d83f9ba7b
# 修改成用graphviz图形化显示,修改者:littleZhuHui
import os
# tree node definition
class Node(object):
def __init__(self, value, height=0, lchild=None, rchild=None):
self.lchild = lchild
self.rchild = rchild
self.value = value
self.height = height
# tree definition
class Tree(object):
def __init__(self, root=None):
self.root = root
# node in-order traversal(LDR)
def traversal(self):
self.dotStr=''
traversal(self.root,self)
#print self.dotStr
# insert node
def insert(self, value):
self.root = insert(self.root, value)
# delete node
def delete(self, value):
self.root = delete(self.root, value)
# 生成svg图形对应的文本串
def svgStr(self):
dotHeader ='''
digraph G
{
rankdir = TB;
node [shape=circle];
'''
dotStr = dotHeader + self.dotStr +'}'
dotFile =open('avlTree.dot','w')
dotFile.write(dotStr)
dotFile.close()
#调用dot命令生成svg文件
os.system('dot -Tsvg avlTree.dot -o avlTree.html')
#取出svg图形文件的内容
svgFile =open('avlTree.html','r')
svgStr = svgFile.read()
svgFile.close()
return svgStr
# node in-order traversal(LDR)
def traversal(node,tree):
if not node:
return
traversal(node.lchild,tree)
#print(node.value, 'height=', node.height)
tree.dotStr += 'node%s [label = "%s" ];\n'% (node.value,node.value)
traversal(node.rchild,tree)
if node.lchild:
tree.dotStr +='node%s:s-> node%s:n;\n'% (node.value,node.lchild.value)
if node.rchild:
tree.dotStr +='node%s:s-> node%s:n;\n'% (node.value,node.rchild.value)
def println(str):
print('\n<br>')
print(str)
print('\n<br>')
# insert node
'''
the recursive insert operation has a flaw,
that the binary tree will recursively updates the height of the parent node
even if the inserted element already exists.
'''
def insert(root, value):
if not root:
return Node(value)
if value < root.value:
root.lchild = insert(root.lchild, value)
elif value > root.value:
root.rchild = insert(root.rchild, value)
return checkBalance(root)
# check whether the tree is balanced
def checkBalance(root):
if not root:
return None
if abs(heightDiffL2R(root)) < 2: # the tree is balance
updateHeight(root)
elif heightDiffL2R(root) == 2: # situation L
if heightDiffL2R(root.lchild) == -1: # situation LR
root.lchild = rotateR2L(root.lchild)
root = rotateL2R(root)
else: # situation LL
root = rotateL2R(root)
elif heightDiffL2R(root) == -2: # situation R
if heightDiffL2R(root.rchild) == 1: # situation RL
root.rchild = rotateL2R(root.rchild)
root = rotateR2L(root)
else: # situation RR
root = rotateR2L(root)
return root
# get the height difference between left-child and right-child
def heightDiffL2R(node):
if node.lchild and node.rchild:
return node.lchild.height - node.rchild.height
if node.lchild:
return node.lchild.height + 1
if node.rchild:
return -(node.rchild.height + 1)
return 0
# update the height of the node
def updateHeight(root):
if root.lchild and root.rchild:
root.height = max(root.lchild.height, root.rchild.height) + 1
elif root.lchild:
root.height = root.lchild.height + 1
elif root.rchild:
root.height = root.rchild.height + 1
else:
root.height = 0
# rotate from left to right with the left-child node as the axis
def rotateL2R(node):
leftChild = node.lchild
leftChild.rchild, node.lchild = node, leftChild.rchild
updateHeight(node)
updateHeight(leftChild)
return leftChild
# rotate from right to left with the right-child node as the axis
def rotateR2L(node):
rightChild = node.rchild
rightChild.lchild, node.rchild = node, rightChild.lchild
updateHeight(node)
updateHeight(rightChild)
return rightChild
# 删除指定节点
def delete(root, value):
if not root:
return None
if root.value > value:
root.lchild = delete(root.lchild, value)
elif root.value < value:
root.rchild = delete(root.rchild, value)
else:
if root.lchild and root.rchild: # degree of the node is 2
target = transferDeleteNode(root)
root = delete(root, target)
root.value = target
else: # degree of the node is [0|1]
root = root.lchild if root.lchild else root.rchild
return checkBalance(root)
# find the maximum node or the minimum node in the tree
def transferDeleteNode(node):
if node.rchild.height > node.lchild.height:
target = node.rchild
while target.lchild:
target = target.lchild
else:
target = node.lchild
while target.rchild:
target = target.rchild
return target.value
if __name__ == '__main__':
#arr = [5, 3, 4, 0, 2, 1, 8, 6, 9, 7,7]
#arr = [1, 2,3, 4, 5, 6, 7, 8, 6, 9, 10]
arr = [10, 3, 4, 5, 2, 1, 8, 6, 99, 7,18]
println('根据如下数据')
print(arr)
println('生成平衡二叉树:')
T = Tree()
println('开始插入节点 ......')
for i in arr:
println('插入节点 [ %d ]之后的平衡二叉树:'%i)
T.insert(i)
T.traversal()
print(T.svgStr())
println('开始删除节点 ......')
for i in arr[::-1]:
println('删除节点 [ %d ]之后的平衡二叉树:'%i)
T.delete(i)
T.traversal()
print(T.svgStr())
补充说明
先将以上所示的python代码保存在某个目录下,命名为avlTree.py
为了检查图形效果,有两种方式可以实现
方法1,在终端窗口执行如下的命令: python avlTree.py >res.html
将会在avlTree.py所在的目录下生成res.html文件,双击此html文件,将会在浏览器中打开,就可以看到图形化的效果。
在此之前应确保python.exe所在的路径已经加入到操作系统的环境变量path中了。
方法2,如果每次修改了python源文件后都要在命令行中执行一次命令,并不是很方便。一个更巧妙的办法用apache服务器执行python脚本,直接在浏览器中查看图形化的效果。apache一般与php配合使用。这里是python脚本,该怎么办呢?
其实很简单,写一个一句话的php脚本就可以了
avlTree.py.php
passthru('python avlTree.py');
这个php脚本什么别的也不干,就是调用python.exe执行avlTree.py这个脚本。
关于将本来在命令行执行的脚本转为在浏览器上执行的详细过程,可以参见我以前的一篇文章《命令行执行php改成通过浏览器执行,自动实现图形菜单功能》
假定你已经将上述两个文件 avlTree.py与 avlTree.py.php放在了某个目录的avl目录下,并在apache中修改了配置,将一个虚拟目录指向了这个目录。那么在浏览器的地址栏输入如下的内容:
http://localhost:8080/graphWork/avl/
应该就可以看到如下图所示的效果
用鼠标点击" avlTree.py.php"应该就可以看到图形效果:
可能有的人觉得方法2太麻烦,还不如方法1简便,但实际上apache的配置是一次性的,如果在文本编辑器中修改了python源代码。比如要在树中多插入一个节点[20],只需修改python源代码,将第178行的
arr = [10, 3, 4, 5, 2, 1, 8, 6, 99, 7,18]
修改为
arr = [10, 3, 4, 5, 2, 1, 8, 6, 99, 7,18,20]
然后用鼠标点击浏览器页面上的" avlTree.py.php",就可以立即看到图形化的效果,不用再转去命令行执行python命令。
如果要不断地修改python源文件并查看修改后的效果,方法2的简便性就体现出来了。