每日一恋 - LeetCode 144 & 94 & 145. 二叉树的前序遍历、中序遍历、后序遍历

前言

实现二叉树的前序(preorder)、中序(inorder)、后序(postorder)遍历有两个常用的方法:一是递归(recursive),二是使用栈实现的迭代版本(stack+iterative)。这两种方法都是O(n)的空间复杂度(递归本身占用stack空间或者用户自定义的stack)。第三种是Morris方法遍历,它的空间复杂度只有O(1)。

复杂度分析:

  1. Recursive solution. Time: O(n), Space: O(n).
  2. Iterative way (stack). Time: O(n), Space: O(n).
  3. Threaded tree (Morris). Time: O(n), Space: O(1).

144. 二叉树的前序遍历

解法一

迭代版,用栈存放遍历的节点,注意是按照访问节点,压入右节点,压入左节点的顺序

public List<Integer> preorderTraversal(TreeNode root) {
    List<Integer> list = new ArrayList<Integer>();
    Stack<TreeNode> stack = new Stack<TreeNode>();
    stack.push(root);
    while (!stack.isEmpty()) {
        TreeNode node = stack.pop();
        list.add(node.val);
        if (node.right != null) stack.push(node.right);
        if (node.left != null) stack.push(node.left);
    }
    return list;
}

解法二

迭代版,将节点全部压入栈中,再弹出。

步骤:

  1. 当前节点不为空,入栈并输出当前节点,将其左孩子作为当前节点。
  2. 当前节点为空,栈中不为空,弹出一个节点,将其右孩子作为当前节点。
  3. 重复1、2直到栈为空,当前节点为空时结束循环。
public List<Integer> preorderTraversal(TreeNode root) {
    List<Integer> list = new ArrayList<Integer>();
    if (root == null)
        return list;

    Stack<TreeNode> stack = new Stack<TreeNode>();
    TreeNode p = root;
    while (!stack.isEmpty() || p != null) {
        while (p != null) {
            stack.push(p);
            list.add(p.val);
            p = p.left;
        }
        p = stack.pop();
        p = p.right;
    }
    return list;
}

或者

public List<Integer> preorderTraversal(TreeNode root) {
    List<Integer> result = new ArrayList<>();
    Deque<TreeNode> stack = new ArrayDeque<>();
    TreeNode p = root;
    while(!stack.isEmpty() || p != null) {
        if(p != null) {
            stack.push(p);
            result.add(p.val);  
            p = p.left;
        } else {
            TreeNode node = stack.pop();
            p = node.right;
        }
    }
    return result;
}

解法三

迭代版,也使用栈进行储存,但是栈里只存放右节点。每当到达一个节点时,右节点不为空先将右节点压入栈中;然后访问左节点,判断左节点是否为 null,如果为 null,并且栈不为空的话,就弹出之前存放的右子节点继续遍历。如果用示意图来表达的话,就是访问指针每次都先往左子节点遍历,直到为null,然后取出其父节点的右子树继续向左子节点遍历,以此类推。

步骤:

  1. 当前节点不为空,输出当前节点。
  2. 如果当前节点的右孩子不为空,向栈中压入右孩子。将其左孩子作为当前节点。
  3. 如果当前节点为空,但栈不为空,继续从栈中弹出一个节点作为当前节点。

代码:

public List<Integer> preorderTraversal(TreeNode node) {
    List<Integer> list = new LinkedList<Integer>();
    Stack<TreeNode> rights = new Stack<TreeNode>();
    while (node != null) {
        list.add(node.val);
        if (node.right != null) {
            rights.push(node.right);
        }
        node = node.left;
        if (node == null && !rights.isEmpty()) {
            node = rights.pop();
        }
    }
    return list;
}

解法四

Morris Traversal方法遍历二叉树(非递归,不用栈,O(1)空间),它能实现:

  1. O(1)空间复杂度,即只能使用常数空间;
  2. 二叉树的形状不能被破坏(中间过程允许改变其形状)。

