图和树的遍历是必会的算法之一,本文旨在帮助大家统一理解、灵活掌握图和树的遍历。
快速学习一个非常重要的策略就是,对繁杂的知识,进行统一划归,并进一步简化,去伪存真。因为除非你是智力超群的那一撮人,大部分人学习慢都是因为被杂乱的知识扰乱了精力,任何一种知识,只要你能抓最主要的矛盾,掌握最核心的概念,你就能快速学习。这也是在硅谷大热的“第一性原理”(据说 Elon Musk 是第一性原理的超级信徒)。
★ “每个系统中存在一个最基本的命题,它不能被违背或删除。” -- 亚里士多德 ”
树与图的联合理解
根据这一原理,我们暂时不需要分树和图,因为树其实是图的一种特殊情况。我们直接看图的知识点。
总所周知,图有两种遍历方式,深度优先遍历(DFS)和广度优先遍历(BFS),我们一个一个来看。
深度优先遍历
深度优先遍历可以用 Stack 来实现,具体实现基本上可以分这么几步:
- 如果栈不空,出栈得到当前节点
- 查看当前节点左右子树,如果不为空,入栈
可以说非常简单了,下面是一个简单实现:
class Solution(object):
def dfs(self, root):
stack = [(root, 0)] # 记录当前节点的深度,当然你也可以记录任何你想记录的东西
while stack: # 如果栈不为空
node, depth = stack.pop() # 出栈
if node is not None:
stack.append((node.left, depth+1)) # 入栈
stack.append((node.right, depth+1)) # 入栈
return
整个代码非常简洁。需要注意的是,在 Python里面,一个简单实现 Stack 的方式就是直接用 List,入栈就是 list.append()(从栈尾入栈),出栈是 list.pop()(从栈尾出栈)。
这个时候,我们可能会回想到,对于树来说,前中后序遍历能不能统一进来?能,其实前中后序遍历都可以划归到深度优先遍历里面去,只是访问的顺序的问题。
前序遍历
上面的那个代码,不用改就是前序遍历的代码:
class Solution(object):
def dfs(self, root):
stack = [(root, 0)] # 记录当前节点的深度,当然你也可以记录任何你想记录的东西
while stack: # 如果栈不为空
node, depth = stack.pop() # 出栈
# 前序遍历的操作在这里
if node is not None:
stack.append((node.right, depth+1)) # 入栈
stack.append((node.left, depth+1)) # 入栈
# 注意这里的入栈顺序会直接影响到最后的结果
# 一般来说,我们是让左节点最后入栈,这样出栈的时候,能优先出栈
return
写成递归形式,大体长这样
def preorder(node):
# 前序遍历的操作在这里
preorder(node.left)
preorder(node.right)
中序遍历
中序遍历的实现跟上面相比,稍微有点复杂,但是只要想清楚了,其实只是比上面前序遍历复杂了一点点。
首先,我们要知道为什么要使用栈来实现 DFS,因为在 DFS 里面,我们要一直往深度探索,这个没问题(入栈)。但是当我们遇到叶子节点要回溯的时候,一定是从上一个最新深度为起点接着探索(出栈)。而这些前中后序就是不同的出栈规则(操作总是紧跟出栈)。
在前序遍历中,每当我们到一个新节点的时候,就立即将其出栈,然后把他的孩子节点入栈。那么对于中序遍历,可以想象到,每当我们到达一个新节点的时候,我们要做一个判断:
- 如果他的左节点不是空的,那么我们就不能出栈,而是要继续把他的左节点放进来
- 如果他的左节点是空的,这个时候可以出栈了,但是记得出栈完把自己的右节点放进来。
非递归代码如下,
class Solution(object):
def dfs(self, root):
stack = [] # 记录当前节点的深度,当然你也可以记录任何你想记录的东西
while (len(stack)>0) or (root is not None): # 如果栈不为空
if root is not None:
stack.append(root)
root = root.left
else: # 如果左节点是空
root = stack.pop()
# 中序遍历的操作在这里
root = root.right
return
递归实现就很简单了:
def preorder(node):
preorder(node.left)
# 中序遍历的操作在这里
preorder(node.right)
后序遍历
后序遍历还要更复杂一些,因为后序遍历的定义就是,只有当一个节点的左右节点都访问过了,才能访问这个节点。
思路是: 先将当前节点的所有左侧子结点压入栈,现在要保证在访问当前节点的右子结点之后才能访问当前节点。所以每次从栈中拿出节点时,都需要判断该节点的右子树是否存在或右子树是否被访问过,这里使用了一个 preNode 来记录刚被访问过的节点,这样就可以实现只有当前节点的右子结点访问完成,才能访问当前节点。
class Solution(object):
def dfs(self, root):
stack = [] # 记录当前节点及其深度,当然你也可以记录任何你想记录的东西
node = root # 当前节点
preNode = None # 上一个被访问的节点
while (len(stack)>0) or (root is not None): # 如果栈不为空
while node is not None:
stack.append(root)
root = root.left
if len(stack) > 0:
tmp = stack[-1].right # 这个地方不能直接出栈,因为栈顶位置不一定能能方法,还需要判断其右节点被已经被访问过了
if (tmp is None) or (tmp==preNode):
node = stack.pop()
# 中序遍历的操作在这里
preNode = node
node=None
else:
node = tmp # 处理右节点
return
递归实现就很简单了:
def preorder(node):
preorder(node.left)
preorder(node.right)
# 后序遍历的操作在这里
广度优先遍历
广度优先遍历和深度优先相比,只需要把 Stack 换成 Queue。
from collection import deque
class Solution(object):
def dfs(self, root):
queue = deque([(root, 0)]) # 记录当前节点的深度,当然你也可以记录任何你想记录的东西
while queue: # 如果队列不为空
node, depth = queue.popleft() # 出队列
if node is not None:
queue.append((node.left, depth+1)) # 入队列
queue.append((node.right, depth+1)) # 入队列
return
特点总结
深度优先:
以中序遍历为例:某个节点被访问时,其左子树一定已经全部被访问,其右子树一定没有被访问。(“深度”对应“子树”的概念。)
广度优先(层次遍历):
从根节点开始,访问顺序一定是按照深度递增的。
实战习题
这两题都是 FaceBook 高频题目。
习题 1
对树进行搜索(可以广度优先,也可以前序深度优先),在搜索的时候,记录节点和其对应的宽度。例如,根节点的宽度是 0,然后对其左节点,入队列,并将其宽度置为 -1,右节点也入队列,宽度置为 +1。同理遍历整个树即可。
class Solution:
def verticalOrder(self, root: TreeNode) -> List[List[int]]:
if root is None:
return []
from collections import deque, defaultdict
res_dict = defaultdict(list)
queue = deque([(root, 0)])
while queue:
node, vert = queue.popleft()
res_dict[vert].append(node.val)
# if node is not None:
if node.left is not None:
queue.append([node.left, vert-1])
if node.right is not None:
queue.append([node.right, vert+1])
return [res_dict[i] for i in sorted(res_dict.keys())]
习题 2
上面的题是树中每个宽度(水平方向)上的问题,这一题就是每个深度(竖直方向)上的问题。我们只需要找到每个深度上面最右边的那个节点即可。
DFS 或者 BFS 都可以,但是 BFS 实现起来简单一些,因为只需要记录每个深度上最后一个节点即可。
from collections import deque
class Solution(object):
def rightSideView(self, root):
rightmost_value_at_depth = dict() # depth -> node.val
max_depth = -1
queue = deque([(root, 0)])
while queue:
node, depth = queue.popleft()
if node is not None:
# maintain knowledge of the number of levels in the tree.
max_depth = max(max_depth, depth)
# overwrite rightmost value at current depth. the correct value
# will never be overwritten, as it is always visited last.
rightmost_value_at_depth[depth] = node.val
queue.append((node.left, depth+1))
queue.append((node.right, depth+1))
return [rightmost_value_at_depth[depth] for depth in range(max_depth+1)]