上面的递归遍历是依赖函数堆栈递归遍历二叉树的,如果把递归函数 traverse 看做一个指针,那么这个指针在二叉树上游走的顺序大概是从最左侧开始,一列一列走到最右侧。
你可以结合可视化面板,点击其中的 这一行代码,看 root 变量在树上游走的顺序,就能直观地看到 traverse 函数访问节点的顺序:
二叉树的层序遍历,顾名思义,就是一层一层地遍历二叉树。这个遍历方式需要借助队列来实现,而且根据不同的需求,主要有三种不同的写法,下面一一列举。
# 输入一棵二叉树的根节点,层序遍历这棵二叉树
def levelTraverse(root: TreeNode) -> None:
if root is None:
return
q = [root]
# 从上到下遍历二叉树的每一层
while q:
sz = len(q)
# 从左到右遍历每一层的每个节点
for _ in range(sz):
cur = q.pop(0)
# 将下一层节点放入队列
if cur.left is not None:
q.append(cur.left)
if cur.right is not None:
q.append(cur.right)
写法一
这是最简单的写法,代码如下:
from collections import deque
def levelOrderTraverse(root):
if root is None:
return
q = deque()
q.append(root)
while q:
cur = q.popleft()
# 访问 cur 节点
print(cur.val)
# 把 cur 的左右子节点加入队列
if cur.left is not None:
q.append(cur.left)
if cur.right is not None:
q.append(cur.right)
你可以打开这个可视化面板,点击其中的 这一行代码,观察 cur 变量在树上游走的顺序,就可以看到层序遍历是一层一层,从左到右的遍历二叉树节点:
这种写法的优缺点
这种写法最大的优势就是简单。每次把队头元素拿出来,然后把它的左右子节点加入队列,就完事了。
但是这种写法的缺点是,无法知道当前节点在第几层。知道节点的层数是个常见的需求,比方说让你收集每一层的节点,或者计算二叉树的最小深度等等。
所以这种写法虽然简单,但用的不多,下面介绍的写法会更常见一些。
写法二
对上面的解法稍加改造,就得出了下面这种写法:
from collections import deque
def levelOrderTraverse(root):
if root is None:
return
q = deque()
q.append(root)
# 记录当前遍历到的层数(根节点视为第 1 层)
depth = 1
while q:
sz = len(q)
for i in range(sz):
cur = q.popleft()
# 访问 cur 节点,同时知道它所在的层数
print(f"depth = {depth}, val = {cur.val}")
# 把 cur 的左右子节点加入队列
if cur.left is not None:
q.append(cur.left)
if cur.right is not None:
q.append(cur.right)
depth += 1
注意代码中的内层 for 循环:
int sz = q.size();
for (int i = 0; i < sz; i++) {
…
}
这个变量 i 记录的是节点 cur 是当前层的第几个,大部分算法题中都不会用到这个变量,所以你完全可以改用下面的写法:
int sz = q.size();
while (sz-- > 0) {
…
}
这个属于细节问题,按照自己的喜好来就行。
但是注意队列的长度 sz 一定要在循环开始前保存下来,因为在循环过程中队列的长度是会变化的,不能直接用 q.size() 作为循环条件。
你可以打开这个可视化面板,点击其中的 这一行代码,观察 cur 变量在树上游走的顺序,就可以看到还是一层一层,从左到右的遍历二叉树节点,但是这次还会输出节点所在的层数:
这种写法就可以记录下来每个节点所在的层数,可以解决诸如二叉树最小深度这样的问题,是我们最常用的层序遍历写法。
写法三
既然写法二是最常见的,为啥还有个写法三呢?因为要给后面的进阶内容做铺垫。
现在我们只是在探讨二叉树的层序遍历,但是二叉树的层序遍历可以衍生出
多叉树的层序遍历,
图的 BFS 遍历,以及经典的
BFS 暴力穷举算法框架,所以这里要拓展延伸一下。
回顾写法二,我们每向下遍历一层,就给 depth 加 1,可以理解为每条树枝的权重是 1,二叉树中每个节点的深度,其实就是从根节点到这个节点的路径权重和,且同一层的所有节点,路径权重和都是相同的。
那么假设,如果每条树枝的权重和可以是任意值,现在让你层序遍历整棵树,打印每个节点的路径权重和,你会怎么做?
这样的话,同一层节点的路径权重和就不一定相同了,写法二这样只维护一个 depth 变量就无法满足需求了。
写法三就是为了解决这个问题,在写法一的基础上添加一个 State 类,让每个节点自己负责维护自己的路径权重和,代码如下:
class State:
def __init__(self, node, depth):
self.node = node
self.depth = depth
def levelOrderTraverse(root):
if root is None:
return
q = deque()
# 根节点的路径权重和是 1
q.append(State(root, 1))
while q:
cur = q.popleft()
# 访问 cur 节点,同时知道它的路径权重和
print(f"depth = {cur.depth}, val = {cur.node.val}")
# 把 cur 的左右子节点加入队列
if cur.node.left is not None:
q.append(State(cur.node.left, cur.depth + 1))
if cur.node.right is not None:
q.append(State(cur.node.right, cur.depth + 1))