Morris Traversal方法可以做到这两点,与递归和栈实现的不同在于该方法只需要O(1)空间,而且同样可以在O(n)时间内完成。具体可以参看(http://www.cnblogs.com/AnnieKim/archive/2013/06/15/morristraversal.html)

主要步骤:

  1. 如果当前节点的左孩子为空,则输出当前节点并将其右孩子作为当前节点。
  2. 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。
    a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。输出当前节点(在这里输出,这是与中序遍历唯一一点不同)。当前节点更新为当前节点的左孩子。
    b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空。当前节点更新为当前节点的右孩子。
  3. 重复以上1、2直到当前节点为空。

图示:
这里写图片描述

代码:

public void preorderMorrisTraversal(TreeNode root) {
    TreeNode cur = root, prev = null;
    while (cur != null) {
        if (cur.left == null) {
            System.out.println(cur.val);
            cur = cur.right;
        }
        else {
            prev = cur.left;
            while (prev.right != null && prev.right != cur)
                prev = prev.right;

            if (prev.right == null) {
                System.out.println(cur.val);
                prev.right = cur;
                cur = cur.left;
            }
            else {
                prev.right = null;
                cur = cur.right;
            }
        }
    }
}

解法五

递归版本

public List<Integer> preorderTraversal(TreeNode node) {
    List<Integer> list = new ArrayList<Integer>();
    dfs(node, list);
    return list;
}

public void dfs(TreeNode node, List<Integer> list) {
    if (node == null)
        return;
    list.add(node.val);
    dfs(node.left, list);
    dfs(node.right, list);
}

94. 二叉树的中序遍历

解法一

与前序遍历同样是先遍历左子节点再遍历右子节点,不同之处在于:前序遍历的实现方式是在压入栈的时候就访问了这个节点,而中序遍历的实现方式是从栈中弹出节点之后进行访问,保存其顺序。

先遍历当前节点的左子节点,直到没有左子节点,然后pop出当前最后一个左子节点,访问之,再遍历其右子节点的左子节点,以此类推。如果遍历到某个节点为空,那继续pop出下一个节点。

public List<Integer> inorderTraversal(TreeNode root) {

    List<Integer> list = new ArrayList<Integer>();
    if (root == null)
        return list;

    Stack<TreeNode> stack = new Stack<TreeNode>();
    TreeNode p = root;
    while ( !stack.isEmpty() || p != null ) {
        while (p != null) {
            stack.push(p);
            p = p.left;
        }
        p = stack.pop();
        list.add(p.val);
        p = p.right;
    }
    return list;
}

或者

public List<Integer> inorderTraversal(TreeNode root) {
    List<Integer> result = new ArrayList<>();
    Deque<TreeNode> stack = new ArrayDeque<>();
    TreeNode p = root;
    while(!stack.isEmpty() || p != null) {
        if(p != null) {
            stack.push(p);
            p = p.left;
        } else {
            TreeNode node = stack.pop();
            result.add(node.val);  
            p = node.right;   
        }
    }
    return result;
}

解法二

Morris Traversal中序遍历,非递归。

步骤:

  1. 如果当前节点的左孩子为空,则输出当前节点并将其右孩子作为当前节点。
  2. 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。
    a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。当前节点更新为当前节点的左孩子。
    b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空(恢复树的形状)。输出当前节点。当前节点更新为当前节点的右孩子。
  3. 重复以上1、2直到当前节点为空。

图示:
下图为每一步迭代的结果(从左至右,从上到下),cur代表当前节点,深色节点表示该节点已输出。
这里写图片描述

代码:

public void inorderMorrisTraversal(TreeNode root) {
    TreeNode cur = root, prev = null;
    while (cur != null) {
        if (cur.left == null) {
            System.out.println(cur.val);
            cur = cur.right;
        }
        else {
            prev = cur.left;
            while (prev.right != null && prev.right != cur)
                prev = prev.right;

            if (prev.right == null) {
                prev.right = cur;
                cur = cur.left;
            }
            else {
                prev.right = null;
                System.out.println(cur.val);
                cur = cur.right;
            }
        }
    }
}

O(1)空间复杂度
O(n)时间复杂度

空间复杂度:O(1),因为只用了两个辅助指针。
时间复杂度:O(n)。证明时间复杂度为O(n),最大的疑惑在于寻找中序遍历下二叉树中所有节点的前驱节点的时间复杂度是多少,即以下两行代码:

while (prev->right != NULL && prev->right != cur)
    prev = prev->right;

直觉上,认为它的复杂度是O(nlgn),因为找单个节点的前驱节点与树的高度有关。但事实上,寻找所有节点的前驱节点只需要O(n)时间。n个节点的二叉树中一共有n-1条边,整个过程中每条边最多只走2次,一次是为了定位到某个节点,另一次是为了寻找上面某个节点的前驱节点,如下图所示,其中红色是为了定位到某个节点,黑色线是为了找到前驱节点。所以复杂度为O(n)。

这里写图片描述

解法三

递归版,参数传递结果。

public List<Integer> inorderTraversal(TreeNode node) {
    List<Integer> list = new ArrayList<Integer>();
    dfs(node, list);
    return list;
}

public void dfs(TreeNode node, List<Integer> list) {
    if (node == null)
        return;
    dfs(node.left, list);
    list.add(node.val);
    dfs(node.right, list);
}

解法四

递归版,返回值作为结果。

public List<Integer> inorderTraversal(TreeNode root) {

    List<Integer> list = new ArrayList<Integer>();

    if (root == null)
        return list;

    List<Integer> left = inorderTraversal(root.left);
    if (left != null) list.addAll(left);
    list.add(root.val);
    List<Integer> right = inorderTraversal(root.right);
    if (right != null) list.addAll(right);
    return list;
}

145. 二叉树的后序遍历

解法一

与前序遍历的解法一类似,前序遍历每次先压入右孩子然后再左孩子(访问顺序先左后右),后序遍历正好相反,压入栈时先左后右(访问顺序先右后左)。后序遍历从根节点开始,越早遍历的节点越晚打印,因此需要后入先出的数据结构来保存结果,实际上队列和栈都可以。

public List<Integer> postorderTraversal(TreeNode root) {
    LinkedList<Integer> list = new LinkedList<>();
    Stack<TreeNode> stack = new Stack<TreeNode>();
    stack.push(root);
    while (!stack.isEmpty()) {
        TreeNode node = stack.pop();
        list.addFirst(node.val);
        if (node.left != null) stack.push(node.left);
        if (node.right != null) stack.push(node.right);
    }
    return list;
}

解法二

与前序遍历的解法二类似。关键在于倒序输出,先访问右孩子再访问左孩子

public List<Integer> postorderTraversal(TreeNode root) {

    LinkedList<Integer> list = new LinkedList<Integer>();
    if (root == null)
        return list;

    Stack<TreeNode> stack = new Stack<TreeNode>();
    TreeNode p = root;
    while ( !stack.isEmpty() || p != null ) {
        while (p != null) {
            stack.push(p);
            list.addFirst(p.val);
            p = p.right;
        }
        p = stack.pop();
        p = p.left;
    }
    return list;
}

或者

public List<Integer> postorderTraversal(TreeNode root) {
    LinkedList<Integer> result = new LinkedList<>();
    Deque<TreeNode> stack = new ArrayDeque<>();
    TreeNode p = root;
    while(!stack.isEmpty() || p != null) {
        if(p != null) {
            stack.push(p);
            result.addFirst(p.val);  
            p = p.right;             
        } else {
            TreeNode node = stack.pop();
            p = node.left;           
        }
    }
    return result;
}

关于层序遍历请看每日一恋 - LeetCode 102 & 107. 二叉树的层次遍历

本文是我参考了网上的资料和加上自己的理解总结的,如果有什么不对的地方非常欢迎大家的指正。

参考:
http://www.cnblogs.com/AnnieKim/archive/2013/06/15/morristraversal.html
https://www.yunaitong.cn/binary-tree-traverse.html

猜你喜欢

转载自blog.csdn.net/smartnijun/article/details/81805283
今日推荐