前言
最近在复习堆排序的内容,发现基于数组的堆虽然用起来很方便,但打印不方便。所以本文实现了一个简单美观的打印一颗基于数组的完全二叉树的算法(堆就是一种完全二叉树嘛,但实现最小堆一般是基于数组的)。
算法思路
打印的层分为数字层和指针层:
- 指针层就是
/ \ / \
- 数字层就是
5 43 7 n
。(PS:n代表无节点)
示例打印:
1
/ \
22 11
/ \ / \
5 43 7 n
先构建最后一层(数字层)的字符串,因为它的宽度是整个打印二叉树的最大宽度,需要最先得到最大宽度。构建最后一层时,不考虑每个数字是否为多位,直接每个数字之间间隔3个空格(注意,3这个数字是可以调节的)。而且,如果考虑多位数字从而减少后面的空格,反而不好控制也不美观,最关键的是,如果是4位数字,那这个4位数字与后面的数字就没有间隔了。
构建好最后一层数字层后,从下往上接着构建。接下来的每层的字符串的长度也都是之前得到的最大宽度,初始化时,每层字符串都是长度为最大宽度的全是空格的字符串。
构建指针层时,总是以下面最接近的数字层为标准。比如示例打印的倒数第二层。
- 它的第一个
/
是根据5来的,具体的说,因为5是一个左孩子,我们获得5在该层字符串中的索引index,然后令本层的index+1的字符变成/
。 - 同理,它的第一个
\
是根据43来的,但这里有所不同,我们关注的是43的第一个字符的索引index。然后令本层的index-1的字符变成\
,因为43是一个右孩子。
构建数字层(这里指除了最后一层的数字层)时,也总是以下面最接近的数字层为标准。比如示例打印的倒数第三层。
- 它的22的第一个2的位置是根据它的左孩子和右孩子的位置来的。它的左孩子5在字符串中的索引为0,右孩子43在字符串中的索引为4(同样,只看第一个字符的位置)。那么22的位置就是
(0+4)/2
得到一个整数index,然后将22放置到字符串的index位置(注意,就算是多位数字,也是把多位数字的第一位放到index,多位数字的其他位也依次放置)。
最重要在于,总是以下面最接近的数字层为标准来构建每层的字符串,这样指针总是刚好靠近孩子,父节点总是在孩子节点的中间位置(因为是整除,可能有时稍有点偏,但完全不影响美观)。
算法实现
data = [1,22,11,5,43,7,1,36,42,43,1,6,14,22333,3,4,5,6,7,81]
def replace(s, rep, index):
#替换s的从index开始的部分字符串,替换长度为len(rep)
prev = s[0:index]
next = s[index+len(rep):len(s)]
return prev + rep + next
def getLevelAssign(List, limit):
#观察范围是[0, limit]的完全二叉树tree,
#然后得到tree对应的满二叉树的每层的信息
#包括 每层开始节点的所在数组索引levelStart
# 每层结束节点的所在数组索引+1 levelLimit
# 每层节点个数(虽然没用到)
assign = []
levelStart = 0
levelCount = 1 #每层节点的个数,2 的幂
levelLimit = 1 #每层节点索引的限制
while(levelStart < limit):
#基本信息弄成一维数组,放到一个二维数组中,以栈的方式
assign.insert(0, [levelStart, levelLimit, levelCount])
levelCount = levelCount << 1
levelStart = levelCount - 1
levelLimit += levelCount
# 返回时,assign[0]是满二叉树最后一层的信息,assign[1]是满二叉树倒数第二层的信息
return assign
def printHeap(List, limit):
assign = getLevelAssign(List,limit)#得到了每个数字层的信息
maxLevelLen = 0
maxLevelBlankNumber = 3 #此处可调节。代表最后一层的数字之间的空格数
printLi = []
lastLevel = True
for ass in assign:
#构建数字层
levelStr = "" #代表数字层
if lastLevel: #如果是最后一层(第一次循环当然是最后一层)
for i in range(ass[0],ass[1]):
if i < limit:
levelStr += str(List[i])+" "*maxLevelBlankNumber
else:
levelStr += "n"+" "*maxLevelBlankNumber
levelStr = levelStr[:-maxLevelBlankNumber]
maxLevelLen = len(levelStr) #得到最大宽度
lastLevel = False
else: # 如果不是最后一层的数字层
#下面最接近的数字层在printLi[1],因为隔了一个指针层
levelStr = " "*maxLevelLen
takeIndex = ass[0] #从原始数组中取数字的开始索引
# count代表在最接近的数字层遇到的孩子节点的次数
# left 代表某连续两个左右孩子的左孩子的索引 (指左孩子字符串在它所在层的构建字符串中的索引)
# right代表某连续两个左右孩子的右孩子的索引
count = left = right = 0
for index in range(len(printLi[1])):
#当遇到连续数字字符串中第一个数字时
if printLi[1][index] != " " and (index == 0 or printLi[1][index-1] == " "):
#左移存储left right
left = right
right = index
count += 1
#当遇到了偶数次孩子时,我们就可以为本层数字层加入数字字符串了
if count != 0 and count%2 == 0:
middle = int((left + right)/2)#取得一个中间位置
levelStr = replace(levelStr, str(List[takeIndex]), middle)
takeIndex += 1
printLi.insert(0, levelStr)
#构建完数字层,需要构建数字层之上的指针层,第一层数字层不需要,因为它是根节点
if ass[0] != 0:#不是第一层
pointerStr = " "*maxLevelLen
left = True#先遇到的一定是左孩子
# printLi[0]是下面最接近的数字层
for charIndex in range(len(printLi[0])):
#当遇到连续数字字符串中第一个数字时,添加本层的指针
if printLi[0][charIndex] != ' ' and (charIndex == 0 or printLi[0][charIndex-1] == " "):
if left == True:#左孩子,那么本层添加一个/
pointerStr = replace(pointerStr, "/", charIndex+1)
else: #右孩子,那么本层添加一个\
pointerStr = replace(pointerStr, "\\", charIndex-1)
left = not left #置反
printLi.insert(0, pointerStr) #栈的方式添加
for i in printLi:
print(i)
printHeap(data,len(data))
打印效果如下:
1
/ \
22 11
/ \ / \
5 43 7 1
/ \ / \ / \ / \
36 42 43 1 6 14 22333 3
/ \ / \ / \ / \ / \ / \ / \ / \
4 5 6 7 81 n n n n n n n n n n n
其他
printHeap
的第二个参数可以随意调整,只要在[0, len(data)]
的范围内就行。
如果有位数很多的数字时,则可能需要把maxLevelBlankNumber
调大。