二叉树的非递归后序遍历如何实现?
背景
面试时考了这道题,之前一直都会递归遍历,非递归遍历倒是从来没有实际理解过它的具体过程,包括使用什么数据结构,具体的过程是怎样的?满脑子都是二叉树的层序遍历,
但是这里后序遍历和层序遍历还不太一样。
在仔细梳理并讲出自己的思路的时候,还是觉得并非易事。那就来分析分析。
什么是二叉树的后序遍历?
答: 二叉树有多种遍历方式,前序遍历,中序遍历,后序遍历,层序遍历等。层序遍历是一层一层去遍历,比较好理解。前后中也比较好理解, 中节点在前面,就是前序遍历,即中左右;
那后序遍历就是左右中,即先遍历左子树,再遍历右子树,最后是中间根节点,即左右中。
比如上述二叉树,后序遍历结束后, 输出节点顺序如下: 0 null 1 3 5 4 2
代码实现
知道了输出是什么样子,那如何去遍历。这边有两种方式,一种是递归的方式去遍历,另一个是借助栈这个数据结构去遍历。
递归
代码如下
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
inOrder(root, res);
return res;
}
public void inOrder(TreeNode root, List<Integer> res) {
if (root == null) {
return;
}
inOrder(root.left, res); // 左
inOrder(root.right, res); // 右子树
res.add(root.val); // 中间节点
}
}
思路是这样的,后序遍历按照访问左子树——右子树——根节点的方式遍历这棵树,而在访问左子树或者右子树的时候,也是按照同样的方式后序遍历,直到遍历完整棵树。因此整个遍历过程天然具有递归的性质,可直接用递归函数来模拟这一过程。
非递归的方式
非递归的方式有迭代法。
public List<Integer> postorderTraversal(TreeNode root) {
//
Deque<TreeNode> stack = new LinkedList<>();
List<Integer> res = new ArrayList<>();
if (root == null) return res;
stack.push(root);
while (!stack.isEmpty()) { // 不为空的时候,取出一个节点
TreeNode node = stack.pop();
res.addFirst(node.val); // 每次都加在List的头部而不是尾部。
if (node.left != null) {
stack.push(node.left);
}
if (node.right != null) {
stack.push(node.right);
}
}
return res;
}
迭代法需要借助栈和list来实现,stack用于节点的存储,list用于存储节点集。过程是这样的,
首先,将根节点2放入栈中。
1、进入while循环,当栈不为空的时候,弹出栈顶元素2,并将该节点的值加入list中。 由于我们是后序遍历,每次从list的头部添加元素,先加根节点,再加右节点以及孩子节点,最后加左节点以及孩子节点,这样就可以实现后序遍历。 此时list中存储了一个元素2;
接下来,要遍历节点2的左节点和右节点,当不为空时,才加入栈中。先加左节点,再加右节点,按照栈后进先出的特点,可以实现后序遍历。
2、继续while循环,此时栈中存储的是节点1和节点4,先弹出节点4,加入list, 对于节点4而言,它的两个孩子节点3和节点5同样要后序遍历。于是将3和5加入栈。此时list 中存储4 2 ;
3、继续while循环,此时栈中存储的是节点1和节点3和节点5,先弹出节点5,加入list;节点5都没有子节点,此时栈中有节点1,节点3; 此时list 中存储 5 4 2 ;
4、继续while循环,此时栈中存储的是节点1和节点3,先弹出节点3,加入list;节点3都没有子节点,此时栈中只有节点1; 进入左子树的遍历,过程类似。此时list 中存储3 5 4 2 ;
5、继续while, 开始处理左子树,弹出栈中的节点1。加入list。节点1的左节点不为null, 将 0 加入栈。此时list 中存储 1 3 5 4 2 ;
6、继续while, 弹出栈内的0节点,加入list。0没有孩子。此时list 中存储0 1 3 5 4 2, 正是我们需要的结果 ;
7、此时栈为空。循环结束,输出list返回,遍历结束。
整个思路还是很清晰的,就是把当前节点压栈,然后取出,加入list; 同时将左孩子和右孩子入栈,弹出右孩子,压栈其左右孩子;弹出左孩子,压栈左右孩子。直到栈为空,循环结束,遍历节点。此时代表没有节点要遍历了。
总的来说,被遍历过的节点都会经过一次压栈的过程,弹出的时候,按照先进后出,先弹右孩子,再弹左孩子。
栈的压入顺序是 根节点,左孩子,右孩子, 左孩子和右孩子一起压栈;加入结果集的过程,选择从头部加入,从而可以实现我们所需的后序遍历。