全面解析DFS(必看)

颜色标记法

Python

在 LeetCode 94的一篇文章看到了进行树的深度遍历的方法。效果是:无论是前序中序后序都是一样的写法。这种方法被作者称为“颜色标记法”。作者为:hzhu212,原文地址

这里直接附上原作者的思路分析:

其核心思想如下:

  1. 使用颜色标记节点的状态,新节点为白色,已访问的节点为灰色。
  2. 如果遇到的节点为白色,则将其标记为灰色,然后将其右子节点、自身、左子节点依次入栈。
  3. 如果遇到的节点为灰色,则将节点的值输出。

中序遍历

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 |
        |___|    |___|    |___|    |___|   |___|   |___|
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值