背景
二叉树的遍历有三种,前序、中序、后序。
这三种遍历用递归实现非常简单:
- 前序遍历:打印根节点、遍历左子树、遍历右子树
- 中序遍历:遍历左子树、打印根节点、遍历右子树
- 后序遍历:遍历左子树、遍历右子树、打印根节点
递归版本很简单,用栈模拟递归也不难,本文主要说说莫里斯遍历,顺带补充迭代版本。
莫里斯遍历是由 J. H. Morris 在 1979 年的论文「Traversing Binary Trees Simply and Cheaply」中首次提出,因此被称为 Morris 遍历。
Morris 遍历的核心是利用树中的空闲指针,将空间缩减为常数。
前序、中序、后序遍历的时间复杂度都是 $O(N)$
前序、中序、后序遍历的递归和栈模拟递归版本 空间复杂度是 $O(h)$,这里的h
是树的高度。
前序、中序、后续遍历的莫里斯版本空间复杂度是 $O(1)$
前序遍历
迭代版本
这里就是模拟递归调用:
打印根节点
dfs(root.left)
打印根节点
dfs(root.left)
root为空返回
dfs(root.right)
dfs(root.left)
dfs(root.right)
dfs(root.right)
打印根节点
dfs(root.left)
dfs(root.right)
也就是先打印根节点、再一直遍历左节点(入栈),如果左边为空则则出栈,遍历右节点。
Python代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
class Solution(object):
def preorderTraversal(self, root):
res = []
stack = []
while root or stack:
# 不断往左子树方向走,每走一次就将当前节点保存到栈中
# 这是模拟递归的调用
if root:
res.append(root.val)
stack.append(root)
root = root.left
# 当前节点为空,说明左边走到头了,从栈中弹出节点并保存
# 然后转向右边节点,继续上面整个过程
else:
tmp = stack.pop()
root = tmp.right
return res
|
莫里斯遍历-1
这是破坏性的版本,传入的是一颗二叉树,等遍历完后,这棵二叉树就变成链表了。
它的原理是改变二叉树的结构,让根节点的左子树的最右节点 -指向-> 根节点的右子树,再把根节点的左子树变成右子树,伪代码如下:
root.left.最右节点.right = root.right
root.right = root.left
root.left = None
图解如下,第一步,把5
(根节点的左子树的最右节点),指向3
(根节点的右节点),再把跟节点的左子树变成右子树:
之后,用root.right
挨个遍历就可以了,直到3
这个节点时,将其子节点6
调整到右边。
代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
class Solution(object):
def preorderTraversal(self, root):
res = []
while root:
if root.left:
# 如果左子树不空,则找到左子树的最右节点
pre = root.left
while pre.right:
pre = pre.right
# 将左子树最右节点.right 指向根节点的右子树
pre.right = root.right
# 将左子树调整为右子树,并将左边置为空
root.right = root.left
root.left = None
else:
# 此时树就变成链表了,不断访问右节点即可
res.append(root.val)
root = root.right
return res
|
莫里斯遍历-2
这里使用了空闲指针的方式,同样也是找到 根节点左子树的最右节点,让其指向 根节点,然后根节点继续往左遍历。
root.left.最右节点.right = root
root = root.left
对应到下图,让5
的right指向1
,再让4
的right指向2
。
当左边遍历完后,就往右遍历,由于4
的右边是2
。当我们再次访问到2
时,发现2
的左子树最右节点4
的right等于2
。
这就说明2
是被访问过了,所以就可以断开4
的right,让其指向空,恢复二叉树的结构,同时让2
继续往右走。
对于1
也是一样,当再次遍历到1
时,就可以断开5
的right了。
Python代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
class Solution(object):
def preorderTraversal(self, root):
res = []
while root:
if root.left:
# 找到根节点左子树的最右节点
tmp = root.left
while tmp.right and tmp.right != root:
tmp = tmp.right
# 如果最右节点.right不为空,说明是第一次访问
if not tmp.right:
# 第一次访问,加入到结果集中
res.append(root.val)
# 并将右节点指向根
tmp.right = root
# 根节点继续往左访问
root = root.left
# 最右节点不空,说明访问过了,要断开
else:
tmp.right = None
root = root.right
# 访问根的右边
else:
res.append(root.val)
root = root.right
return res
|
中序遍历
迭代版本
Python代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
class Solution(object):
def inorderTraversal(self, root):
res = []
stack = []
while root or stack:
# 不断往左子树方向走,每走一次就将当前节点保存到栈中
# 这是模拟递归的调用
if root:
stack.append(root)
root = root.left
# 当前节点为空,说明左边走到头了,从栈中弹出节点并保存
# 然后转向右边节点,继续上面整个过程
else:
node = stack.pop()
res.append(node.val)
root = node.right
return res
|
莫里斯遍历-1
中序遍历的破坏版本,跟前序遍历很类似。前序遍历是先打印跟、再遍历左、再是右。所以改变完后的树结构是:
root.left.最右节点.right = root.right
root.right -> root.left
root.left = None
对于中序遍历改造后的结构是:
root.left.最右节点.right = root
tmp = root
root = root.left
tmp.left = None
如下图,这会我们要让5
的right指向1
,而1
的右边不动,之后把1
的左边置为空。
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
class Solution(object):
def inorderTraversal(self, root):
res = []
while root:
# 如果左子树不空,则找到左子树的最右节点
if root.left:
pre = root.left
while pre.right:
pre = pre.right
# 将左子树最右节点.right 指向根节点
pre.right = root
# 将root的left置为空并继续往左遍历
tmp = root
root = root.left
tmp.left = None
# 左子树为空,则打印这个节点,并向右边遍历
else:
res.append(root.val)
root = root.right
return res
|
莫里斯遍历-2
通过空闲指针的方式,跟前序版本非常类似,差别是在:
前序遍历时,添加值是在if not tmp.right
部分
中序遍历时,添加值是在else
部分
前序遍历时,当tmp.right
为空时,说明是第一次访问,所以要加入到集合中
中序遍历时,当tmp.right
不为空,说明是第二次访问,所以要添加到集合中
Python代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
class Solution(object):
def inorderTraversal(self, root):
res = []
while root:
if root.left:
# 找到根节点左子树的最右节点
tmp = root.left
while tmp.right and tmp.right != root:
tmp = tmp.right
# 为空说明是第一次访问
if not tmp.right:
# 并将右节点指向根
tmp.right = root
# 根节点继续往左访问
root = root.left
# 最右节点不空,说明访问过了,要断开
# 同时将值加入到结果集中
else:
res.append(root.val)
tmp.right = None
root = root.right
# 访问根的右边
else:
res.append(root.val)
root = root.right
return res
|
后序遍历
迭代版本
后序遍历是:左节点-右节点-打印根 这种顺序,如果将顺序改为:
打印根-右节点-左节点
再将顺序取反,就变成了:
左节点-右节点-打印根
这正好就是后序遍历的结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
class Solution(object):
def postorderTraversal(self, root):
if not root:
return []
res = []
stack = []
pre = None
while stack or root:
# 不断往右子树方向走,每走一次就将当前节点保存到栈中
if root:
res.append(root.val)
stack.append(root)
root = root.right
# 当前节点为空,说明右边走到头了,从栈中弹出节点并保存
# 然后转向左边节点,继续上面整个过程
else:
root = stack.pop()
root = root.left
# 结果集中保存的是[根、右、左],将其反转就是后序遍历
return res[::-1]
|
后序遍历的另一个版本
Python代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
class Solution(object):
def postorderTraversal(self, root):
if not root:
return []
res = []
stack = []
pre = None
while root or stack:
# 仍然是不断往左,并不断加入到栈中
if root:
stack.append(root)
root = root.left
else:
# 从栈中弹出,如果右边是空的,或者root.right==pre,
# 说明左右都访问过了,那么就将root.val加入到结果集中
# 1(root)
# / \
# 2 3(pre)
root = stack.pop()
if not root.right or root.right == pre:
res.append(root.val)
pre = root
root = None
# 继续访问右边
else:
stack.append(root)
root = root.right
return res
|
莫里斯遍历-1
跟迭代类似,我们要构造出 [根、右、左] 这种模式,然后取反。
也就是将 根的右节点的最左节点,指向根的右节点,再将根的右节点调整为左节点,并将右节点置为空,伪代码如下:
root.right.最左节点.left = root.left
root.left = root.right
root.right = None
如下图,将6
(根节点右子树的最左节点)的left指向根的左边2
。
再将1
的left指向3
,右边置为空。
代码整体跟前序很类似,把所有 left 改成了 right
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
class Solution(object):
def postorderTraversal(self, root):
res = []
while root:
# 如果右子树不空,则找到右子树的最左节点
if root.right:
tmp = root.right
while tmp.left:
tmp = tmp.left
# 将右子树最左节点.left 指向根节点的左子树
tmp.left = root.left
# 将右子树调整为左子树,并将右边置为空
root.left = root.right
root.right = None
else:
# 此时树就变成链表了,不断访问左节点即可
res.append(root.val)
root = root.left
# 结果集中保存的是[根、右、左]这种模式,将其反转后就变成了后序遍历结果了
return res[::-1]
|
莫里斯遍历-2
跟前序遍历类似,只是将所有的 left 改成 right。
首先让6
(根的右边子树的最左节点)的left指向1
当再次访问节点1
时,说明根和其右边都被访问过了,可以将6
的left置空。
同理,当再次访问2
时,将5
的left置空。
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
class Solution(object):
def postorderTraversal(self, root):
res = []
while root:
if root.right:
# 找到根节点右子树的最左节点
tmp = root.right
while tmp.left and tmp.left != root:
tmp = tmp.left
# 找到根节点右子树的最左节点
if not tmp.left:
# 保存到结果集中,并将左节点指向根
res.append(root.val)
tmp.left = root
root = root.right
# 最左节点不空,说明访问过了,要断开
else:
tmp.left = None
root = root.left
# 访问根的左边
else:
res.append(root.val)
root = root.left
# 结果集中保存的是[根、右、左]这种模式,将其反转后就变成了后序遍历结果了
return res[::-1]
|