深度优先遍历(DFS)和广度优先遍历(BFS)是两种遍历或搜索树和图的方法,常用于,呃,面试中……
深度优先遍历(DFS:Depth First Search)
DFS的遍历顺序又分为三种。下面以N代表当前结点,L代表左侧子结点,R代表右侧子结点。
- 先序(preorder):先处理当前结点,然后对子结点从左向右处理。NLR,从上向下,从左向右
- 中序(inorder):先处理左侧子结点,然后处理当前结点,最后处理右侧子结点。LNR,左–>当前–>右
- 后序(postorder):先从左向右处理子结点,然后处理当前结点。LRN,从下向上,从左向右
如果把表达式放入二叉树,那么先序遍历的结果是前缀表示法(波兰表示法,Polish Notation),中序遍历的结果是中缀表示法,后序遍历的结果是后缀表示法(逆波兰表示法,Reverse Polish Notation)。
DFS的特点天然地适合使用递归的方法实现。三种顺序仅仅通过改变处理程序片段的位置就能实现,十分简单易行。用循环来实现的话,就得自己维护一个栈,而且实现起来也比较麻烦。
private void dfsInorder(Node node) { if (node == null) { return; } // 处理左子结点 if (node.left != null) { dfsInorder(node.left); } // 处理本结点 handle(node); // 处理右子结点 if (ndoe.right != null) { dfsInorder(node.right); } }
private void dfsPreorder(Node node) { if (node == null) { return; } // 处理本结点 handle(node); // 处理子结点 if (node.left != null) { dfsPreorder(node.left); } if (ndoe.right != null) { dfsPreorder(node.right); } }
private void dfsPostorder(Node node) { if (node == null) { return; } // 处理子结点 if (node.left != null) { dfsPostorder(node.left); } if (ndoe.right != null) { dfsPostorder(node.right); } // 处理本结点 handle(node); }
循环的方法:估计面试时不会要求做这个吧?其中,前序比较容易,而中序比较难,后序本来更难,但鉴于它与先序的对称性,可以把结果按先序(但是改为从右向左)压入栈中,再反向逐个弹出即可。
public void dfsPreorder(Node root) { if (root == null) { return; } Deque<Node> stack = new ArrayDeque<>(); stack.push(root); while (!stack.isEmpty()) { Node node = stack.pop(); handle(node); if (node.right!= null) { stack.push(node.right); } if (node.left != null) { stack.push(node.left); } } } public void dfsInorder(Node root) { if (root == null) { return; } Deque<Node> stack = new ArrayDeque<>(); Node node = root; while (node != null || !stack.isEmpty()) { if (node != null) { stack.push(node); node = node.left; } else { node = stack.pop(); handle(node); node = node.right; } } } public void dfsPostorder(Node root) { if (root == null) { return; } Deque<Node> stack = new ArrayDeque<>(); Deque<Node> reversedStack = new ArrayDeque<>(); stack.push(root); while (!stack.isEmpty()) { Node node = stack.pop(); reversedStack.push(node); if (node.left != null) { stack.push(node.left); } if (node.right != null) { stack.push(node.right); } } while (!reversedStack.isEmpty()) { handle(reversedStack.pop()); } }
广度优先遍历(BFS:Breadth First Search)
BFS的访问顺序一般是从左向右,从上向下,一层一层地处理。一般用循环通过某种容器(queue、stack、set等)来实现。
public void bfs(Node root) { if (root == null) { return; } Queue<Node> queue = new LinkedList<>(); queue.offer(root); while (!queue.isEmpty()) { Node node = queue.poll(); handle(node); if (node.left != null) { queue.offer(node.left); } if (node.right != null) { queue.offer(node.right); } } }
有时候在做BFS时,需要在处理完每一层后,都做一些其他的处理。这种情况下,可以在while循环中再套一个循环来实现:
public void bfsPerLevel(Node root) { if (root == null) { return; } Queue<Node> queue = new LinkedList<>(); queue.offer(root); int count = 0; while (!queue.isEmpty()) { int n = queue.size(); for (int i = 0; i < n; ++i) { Node node = queue.poll(); handle(node); if (node.left != null) { queue.offer(node.left); } if (node.right != null) { queue.offer(node.right); } } // 此处加入处理完当前层后的代码 log.info(">>> end of level {}", count++); } }
一般来说,递归跟栈相关,所以对于DFS这种递归属性的算法,可以在循环中用栈来实现。而对于BFS这种跟栈没关系的算法,一般人就会认为不能用递归实现。但实际上还是有办法做到的:
private void recursiveBfsHelper(List<Node> nodeList) { if (nodeList.isEmpty()) { return; } List<Node> nextList = new ArrayList<>(); for (Node node : nodeList) { handle(node); if (node.left != null) { nextList.add(node.left); } if (node.right != null) { nextList.add(node.right); } } recursiveBfsHelper(nextList); } public void recursiveBfs(Node root) { recursiveBfsHelper(Collections.singletonList(root)); }
DFS算法实现起来简短方便(用递归的话),因此十分适合在面试中使用。但有些问题则需要用到BFS,比如求迷宫最短路径问题(因为逐层扫描直到找到解为止,这个解一定是最优解(或最优解之一),这里的最优解是路径最短的意思。
例题:二叉树的序列化和反序列化
将二叉树序列化为一个字符串,并将字符串反序列化为二叉树。
- 需要产生压缩的结果,否则对于只有右子树的二叉树,结果会过长。
- 由于上述要求,用DFS比较恰当,因为产生的结果便于反序列化。
// 序列化 // DFS preorder private void dfs(TreeNode root, StringBuilder sb) { sb.append(","); if (root != null) { sb.append(root.val); dfs(root.left, sb); dfs(root.right, sb); } } // Encodes a tree to a single string. public String serialize(TreeNode root) { if (root == null) { return ""; } StringBuilder sb = new StringBuilder(root.val); dfs(root.left, sb); dfs(root.right, sb); return sb.toString(); } // 反序列化 private TreeNode dfs(Queue<String> queue) { TreeNode root = null; if (!queue.isEmpty()) { String s = queue.poll(); if (!s.equals("")) { root = new TreeNode(Integer.parseInt(s)); root.left = dfs(queue); root.right = dfs(queue); } } return root; } // Decodes your encoded data to tree. public TreeNode deserialize(String data) { if (data == null || data.length() == 0) { return null; } Queue<String> queue = Arrays.stream(data.split(",", -1) .collect(Collectors.toCollection(LinkedList::new)); return dfs(queue); }