(本文代码放到了 自己的网站)
我们从二叉树的顺序存储说起。
我们知道,对于一个完全二叉树,做广度优先搜索的话,每个节点对应的序号是固定的。
例如,下面这个二叉树
这个二叉树和一个顺序存储相互可以唯一表示的:
[2, 1, 3, 0, 7, 9, 1]
给定二叉树,可以唯一确定这个顺序存储;反之亦然。
这么存储的好处是:寻址很快,例如,想知道二叉树第
行第
个节点的值,直接寻址
,复杂度 O(1)
插入、修改都挺快的,堆结构虽然是二叉树的原理,但实际上是存到顺序存储的
每个节点的孩子节点,也是立即可以确定的,例如,
号节点的两个孩子序号是
然后我们考虑一个非完全二叉树
我们用空值 None 来“填充”上面的二叉树,使之变成完全二叉树,也可以做类似的操作
也可以与一个顺序存储相互唯一表示:(没找到教科书上有命名,我称之为稀疏型顺序存储)
[2, 1, 3, 0, 7, 9, 1, 2, None, 1, 0, None, None,
8, 8, None, None, None, None, None, None, 7,
None, None, None, None, None, None, None,
None, None]
稀疏型顺序存储顾名思义,浪费了大量的存储空间。想做一下改进。
我们发现,修改一下映射规则:
None节点的子节点省略掉。
发现仍然实现 “这个二叉树和一个顺序存储相互可以唯一表示“ 的效果:(我称之为紧凑型顺序存储)
[2, 1, 3, 0, 7, 9, 1, 2, None, 1, 0, None, None, 8, 8, None, None, None, None, 7]
是不是短了很多!
前言完事儿了,下面是代码实现。
这段代码每一行都是反复修改的,兼顾优美、可扩展、可读性,是可以背下来的
我们用Python的类和对象来模拟每个节点
# Definition for a binary tree node.
class TreeNode(object):
def __init__(self, x):
self.val = x
self.left = None
self.right = None
def __repr__(self):
return 'Node val = {}'.format(str(self.val))
顺序存储转链式存储:
class Transform:
# 稀疏型顺序存储:二叉树的空节点也占一个位置,空节点的两个孩子(虽然实际不存在)也占一个位置
# 所以顺序表的第i个元素,其孩子节点序号必是 2 * i + 1, 2 * i + 2
# 紧凑型顺序存储:空节点占一个位置,但空节点的孩子不再占位置
def list2tree1_1(self, list_num, i=0):
# 顺序结构转稀疏型顺序存储,递归法
if i >= len(list_num):
return None
treenode = TreeNode(list_num[i])
treenode.left = self.list2tree1_1(list_num, 2 * i + 1)
treenode.right = self.list2tree1_1(list_num, 2 * i + 2)
return treenode
def list2tree1_2(self):
# 顺序结构转稀疏型顺序存储,迭代法
if not nums:
return None
nodes = [None if val is None else TreeNode(val) for val in nums]
kids = nodes[::-1]
root = kids.pop()
for node in nodes:
if node:
if kids: node.left = kids.pop()
if kids: node.right = kids.pop()
else:
if kids:
kids.pop()
if kids:
kids.pop()
return root
def list2tree2_2(self, nums):
# 顺序结构转紧凑顺序存储,迭代法
if not nums:
return None
nodes = [None if val is None else TreeNode(val) for val in nums]
kids = nodes[::-1]
root = kids.pop()
for node in nodes:
if node:
if kids: node.left = kids.pop()
if kids: node.right = kids.pop()
return root
链式存储上的遍历
class Travel:
# 注意,三个 DFS 算法中,空节点处理为[],而不是[None]
# 有些场景还是需要空节点返回[None]的,灵活去改动
def InOrder(self, root): # LDR
return [] if (root is None) else self.InOrder(root.left) + [root.val] + self.InOrder(root.right)
def PreOrder(self, root): # DLR
return [] if (root is None) else [root.val] + self.PreOrder(root.left) + self.PreOrder(root.right)
def PostOrder(self, root): # LRD
return [] if (root is None) else self.PostOrder(root.left) + self.PostOrder(root.right) + [root.val]
def level_order(self, root):
# BFS, tree转稀疏型顺序存储。
q, ret = [root], []
while any(q):
ret.extend([node.val if node else None for node in q])
q = [child for node in q for child in [node.left if node else None, node.right if node else None]]
return ret
def level_order2(self, root):
# BFS, tree转紧凑型顺序存储。
q, ret = [root], []
while any(q):
ret.extend([node.val if node else None for node in q])
q = [child for node in q if node for child in [node.left, node.right]]
# 结尾的 None 无意义,清除掉
while ret[-1] is None:
ret.pop()
return ret
def level_order_nary(self, root):
# 针对N-ary Tree的方法,非常漂亮,前面几个 level_order 都是参考这个
# https://leetcode.com/problems/n-ary-tree-level-order-traversal/description/
q, ret = [root], []
while any(q):
ret.append([node.val for node in q])
q = [child for node in q for child in node.children if child]
return ret
def find_track(self, num, root, track_str=''):
'''
二叉树搜索
'''
track_str = track_str + str(root.val)
if root.val == num:
return track_str
if root.left is not None:
self.find_track(num, root.left, track_str + ' ->left-> ')
if root.right is not None:
self.find_track(num, root.right, track_str + ' ->right-> ')
上面的代码每一行我都反复斟酌过,优美、可扩展,用来备考杠杠的。
解释上面的代码:InOrder,PreOrder,PostOrder 三种深度优先搜索,各种一行实现。好好看看里面的逻辑,大多数这一块的算法题在这上面改吧改吧就能做出来,答案还惊艳。
level_order就是广度优先搜索,对应的结果其实就是前言介绍的“稀疏型顺序存储”,“紧凑型顺序存储”
二叉树很多算法有递归版本和迭代版本。递归版本往往更优雅,迭代版本往往更容易实现一些高级需求(不是必然的)。例如题目要求你在 level_order 的基础上加一个“当前层级”作为输出结果,怎么改?(增加两行即可:定义计数器、计数器自加,非常easy)
下面给个打印二叉树的示例,可以看到上面代码如何轻易修改满足新需求
class Draw:
def print_tree(self, root, total_width=36):
q, ret, level = [root], '', 0
while any(q):
nodes = [str(node.val) if node else ' ' for node in q]
col_width = int(total_width / len(nodes))
nodes = [node_name.center(col_width, ' ') for node_name in nodes]
ret += (''.join(nodes) + '\n')
q = [child for node in q for child in [node.left if node else None, node.right if node else None]]
level += 1
return ret
def print_tree_horizontal(self, root, i=0):
'''
打印二叉树,凹入表示法,相当于把树旋转90度看。算法原理根据 RDL
'''
tree_str = ''
if root.right:
tree_str += self.print_tree_horizontal(root.right, i + 1)
if root.val:
tree_str += (' ' * i + '-' * 3 + str(root.val) + '\n')
if root.left:
tree_str += self.print_tree_horizontal(root.left, i + 1)
return tree_str
def drawtree(self, root):
# 用 turtle 画 Tree
def height(root):
return 1 + max(height(root.left), height(root.right)) if root else -1
def jumpto(x, y):
t.penup()
t.goto(x, y)
t.pendown()
def draw(node, x, y, dx):
if node:
t.goto(x, y)
jumpto(x, y - 20)
t.write(node.val, align='center', font=('Arial', 12, 'normal'))
draw(node.left, x - dx, y - 60, dx / 2)
jumpto(x, y - 20)
draw(node.right, x + dx, y - 60, dx / 2)
import turtle
t = turtle.Turtle()
t.speed(0)
turtle.delay(0)
h = height(root)
jumpto(0, 30 * h)
draw(root, 0, 30 * h, 40 * h)
t.hideturtle()
turtle.mainloop()
这里使用了3种打印方式:
transform = Transform()
travel = Travel()
draw = Draw()
nums = [2, 1, 3, 0, 7, 9, 1, 2, None, 1, 0, None, None, 8, 8, None, None, None, None, 7]
root = transform.list2tree2_2(nums)
bfs_res1 = travel.level_order(root)
bfs_res2 = travel.level_order2(root)
print(bfs_res1)
print(bfs_res2)
上面的代码从顺序结构构建一个二叉树。
然后又把这个二叉树转换成 稀疏型、紧凑型 顺序存储结构
打印结果:
[2, 1, 3, 0, 7, 9, 1, 2, None, 1, 0, None, None, 8, 8, None, None, None, None, None, None, 7, None, None, None, None, None, None, None, None, None]
[2, 1, 3, 0, 7, 9, 1, 2, None, 1, 0, None, None, 8, 8, None, None, None, None, 7]
看一下画二叉树:
第一种画法,用level_order 改编而来
draw1 = draw.print_tree(root)
print(draw1)
结果:
2
1 3
0 7 9 1
2 1 0 8 8
7
第二种画法,用深度优先搜索改编而来
draw2 = draw.print_tree_horizontal(root)
print(draw2)
结果:(相当于把树横过来)
---8
---1
---8
---3
---9
---2
---7
---7
---1
---1
---2
第三种(深度优先搜索)
draw.drawtree(root)
输出:
(本文代码放到了 自己的网站)
思考题:如果要求层次遍历返回形如下面的结果,如何修改代码(只要改一个单词)
[
[2],
[1,3],
[0,7,9,1],
[2,1,0,8,8],
[7]
]
2. level_order 稍微修改一下代码,可以计算树的最大/最小深度,试一试吧
3. level_order 稍微修改一下,可以计算树的最大宽度,试一试吧
4. LeetCode 经典题目:给定一个树,镜像翻转这个树。遍历算法稍微修改一下即可,试一试吧
4. 传统的“深度优先搜索”和“广度优先搜索”分别使用了 stack 和 queue 实现的,这里用了Python 强大的列表表达式实现的。想一下不用列表表达式如何实现?