leecode/thinkings/binary-tree-traversal.md
2020-05-22 18:17:19 +08:00

185 lines
7.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 二叉树的遍历算法
## 概述
二叉树作为一个基础的数据结构,遍历算法作为一个基础的算法,两者结合当然是经典的组合了。
很多题目都会有 ta 的身影,有直接问二叉树的遍历的,有间接问的。
> 你如果掌握了二叉树的遍历,那么也许其他复杂的树对于你来说也并不遥远了
二叉数的遍历主要有前中后遍历和层次遍历。 前中后属于 DFS层次遍历属于 BFS。
DFS 和 BFS 都有着自己的应用,比如 leetcode 301 号问题和 609 号问题。
DFS 都可以使用栈来简化操作,并且其实树本身是一种递归的数据结构,因此递归和栈对于 DFS 来说是两个关键点。
DFS 图解:
![binary-tree-traversal-dfs](../assets/thinkings/binary-tree-traversal-dfs.gif)
(图片来自 https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/tree/depth-first-search)
BFS 的关键点在于如何记录每一层次是否遍历完成, 我们可以用一个标识位来表式当前层的结束。
下面我们依次讲解:
## 前序遍历
相关问题[144.binary-tree-preorder-traversal](../problems/144.binary-tree-preorder-traversal.md)
前序遍历的顺序是`根-左-右`
思路是:
1. 先将根结点入栈
2. 出栈一个元素,将右节点和左节点依次入栈
3. 重复 2 的步骤
总结: 典型的递归数据结构,典型的用栈来简化操作的算法。
其实从宏观上表现为:`自顶向下依次访问左侧链,然后自底向上依次访问右侧链`
如果从这个角度出发去写的话,算法就不一样了。从上向下我们可以直接递归访问即可,从下向上我们只需要借助栈也可以轻易做到。
整个过程大概是这样:
![binary-tree-traversal-preorder](../assets/thinkings/binary-tree-traversal-preorder.png)
这种思路解题有点像我总结过的一个解题思路`backtrack` - 回溯法。这种思路有一个好处就是
可以`统一三种遍历的思路`. 这个很重要,如果不了解的朋友,希望能够记住这一点。
## 中序遍历
相关问题[94.binary-tree-inorder-traversal](../problems/94.binary-tree-inorder-traversal.md)
中序遍历的顺序是 `左-根-右`,根节点不是先输出,这就有一点点复杂了。
1. 根节点入栈
2. 判断有没有左节点,如果有,则入栈,直到叶子节点
> 此时栈中保存的就是所有的左节点和根节点。
3. 出栈,判断有没有右节点,有则入栈,继续执行 2
值得注意的是中序遍历一个二叉查找树BST的结果是一个有序数组利用这个性质有些题目可以得到简化
比如[230.kth-smallest-element-in-a-bst](../problems/230.kth-smallest-element-in-a-bst.md)
以及[98.validate-binary-search-tree](../problems/98.validate-binary-search-tree.md)
## 后序遍历
相关问题[145.binary-tree-postorder-traversal](../problems/145.binary-tree-postorder-traversal.md)
后序遍历的顺序是 `左-右-根`
这个就有点难度了,要不也不会是 leetcode 困难的 难度啊。
其实这个也是属于根节点先不输出,并且根节点是最后输出。 这里可以采用一种讨巧的做法,
就是记录当前节点状态,如果 1. 当前节点是叶子节点或者 2.当前节点的左右子树都已经遍历过了,那么就可以出栈了。
对于 1. 当前节点是叶子节点,这个比较好判断,只要判断 left 和 rigt 是否同时为 null 就好。
对于 2. 当前节点的左右子树都已经遍历过了, 我们只需要用一个变量记录即可。最坏的情况,我们记录每一个节点的访问状况就好了,空间复杂度 O(n)
但是仔细想一下,我们使用了栈的结构,从叶子节点开始输出,我们记录一个当前出栈的元素就好了,空间复杂度 O(1) 具体请查看上方链接。
## 层次遍历
层次遍历的关键点在于如何记录每一层次是否遍历完成, 我们可以用一个标识位来表式当前层的结束。
![binary-tree-traversal-bfs](../assets/thinkings/binary-tree-traversal-bfs.gif)
(图片来自 https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/tree/breadth-first-search)
具体做法:
1. 根节点入队列, 并入队列一个特殊的标识位,此处是 null
2. 出队列
3. 判断是不是 null 如果是则代表本层已经结束。我们再次判断是否当前队列为空,如果不为空继续入队一个 null否则说明遍历已经完成我们什么都不不用做
4. 如果不为 null说明这一层还没完则将其左右子树依次入队列。
相关问题[102.binary-tree-level-order-traversal](../problems/102.binary-tree-level-order-traversal.md)
## 双色标记法
我们直到垃圾回收算法中,有一种算法叫三色标记法。 即:
- 用白色表示尚未访问
- 灰色表示尚未完全访问子节点
- 黑色表示子节点全部访问
那么我们可以模仿其思想,使用双色标记法来统一三种遍历。
其核心思想如下:
- 使用颜色标记节点的状态,新节点为白色,已访问的节点为灰色。
- 如果遇到的节点为白色,则将其标记为灰色,然后将其右子节点、自身、左子节点依次入栈。
- 如果遇到的节点为灰色,则将节点的值输出。
使用这种方法实现的中序遍历如下:
```python
class Solution:
def inorderTraversal(self, root: TreeNode) -> List[int]:
WHITE, GRAY = 0, 1
res = []
stack = [(WHITE, root)]
while stack:
color, node = stack.pop()
if node is None: continue
if color == WHITE:
stack.append((WHITE, node.right))
stack.append((GRAY, node))
stack.append((WHITE, node.left))
else:
res.append(node.val)
return res
```
如要实现前序、后序遍历,只需要调整左右子节点的入栈顺序即可。
## Morris 遍历
我们可以使用一种叫做 Morris 遍历的方法,既不使用递归也不借助于栈。从而在$O(1)$时间完成这个过程。
```python
def MorrisTraversal(root):
curr = root
while curr:
# If left child is null, print the
# current node data. And, update
# the current pointer to right child.
if curr.left is None:
print(curr.data, end= " ")
curr = curr.right
else:
# Find the inorder predecessor
prev = curr.left
while prev.right is not None and prev.right is not curr:
prev = prev.right
# If the right child of inorder
# predecessor already points to
# the current node, update the
# current with it's right child
if prev.right is curr:
prev.right = None
curr = curr.right
# else If right child doesn't point
# to the current node, then print this
# node's data and update the right child
# pointer with the current node and update
# the current with it's left child
else:
print (curr.data, end=" ")
prev.right = curr
curr = curr.left
```
参考: [what-is-morris-traversal](https://www.educative.io/edpresso/what-is-morris-traversal)