public ArrayList<Integer> postorderTraversal(TreeNode root) { ArrayList<Integer> res = new ArrayList<Integer>(); helper(root, res); return res; } private void helper(TreeNode root, ArrayList<Integer> res) { if(root == null) return; helper(root.left,res); helper(root.right,res); res.add(root.val); }接下来是迭代的做法,本质就是用一个栈来模拟递归的过程,但是相比于 Binary Tree Inorder Traversal 和 Binary Tree Preorder Traversal ,后序遍历的情况就复杂多了。我们需要维护当前遍历的cur指针和前一个遍历的pre指针来追溯当前的情况(注意这里是遍历的指针,并不是真正按后序访问顺序的结点)。具体分为几种情况:
public ArrayList<Integer> postorderTraversal(TreeNode root) { ArrayList<Integer> res = new ArrayList<Integer>(); if(root == null) return res; LinkedList<TreeNode> stack = new LinkedList<TreeNode>(); stack.push(root); TreeNode pre = null; while(!stack.isEmpty()) { TreeNode cur = stack.peek(); if(pre==null || pre.left==cur || pre.right==cur) { if(cur.left!=null) { stack.push(cur.left); } else if(cur.right!=null) { stack.push(cur.right); } else { res.add(cur.val); stack.pop(); } } else if(cur.left==pre && cur.right!=null) { stack.push(cur.right); } else { res.add(cur.val); stack.pop(); } pre = cur; } return res; }
上面迭代实现的思路虽然清晰,但是实现起来还是分情况太多,不容易记忆。我后来再看wiki的时候发现有跟Binary Tree Inorder Traversal和Binary Tree Preorder Traversal非常类似的解法,容易统一进行记忆,思路可以参考其他两种,区别是最下面在弹栈的时候需要分情况一下:
1)如果当前栈顶元素的右结点存在并且还没访问过(也就是右结点不等于上一个访问结点),那么就把当前结点移到右结点继续循环;
2)如果栈顶元素右结点是空或者已经访问过,那么说明栈顶元素的左右子树都访问完毕,应该访问自己继续回溯了。
下面列举一下代码:
public List<Integer> postorderTraversal(TreeNode root) { List<Integer> res = new ArrayList<Integer>(); if(root == null) { return res; } LinkedList<TreeNode> stack = new LinkedList<TreeNode>(); TreeNode pre = null; while(root != null || !stack.isEmpty()) { if(root!=null) { stack.push(root); root = root.left; } else { TreeNode peekNode = stack.peek(); if(peekNode.right!=null && pre != peekNode.right) { root = peekNode.right; } else { stack.pop(); res.add(peekNode.val); pre = peekNode; } } } return res; }
最后介绍Morris遍历方法,这个方法不需要为每个节点额外分配指针指向其前驱和后继结点,而是利用叶子节点中的右空指针指向中序遍历下的后继节点就可以了,所以优势在于不需要额外空间。不过同样相比于Binary Tree Inorder Traversal和Binary Tree Preorder Traversal,后序遍历的情况要复杂得多,因为后序遍历中一直要等到孩子结点访问完才能访问自己,需要一些技巧来维护。
在这里,我们需要创建一个临时的根节点dummy,把它的左孩子设为树的根root。同时还需要一个subroutine来倒序输出一条右孩子路径上的结点。跟迭代法一样我们需要维护cur指针和pre指针来追溯访问的结点。具体步骤是重复以下两步直到结点为空:public ArrayList<Integer> postorderTraversal(TreeNode root) { ArrayList<Integer> res = new ArrayList<Integer>(); TreeNode dummy = new TreeNode(0); dummy.left = root; TreeNode cur = dummy; TreeNode pre = null; while(cur!=null) { if (cur.left==null) { cur = cur.right; } else { pre = cur.left; while (pre.right!=null && pre.right!=cur) pre = pre.right; if (pre.right==null) { pre.right = cur; cur = cur.left; } else { reverse(cur.left, pre); TreeNode temp = pre; while (temp != cur.left) { res.add(temp.val); temp = temp.right; } res.add(temp.val); reverse(pre, cur.left); pre.right = null; cur = cur.right; } } } return res; } void reverse(TreeNode start, TreeNode end) { if (start == end) return; TreeNode pre = start; TreeNode cur = start.right; TreeNode next; while (pre != end) { next = cur.right; cur.right = pre; pre = cur; cur = next; } }到目前为止,二叉树的三种遍历的三种方法都介绍过了,后序遍历相比于前面两种,还是要复杂一些,个人觉得面试中可能倾向于靠其他两种遍历,特别是像Morris遍历方法,如果没有准备过很难在面试中写出这种方法的后序遍历,主要还是要有概念,就是知道方法的存在性以及优劣的分析就可以了,不过递归法和迭代法还是需要掌握一下的。