前言
我也写了一篇文章详细分析了 Java层序遍历中迭代法和递归法的深入分析总结(广度优先搜索),有兴趣的同学可以看一看。
深度优先搜索中不同的方法思路是不同的,我们对前中后序的不同遍历思路来做一个总结。
方法一:递归
前中后序都是可以使用递归来实现的,这种方式也最为简单,只用改变加入数组时的不同顺序就可以达到不同的遍历效果。
public void preorder(TreeNode root, List<Integer> result) {
if (root == null) {
return;
}
result.add(root.val); // 中
preorder(root.left, result); // 左
preorder(root.right, result); // 右
}
方法二:通用迭代法
我们还可以采用通用的迭代法来完成前中后序遍历。具体思想如下:
- 我把所有元素按照我要处理的顺序依次入栈就行,入栈前要判断必须是非空节点,这样再出栈的时候就保证了遍历的顺序是满足我们前中后序要求的。
- 怎么实现依次入栈呢?只有每次弹出当前栈顶的中节点,再根据将这个中节点和它的左右孩子按顺序入栈,才能实现按要求依次入栈。
- 我们只处理‘中节点’,把每次弹出的‘中节点’的值加入数组中。注意这个中节点其实指代的是所有节点,因为所有的子节点都是他们自己子节点的中节点。
- 我们需要在处理中节点的前面加一个标识符‘null’,一旦栈弹出来null,我们就可以知道下一个弹出的节点需要我们处理(存值),这也是之前为什么不允许空节点入栈的原因。
以中序遍历为例,具体代码如下:
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
// 统一迭代法:用栈来做
Stack<TreeNode> st = new Stack<>();
List<Integer> result = new ArrayList<>();
// 不允许空节点入栈
if (root == null) {
return result;
}
st.push(root);
// 进行迭代
while (!st.isEmpty()) {
TreeNode temp = st.peek();
if (temp != null) {
// 要按中序的顺序去处理该节点和他的左右孩子,所以要将该节点弹出,再按顺序依次入栈
st.pop();
// 中序入栈顺序:右 -> 中 -> 左
if (temp.right != null) { // 空节点不许入栈
st.push(temp.right); // 右
}
st.push(temp); // 中
st.push(null); // 中节点访问过,但是还没有处理,加入空节点做为标记。
if (temp.left != null) { // 空节点不许入栈
st.push(temp.left); // 左
}
} else { // 只有遇到空节点的时候,才将下一个节点放进结果集
// 进入说明是空节点,我们用空节点标记了中节点,后面就是中
// 我们只处理所有的'中节点',其实就是把全部节点都处理了,因为所有节点都是他下面左右孩子(不管为不为空)的中节点
st.pop();
result.add(st.pop().val); // 加入到结果集
}
}
return result;
}
}
方法三:改良迭代法
前序遍历
虽然在通用迭代法中能通过更换顺序来改变二叉树遍历的次序,但是我们发现在面对前序遍历的时候其实没有必要把栈顶的中节点弹出后再按顺序加入,等待遍历到的时候再处理。我们可以在弹出栈顶元素后直接对他进行处理(存值),这是因为在前序遍历中我们节点的遍历顺序和需要处理的顺序是一致的,都是先对中间节点进行遍历和处理。
所以我们可以按照以下思想进行前序遍历的改进:
- 进入循环后我们直接弹出栈顶节点,把这个栈顶节点的值存入数组中,
中
就处理完了。(遍历+处理同步进行) - 把这个节点的右孩子入栈;再把左孩子入栈。因为栈的顺序是先进后出,所以后面弹出的顺序肯定是
左 -> 右
。
现在有个问题,我们应不应该让空节点入栈?这里对应着两个不同的写法。
-
不允许空节点入栈,最开始的根节点 和 后面循环里左右孩子 入栈的时候 都需要进行非空判断。
class Solution { public List<Integer> preorderTraversal(TreeNode root) { // 迭代法:用栈来做 Stack<TreeNode> st = new Stack<>(); List<Integer> list = new ArrayList<>(); // 入栈前要保证root不为空 if (root == null){ return list; } st.push(root); while (!st.isEmpty()) { TreeNode temp = st.pop(); list.add(temp.val); // 中 // 注意代码中空节点不入栈 if (temp.right != null) { st.push(temp.right); // 右 } // 注意代码中空节点不入栈 if (temp.left != null) { st.push(temp.left); // 左 } } return list; } }
-
允许空节点入栈,在进入循环的开头对栈顶节点进行非空判断就行,如果为空直接continue,让栈再弹一个节点出来。
class Solution { public List<Integer> preorderTraversal(TreeNode root) { // 迭代法:用栈来做 Stack<TreeNode> st = new Stack<>(); List<Integer> list = new ArrayList<>(); st.push(root); while (!st.isEmpty()) { TreeNode temp = st.pop(); // 注意栈中的空节点直接continue if (temp == null) { continue; } else { list.add(temp.val); // 中 st.push(temp.right); // 右 st.push(temp.left); // 左 } } return list; } }
这两种写法纯看个人喜好,理论上来说不让空节点入栈的执行效率可能更高吧…(不确定)
后序遍历
后序遍历的优化其实正常应该采取统一迭代法里面的写法,因为我们必须在最后处理才能中间节点,但是遍历我们肯定是从根节点开始遍历,也就是先遍历的一定是中间节点,这就造成了极大的遍历和处理顺序冲突,采取统一迭代法这种写法才可以按顺序入栈去处理来解决这种冲突。
但是这里有一个取巧的写法,就是我们前序遍历的时候遍历顺序是:中 -> 左 -> 右
,如果我们把顺序改成中 -> 右 -> 左
然后再进行一个数组的反转操作,不就实现了左 -> 右 -> 中
的后序遍历嘛!
实现代码从前序遍历中修改一下就行了:
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
// 迭代法:用栈来做
Stack<TreeNode> st = new Stack<>();
List<Integer> list = new ArrayList<>();
st.push(root);
while (!st.isEmpty()) {
TreeNode temp = st.pop();
// 注意栈中的空节点直接continue
if (temp == null) {
continue;
} else {
list.add(temp.val); // 中
st.push(temp.left); // 相对于前序遍历,这更改一下入栈顺序 左
st.push(temp.right); // 相对于前序遍历,这更改一下入栈顺序 右
}
}
Collections.reverse(list); // 将结果反转之后就是左右中的顺序了
return list;
}
}
要注意,栈是先入后出的,所以虽然我们入栈点顺序是左 -> 右
,但在处理时的处理顺序是按出栈顺序右 -> 左
来处理的。
中序遍历
中序遍历的处理顺序是左 -> 中 -> 右
,但我们遍历的顺序是先遍历中节点,怎么能使便利顺序和处理顺序统一呢?我们可以采取一个指针来使他们的顺序统一。具体思想如下:
- 定义一个指针cur先指向根节点,然后不断重复下面3个步骤(2~4)。
- 只要cur不为空就入栈并向它的左孩子遍历(让cur入栈并指向他自己的左孩子)。【等到循环到cur为空时,我们就获得了我们要处理的节点cur。虽然它是空,没关系,他同时也是它父亲节点的左孩子,也正因为它是空,相当于我们其实已经处理完左孩子了!】
- 只要cur为空,就指向当前栈弹出的节点(它的父亲节点),把当前cur的值存到数组中,再让cur指向它的右孩子。【站在当前cur的角度去看,只要它自己为空(相当于已经处理完左孩子了),我们应该让它指向它的父亲节点(指向弹出的栈顶节点),这就是我们要处理的中节点,把当前cur的值存入数组(处理中节点)。】
- 让cur指向当前cur的右孩子,回到2继续循环。【接下来该处理的顺序应该是右节点,我们只需要把cur指向当前cur的右孩子就好了。】
关键想法为通过使左孩子为空来达到永远先处理的第一个节点是左孩子的目的(空节点不用处理),代码如下:
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
// 迭代法:用栈来做
Stack<TreeNode> st = new Stack<>();
List<Integer> list = new ArrayList<>();
// 由于中序的遍历顺序和处理顺序不一样,所以需要定义指针,指针代表的是遍历的顺序
TreeNode cur = root;
while (!st.isEmpty() || cur != null) {
if (cur != null) { // 指针来访问节点,访问到最底层
st.push(cur); // 将访问的节点放进栈
cur = cur.left; // 左
} else { // 相当于一直找左孩子找到空为止,把这个空当做最下面的左孩子,所以后面只用按照栈依次处理所有的中节点和右节点就好了
cur = st.pop(); // 从栈里弹出的数据,是空指针(左孩子)的上一个节点(中节点),也是要处理的数据(放进list数组里的数据)
list.add(cur.val); // 中
// 再去处理右孩子
cur = cur.right; // 右
}
}
return list;
}
}
这个中序遍历的思想比较难想,我们这个思想的本质就是不断重复上面2~4的步骤,通过使左孩子为空来达到永远先处理的第一个节点是左孩子的目的(空节点不用处理)。这样我们就可以接着处理中间节点和右节点了。