Code Ganker: Binary Tree Postorder Traversal -- LeetCode

2014年3月24日星期一

Binary Tree Postorder Traversal -- LeetCode

原题链接: http://oj.leetcode.com/problems/binary-tree-postorder-traversal/
Binary Tree Inorder Traversal以及Binary Tree Preorder Traversal一样,二叉树的后序遍历我们还是介绍三种方法,第一种是递归,第二种是迭代方法,第三种是用线索二叉树。 递归还是那么简单,算法的时间复杂度是O(n), 而空间复杂度则是递归栈的大小,即O(logn)。代码如下:
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 TraversalBinary Tree Preorder Traversal,后序遍历的情况就复杂多了。我们需要维护当前遍历的cur指针和前一个遍历的pre指针来追溯当前的情况(注意这里是遍历的指针,并不是真正按后序访问顺序的结点)。具体分为几种情况:
(1)如果pre的左孩子或者右孩子是cur,那么说明遍历在往下走,按访问顺序继续,即如果有左孩子,则是左孩子进栈,否则如果有右孩子,则是右孩子进栈,如果左右孩子都没有,则说明该结点是叶子,可以直接访问并把结点出栈了。
(2)如果反过来,cur的左孩子是pre,则说明已经在回溯往上走了,但是我们知道后序遍历要左右孩子走完才可以访问自己,所以这里如果有右孩子还需要把右孩子进栈,否则说明已经到自己了,可以访问并且出栈了。
(3)如果cur的右孩子是pre,那么说明左右孩子都访问结束了,可以轮到自己了,访问并且出栈即可。
算法时间复杂度也是O(n),空间复杂度是栈的大小O(logn)。实现的代码如下:
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 TraversalBinary 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 TraversalBinary Tree Preorder Traversal,后序遍历的情况要复杂得多,因为后序遍历中一直要等到孩子结点访问完才能访问自己,需要一些技巧来维护。
在这里,我们需要创建一个临时的根节点dummy,把它的左孩子设为树的根root。同时还需要一个subroutine来倒序输出一条右孩子路径上的结点。跟迭代法一样我们需要维护cur指针和pre指针来追溯访问的结点。具体步骤是重复以下两步直到结点为空:
1. 如果cur指针的左孩子为空,那么cur设为其右孩子。
2. 否则,在cur的左子树中找到中序遍历下的前驱结点pre(其实就是左子树的最右结点)。接下来分两种子情况:
(1)如果pre没有右孩子,那么将他的右孩子接到cur。将cur更新为它的左孩子。
(2)如果pre的右孩子已经接到cur上了,说明这已经是回溯访问了,可以处理访问右孩子了,倒序输出cur左孩子到pre这条路径上的所有结点,并把pre的右孩子重新设为空(结点已经访问过了,还原现场)。最后将cur更新为cur的右孩子。
空间复杂度同样是O(1),而时间复杂度也还是O(n),倒序输出的过程只是加大了常数系数,并没有影响到时间的量级。如果对这个复杂度结果不是很熟悉的朋友,可以先看看Binary Tree Inorder Traversal中的分析,在那个帖子中讲得比较详细。实现的代码如下:
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遍历方法,如果没有准备过很难在面试中写出这种方法的后序遍历,主要还是要有概念,就是知道方法的存在性以及优劣的分析就可以了,不过递归法和迭代法还是需要掌握一下的。

没有评论:

发表评论