颜色标记法
Python
在 LeetCode 94的一篇文章看到了进行树的深度遍历的方法。效果是:无论是前序中序后序都是一样的写法。这种方法被作者称为“颜色标记法”。作者为:hzhu212,原文地址
这里直接附上原作者的思路分析:
其核心思想如下:
- 使用颜色标记节点的状态,新节点为白色,已访问的节点为灰色。
- 如果遇到的节点为白色,则将其标记为灰色,然后将其右子节点、自身、左子节点依次入栈。
- 如果遇到的节点为灰色,则将节点的值输出。
中序遍历
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
java
对应的,我写了一个 Java 版本的:
中序遍历
enum Color {
WHITE, BLACK
}
public static Map.Entry<Color, TreeNode> createEntry(Color color, TreeNode node) {
return new AbstractMap.SimpleEntry<>(color, node);
}
public static List<Integer> inorderTraversal3(TreeNode root) {
List<Integer> res = new ArrayList<>();
Stack<Map.Entry<Color, TreeNode>> stack = new Stack<>();
stack.add(createEntry(Color.WHITE, root));
while (!stack.isEmpty()) {
Map.Entry<Color, TreeNode> node = stack.pop();
if (node.getValue() == null) continue;
if (node.getKey() == Color.WHITE) {
stack.add(createEntry(Color.WHITE, node.getValue().right)); // 右
stack.add(createEntry(Color.BLACK, node.getValue())); // 中
stack.add(createEntry(Color.WHITE, node.getValue().left)); // 左
}
else {
res.add(node.getValue().val);
}
}
return res;
}
中序遍历: 左中右, 因此上面的入栈顺序为: 右中左
前序遍历: 中左右, 因此上面的入栈顺序为: 右左中
后序遍历: 左右中, 因此上面的入栈顺序为: 中右左
对于前序和后序遍历,只需要更改上述被注释的三句代码的顺序即可。
另一种通用的循环遍历方法
后序遍历
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
if (root == null) return Collections.emptyList();
List<Integer> res = new ArrayList<>(); //保存结果
Stack<TreeNode> call = new Stack<>(); //调用栈
call.push(root); //先将根结点入栈,即 r(root)
while (!call.isEmpty()) {
TreeNode t = call.pop();
if (t != null) {
call.push(t); //在右节点之前重新插入该节点,以便在最后处理(访问值)
call.push(null); // 下一次访问将不作为函数,而是只作为 root
if (t.right != null) call.push(t.right); // 插入r(右)
if (t.left != null) call.push(t.left); // 插入 r(左)
} else {
res.add(call.pop().val);
}
}
return res;
}
}
中序遍历
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
if (root == null) return Collections.emptyList();
List<Integer> res = new ArrayList<>(); //保存结果
Stack<TreeNode> call = new Stack<>(); //调用栈
call.push(root); //先将根结点入栈,即 r(root)
while (!call.isEmpty()) {
TreeNode t = call.pop();
if (t != null) {
if (t.right != null) call.push(t.right); // 插入r(右)
call.push(t); //在右节点之前重新插入该节点,以便在最后处理(访问值)
call.push(null); // 下一次访问将不作为函数,而是只作为 root
if (t.left != null) call.push(t.left); // 插入 r(左)
} else {
res.add(call.pop().val);
}
}
return res;
}
}
前序遍历
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
if (root == null) return Collections.emptyList();
List<Integer> res = new ArrayList<>(); //保存结果
Stack<TreeNode> call = new Stack<>(); //调用栈
call.push(root); //先将根结点入栈,即 r(root)
while (!call.isEmpty()) {
TreeNode t = call.pop();
if (t != null) {
if (t.right != null) call.push(t.right); // 插入r(右)
if (t.left != null) call.push(t.left); // 插入 r(左)
call.push(t); //在右节点之前重新插入该节点,以便在最后处理(访问值)
call.push(null); // 下一次访问将不作为函数,而是只作为 root
} else {
res.add(call.pop().val);
}
}
return res;
}
}
解析
在评论区看到眉间细雪对代码的解析,在这里附上。
中序遍历:
1
/ \
2 3
/ \
4 5
递归思路:r(1) -> r(2) -> r(4) -> 使用4
| |
使用1 使用2
| |
r(3) -> 使用3 r(5) -> 使用5
栈思路:r(n) = r(左) -> 使用n -> r(右),根据栈先进后出的规则,按照r(右)、使用n、r(左) 的顺序入栈
你会发现有两种需求:一种是r(n)表示递归函数,一种是使用n。
因此为了区分两种需求,引入 null 标志(图中用x表示)
如果栈中取出的是 null,表明要使用下一个弹出的n。
如果取出的不是 null,就是递归函数,按照r(右)、使用n、r(左) 的顺序入栈
| x |
| 4 | | 4 |
| x | | x | | x |
| 2 | | 2 | | 2 | | x |
| 2 | | 5 | | 5 | | 5 | | 5 | | 5 |
| x | | x | | x | | x | | x | | x | | x |
| 1 | | 1 | | 1 | | 1 | | 1 | | 1 | | 1 | | x |
| 1 | | 3 | | 3 | | 3 | | 3 | | 3 | | 3 | | 3 | | 3 | | 3 |
|___| |___| |___| |___| |___| |___| |___| |___| |___| |___| |___|
你会发现有两种需求:一种是r(n)表示递归函数,一种是使用n。
因此,当树的元素第一次被访问时,加入栈,此时它的角色是 递归函数 r(n)
,当它二次进栈时,将作为真正的元素值 n
来访问。
其实这个方法与颜色标记法是同个思路,即采用什么方法来区分 r(n)
和 n
。颜色标记法增加了一个枚举变量 Color
来作区分,而这个方法则通过在 n
之前插入一个 null
来区分。
普通的遍历方法(循环)
对于普通法,我大概写了一下
前序
public static List<Integer> inorderTraversal2(TreeNode root) {
List<Integer> result = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
while (cur != null || !stack.isEmpty()) {
if (cur != null) {
stack.push(cur);
result.add(cur.val); // 中
cur = cur.left;
} else {
cur = stack.pop(); // 左
cur = cur.right; // 右
}
}
return result;
}
中序
public static List<Integer> inorderTraversal2(TreeNode root) {
List<Integer> result = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
while (cur != null || !stack.isEmpty()) {
if (cur != null) {
stack.push(cur);
cur = cur.left;
} else {
cur = stack.pop(); // 左
result.add(cur.val); // 中
cur = cur.right; // 右
}
}
return result;
}
解释:
中序遍历: 4,2,5,1,3
1
/ \
2 3
/ \
4 5
递归思路:r(1) -> r(2) -> r(4) -> 使用4
| |
使用1 使用2
| |
r(3) r(5)
| |
使用3 使用5
stack:栈按 root -> left
| |
| | | |
| | | | | |
| | | | | | | |
| | | | | | | | | | | |
| | | 4 | | | | | | | | | | |
| 2 | | 2 | | 2 | | 5 | | | | | | |
| 1 | | 1 | | 1 | | 1 | | 1 | | 1 | | 3 | | |
|___| |___| |___| |___| |___| |___| |___| |___|
output: 若上述进栈操作暂停,stack 弹出一个(最顶层)进入 output,切换为该元素的右子树继续尝试进栈。
| |
| | | |
| | | | | |
| | | | | | | | | 3 |
| | | | | | | | | | | 1 | | 1 |
| | | | | | | | | 5 | | 5 | | 5 |
| | | | | 2 | | 2 | | 2 | | 2 | | 2 |
| | | | | 4 | | 4 | | 4 | | 4 | | 4 | | 4 |
|___| |___| |___| |___| |___| |___| |___| |___|
后序
从根节点开始依次迭代,弹出栈顶元素输出到输出列表中,然后依次压入它的所有孩子节点,按照从上到下、从左至右的顺序依次压入栈中。
因为深度优先搜索后序遍历的顺序是从下到上、从左至右,所以需要将输出列表逆序输出。
public class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
LinkedList<TreeNode> stack = new LinkedList<>();
LinkedList<Integer> output = new LinkedList<>();
if (root == null) {
return output;
}
stack.add(root);
while (!stack.isEmpty()) {
TreeNode node = stack.pollLast();
output.addFirst(node.val);
if (node.left != null) {
stack.add(node.left);
}
if (node.right != null) {
stack.add(node.right);
}
}
return output;
}
}
解释:
后序遍历: 4,5,2,3,1
1
/ \
2 3
/ \
4 5
递归思路:r(1) -> r(2) -> r(4) -> 使用4
| | |
使用1 使用2 r(5)
| |
r(3) 使用5
|
使用3
stack:栈按 root -> left -> right 进入。
| | | | | | | |
| 3 | | | | 5 | | |
| 1 | | 2 | | 2 | | 4 | | 4 |
|___| |___| |___| |___| |___|
output: 按 root -> right -> left 进入。输出逆序即为后序排列。
| | | | | | | 4 |
| | | | | | | 5 | | 5 |
| | | | | 2 | | 2 | | 2 |
| | | 3 | | 3 | | 3 | | 3 |
| | | 1 | | 1 | | 1 | | 1 | | 1 |
|___| |___| |___| |___| |___| |___|