深度优先遍历(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);
}