LeetCode 94 144 145 LintCode 66 67 1783 二叉树的前中后序遍历所有解法总结(史上最全分类总结)

文章目录

    • @[toc]
  • 1. 简单说明
  • 2. 解法分析
    • 2.1 递归法
    • 2.2 非递归法
      • 2.2.1 一般迭代法
        • 2.2.1.1 中序遍历
        • 2.2.1.2 前序遍历
        • 2.2.1.3 后序遍历
      • 2.2.2 统一模板法
        • 2.2.2.1 设置结点访问标志
        • 2.2.2.2 null值区分
      • 2.2.3 Morris遍历法
        • 2.2.3.1 中序遍历
        • 2.2.3.2 前序遍历
        • 2.2.3.3 后序遍历
          • 2.2.3.3.1 前序遍历反向
          • 2.2.3.3.2 链表逆序打印

自己实现的LeetCode相关题解代码库:https://github.com/Yuri0314/Leetcode

自己实现的LintCode相关题解代码库:https://github.com/Yuri0314/LintCode


二叉树的前序、中序和后序遍历历来都是学习数据结构和算法必须要学习的一个很令人头疼的部分,往往还没有搞清楚某种解法的原理,就已经被网上各式各样的解法搞得头晕脑胀。因此我决定对二叉树的前中后序遍历的各类各种解法进行汇总分类,并各自进行相应说明,以期为大家选择适合自己的方法提供参考,同时也方便自己后面进行总结复习。

1. 简单说明

对应二叉树的前序、中序和后序遍历的题目地址,leetcode为144、94和145题,lintcode为66、67和1783题

  • leetcode:144. 二叉树的前序遍历 94. 二叉树的中序遍历 145. 二叉树的后序遍历
  • lintcode:66. 二叉树的前序遍历 67. 二叉树的中序遍历 1783. 二叉树的后序遍历

在leetcode中,前序和中序遍历的难度均为中等,而后序遍历为困难,由此见得,后序遍历相比于其他两种有一定的难度,那么后序遍历的难点在哪呢?

LeetCode 94 144 145 LintCode 66 67 1783 二叉树的前中后序遍历所有解法总结(史上最全分类总结)_第1张图片

对于上图中的树,不考虑输出结果时,正常的访问顺序(只按顺序访问树的所有结点)是从根节点出发,依次进行下列步骤:

  • 访问当前结点
  • 访问当前结点左子树
  • 访问当前结点右子树

事实上,上述步骤就是这三种遍历顺序递归实现的基本思路。但通常面试时,只使用递归往往过于简单,因此实际面试时常常要求使用非递归的方式来解决这个问题。仔细观察不难看出,访问完当前结点的孩子时,需要再回到当前结点上,这满足了栈的使用场景,因此实际实现非递归方法时,常常使用栈来进行操作。而这里面后序遍历的难点就在于,当访问完当前结点的左孩子时,如果从栈中取出当前结点,那么继续访问完当前结点的右孩子后将无法再找到原有的当前结点;而如果访问完当前结点左孩子不从栈中取出当前结点,反而继续对其右孩子进行压栈操作,那么问题就在于访问了其左右孩子后无法知道当前结点的左右孩子确实已经访问过,这样将会陷入循环再次访问这个结点的左右孩子。

根据各自遍历顺序的特点,网上提出了各种各样的解决思路,在此我对他们进行如下分类:

  • 递归法
  • 非递归法:
    • 一般迭代法
    • 统一模板法
    • Morris遍历法

其中非递归法里,不同遍历顺序对应于不同的解法也有不同的实现,下面我分情况进行讨论说明。

2. 解法分析

下面每类方法里将按照中序、前序、后序的顺序进行说明。

2.1 递归法

递归法实现非常简单,按照我上面说的思路即可实现,不再作说明。

中序遍历

class Solution {
     
    private List<Integer> ans = new ArrayList<Integer>();
    public List<Integer> inorderTraversal(TreeNode root) {
     
        traverse(root);
        return ans;
    }

    private void traverse(TreeNode root) {
     
        if (root == null) return;
        traverse(root.left);
        ans.add(root.val);
        traverse(root.right);
    }
}

前序遍历

class Solution {
     
    private List<Integer> ans = new ArrayList<Integer>();
    public List<Integer> preorderTraversal(TreeNode root) {
     
        traverse(root);
        return ans;
    }

    private void traverse(TreeNode root) {
     
        if (root == null) return;
        ans.add(root.val);
        traverse(root.left);
        traverse(root.right);
    }
}

后序遍历

class Solution {
     
    private List<Integer> ans = new ArrayList<Integer>();
    public List<Integer> postorderTraversal(TreeNode root) {
     
        traverse(root);
        return ans;
    }

    private void traverse(TreeNode root) {
     
        if (root == null) return;
        traverse(root.left);
        traverse(root.right);
        ans.add(root.val);
    }
}

2.2 非递归法

2.2.1 一般迭代法

之所以我把这类解法称为一般解法,是因为绝大多数现有标准教材和资料都提供的是该类解法,即使用一个栈根据各自遍历的特点进行实现。

2.2.1.1 中序遍历

中序遍历的的一般迭代法思路很简单:

  1. 从当前结点一直向其最左孩子搜索,直到没有左孩子了停止,这个过程中将路程中的所有结点入栈
  2. 弹出栈顶元素,将其记录在答案中,并把当前结点置为弹出元素的右孩子并重复上述1过程
class Solution {
     
    public List<Integer> inorderTraversal(TreeNode root) {
     
        List<Integer> ans = new ArrayList<Integer>();
        Stack<TreeNode> stack = new Stack<TreeNode>();
        while (root != null || !stack.isEmpty()) {
     
            while (root != null) {
     
                stack.push(root);
                root = root.left;
            }
            root = stack.pop();
            ans.add(root.val);
            root = root.right;
        }
        return ans;
    }
}

2.2.1.2 前序遍历

方法1

因为前序遍历访问顺序是“中-左-右”,所以可以先将根结点压栈,然后按照下列步骤执行:

  1. 如果栈不为空,则弹出栈顶元素存入结果中
  2. 如果弹出元素的右孩子不为空则将右孩子压栈,然后如果其左孩子也不为空将其左孩子压栈(因为栈是后入先出的,所以为了达到“中-左-右”的顺序,需要先压入右孩子,再压入左孩子)
class Solution {
     
    public List<Integer> preorderTraversal(TreeNode root) {
     
        List<Integer> ans = new ArrayList<Integer>();
        if (root == null) return ans;
        Stack<TreeNode> stack = new Stack<TreeNode>();
        stack.push(root);
        while (!stack.isEmpty()) {
     
            root = stack.pop();
            ans.add(root.val);
            if (root.right != null) stack.push(root.right);
            if (root.left !=null) stack.push(root.left);
        }
        return ans;
    }
}

方法2——根据中序遍历进行微调

因为前序遍历与中序遍历的区别仅仅在于中结点和左节点的顺序上,而中序遍历的一般迭代法中会记录直到当前节点最左孩子路径上的所有结点,因此,前序遍历也可以应用这种方法来实现。

调整的部分仅仅是将结点值加入结果中的这一行语句的位置,即ans.add(root.val)语句的位置,其余保持不变即可。

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

2.2.1.3 后序遍历

方法1——根据前序遍历进行反向

因为前序遍历的顺序是“左-中-右”,而后序遍历顺序是“左-右-中”,不考虑左结点,区别只是在于中结点和右结点的顺序进行了反向而已,因此可以使用前序遍历的代码进行调整,只需要将前序遍历对左右孩子压栈的顺序反向即可,即先压入左孩子,再压入右孩子。除此之外,因为按照这种方法调整得到的遍历顺序为“中-右-左”,正好是后序遍历的反向顺序,因此在获得遍历序列后还需进行逆序操作。

注意:这里我使用了LinkedList头插结果的方式来实现反向。

PS:该方法利用了逆序的关系,如果还需要按照正常遍历顺序对树结点依次操作,则此方法无法满足,这是该方法的缺点。

class Solution {
     
    public List<Integer> postorderTraversal(TreeNode root) {
     
        List<Integer> ans = new LinkedList<Integer>();
        if (root == null) return ans;
        Stack<TreeNode> stack = new Stack<TreeNode>();
        stack.push(root);
        while (!stack.isEmpty()) {
     
            root = stack.pop();
            ans.add(0, root.val);
            if (root.left != null) stack.push(root.left);
            if (root.right != null) stack.push(root.right);
        }
        return ans;
    }
}

方法2——记录前置结点

后序遍历的难点就在于区分当前结点是否访问过,因此可以在遍历的过程中实时记录当前结点的前置结点。

  • 如果栈顶元素的左右孩子均非前置结点,并且该元素有左孩子结点,那么说明该结点的孩子均未访问过,按照后序遍历顺序将其左孩子入栈;
  • 同理,如果其没有左孩子有孩子,并且其右孩子不是前置结点,则说明其孩子未访问过,按照后序遍历顺序,其右孩子应该在它签名访问,因此将其右孩子入栈;
  • 如果左右孩子出现了前置结点,那么说明该结点的孩子已访问过,按照后序遍历顺序,应该接下来访问其自身,因此将其出栈并加入结果中,此时将其置为新的前置结点
class Solution {
     
    public List<Integer> postorderTraversal(TreeNode root) {
     
        List<Integer> ans = new ArrayList<Integer>();
        if (root == null) return ans;
        Stack<TreeNode> stack = new Stack<TreeNode>();
        TreeNode pre = root;
        stack.push(root);
        while (!stack.isEmpty()) {
     
            root = stack.peek();
            if (root.left != null && root.left != pre && root.right != pre) stack.push(root.left);
            else if (root.right != null && root.right != pre) stack.push(root.right);
            else {
     
                ans.add(stack.pop().val);
                pre = root;
            }
        }
        return ans;
    }
}

2.2.2 统一模板法

只掌握各遍历方法的一般迭代法应付面试时往往会把自己弄的昏头转向,那么有没有一种像递归法一样,只改一改三个语句的顺序就能统一实现三种遍历方法的实现呢?答案是肯定的。只要我们为访问的每一个结点附加上一个可以用来判断其是否是第二次访问的信息即可。这种统一模板的方法有两种。

2.2.2.1 设置结点访问标志

顾名思义,这种方法就是给每一个结点附加一个额外的标志位,如果没访问过,标志位置为false,如果已经访问过了,则置为true,并且将已访问过的结点加入结果中。

注意:不同遍历顺序入栈的顺序不同,并且因为栈是后入先出的,所以入栈顺序应该根输出顺序相反。比如,中序遍历应该按照“右-中-左”的顺序入栈。

中序遍历

class Solution {
     
    public List<Integer> inorderTraversal(TreeNode root) {
     
        List<Integer> ans = new ArrayList<Integer>();
        if (root == null) return ans;
        Stack<TreeNodePair> stack = new Stack<TreeNodePair>();
        stack.push(new TreeNodePair(root, false));
        while (!stack.isEmpty()) {
     
            TreeNodePair pair = stack.pop();
            if (!pair.flag) {
     
                if (pair.node.right != null) stack.push(new TreeNodePair(pair.node.right, false));
                stack.push(new TreeNodePair(pair.node, true));
                if (pair.node.left != null) stack.push(new TreeNodePair(pair.node.left, false));
            }
            else {
     
                ans.add(pair.node.val);
            }
        }
        return ans;
    }

    private class TreeNodePair {
     
        TreeNode node;
        boolean flag;
        TreeNodePair(TreeNode node, boolean flag) {
     
            this.node = node;
            this.flag = flag;
        }
    }
}

前序遍历

class Solution {
     
    public List<Integer> preorderTraversal(TreeNode root) {
     
        List<Integer> ans = new ArrayList<Integer>();
        if (root == null) return ans;
        Stack<TreeNodePair> stack = new Stack<TreeNodePair>();
        stack.push(new TreeNodePair(root, false));
        while (!stack.isEmpty()) {
     
            TreeNodePair pair = stack.pop();
            if (!pair.flag) {
     
                if (pair.node.right != null) stack.push(new TreeNodePair(pair.node.right, false));
                if (pair.node.left != null) stack.push(new TreeNodePair(pair.node.left, false));
                stack.push(new TreeNodePair(pair.node, true));
            }
            else {
     
                ans.add(pair.node.val);
            }
        }
        return ans;
    }

    private class TreeNodePair {
     
        TreeNode node;
        boolean flag;
        TreeNodePair(TreeNode node, boolean flag) {
     
            this.node = node;
            this.flag = flag;
        }
    }
}

后序遍历

class Solution {
     
    public List<Integer> postorderTraversal(TreeNode root) {
     
        List<Integer> ans = new ArrayList<Integer>();
        if (root == null) return ans;
        Stack<TreeNodePair> stack = new Stack<TreeNodePair>();
        stack.push(new TreeNodePair(root, false));
        while (!stack.isEmpty()) {
     
            TreeNodePair pair = stack.pop();
            if (!pair.flag) {
     
                stack.push(new TreeNodePair(pair.node, true));
                if (pair.node.right != null) stack.push(new TreeNodePair(pair.node.right, false));
                if (pair.node.left != null) stack.push(new TreeNodePair(pair.node.left, false));
            }
            else {
     
                ans.add(pair.node.val);
            }
        }
        return ans;
    }

    private class TreeNodePair {
     
        TreeNode node;
        boolean flag;
        TreeNodePair(TreeNode node, boolean flag) {
     
            this.node = node;
            this.flag = flag;
        }
    }
}

2.2.2.2 null值区分

与设置结点访问标志法一样,这种方法也是通过引入某种信息来判断某一个在栈内的结点是否已访问过,不过该方法没有定义一个新类,而是对于每一个未访问过的结点,除了将其本身入栈外,还额外压栈一个null值(或者压入某种与其他结点不冲突且能唯一标识的结点值也可以)。

  • 当取出的栈顶元素不为null时,说明其为首次访问,按照各遍历需要的顺序入栈,但压入其本身时还需要额外压入一个null值。
  • 当取出的栈顶元素为null时,说明下一个栈顶元素一定是访问过的元素,将其弹出并加入至结果中。

注意:与设置结点访问标志法一样,因为使用的是栈结构,需要注意其压栈顺序与输出顺序相反。

中序遍历

class Solution {
     
    public List<Integer> inorderTraversal(TreeNode root) {
     
        List<Integer> ans = new ArrayList<Integer>();
        Stack<TreeNode> stack = new Stack<TreeNode>();
        if (root != null) stack.push(root);
        while (!stack.isEmpty()) {
     
            root = stack.pop();
            if (root != null) {
     
                if (root.right != null) stack.push(root.right);
                stack.push(root);
                stack.push(null);
                if (root.left != null) stack.push(root.left);
            }
            else ans.add(stack.pop().val);
        }
        return ans;
    }
}

前序遍历

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

后序遍历

class Solution {
     
    public List<Integer> postorderTraversal(TreeNode root) {
     
        List<Integer> ans = new ArrayList<Integer>();
        Stack<TreeNode> stack = new Stack<TreeNode>();
        if (root != null) stack.push(root);
        while (!stack.isEmpty()) {
     
            root = stack.pop();
            if (root != null) {
     
                stack.push(root);
                stack.push(null);
                if (root.right != null) stack.push(root.right);
                if (root.left != null) stack.push(root.left);
            }
            else ans.add(stack.pop().val);
        }
        return ans;
    }
}

2.2.3 Morris遍历法

上述所有非递归方法都使用了一个额外的栈结构,因此其空间复杂度均为O(N)。那么是否有一种不使用额外空间,使得空间复杂度降为O(1)的遍历方法呢?确实存在这样的方法,这就是Morris遍历法,这也是线索二叉树所使用的结构,详见Threaded binary tree.

该方法的思路简单说就是,对于每一个结点,找到它左孩子的最右子结点,因为按照正常访问顺序,其左孩子的最有子节点访问完后就应该访问其本身了,因此将其左孩子最右子节点的右指针指向它。

基本步骤如下:

  • 如果当前结点左孩子为空,说明最左边访问完毕,将其置为其右孩子(注意:此时置为右孩子既可能是指向了树结构中真正的右孩子,也可能是通过手工添加的右指针回到了其下一步该访问的祖先节点位置)
  • 如果当前结点左孩子不为空,那么开始尝试找到该结点左孩子的最右子节点,建立连接关系
    • 如果找到的当前结点的左孩子的最右子节点右指针为空,说明还未建立连接关系,是首次访问当前结点,那么将该最右结点的右指针指向当前结点,然后当前结点向左孩子走一步继续重复所有步骤。
    • 如果找到的当前结点的左孩子的最右子节点右指针不为空,说明已建立过连接关系,是第二次访问当前结点,这意味着当前结点的左子树应该已经全部遍历完了,此时应恢复连接关系重新置为空,然后当前结点向右孩子走一步继续重复所有步骤。

注意:该方法虽然保证了O(1)的空间复杂度,但其在遍历过程中改变了部分结点的指向,破坏了树的结构,如果题目或者实际应用中严格要求不能改变树的结构,则此方法无法使用。

2.2.3.1 中序遍历

注意添加结点值至结果中的位置

class Solution {
     
    public List<Integer> inorderTraversal(TreeNode root) {
     
        List<Integer> ans = new ArrayList<Integer>();
        TreeNode cur = root;
        TreeNode pre = null;
        while (cur != null) {
     
            if (cur.left == null) {
     
                ans.add(cur.val);
                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 {
     
                    ans.add(cur.val);
                    pre.right = null;
                    cur = cur.right;
                }
            }
        }
        return ans;
    }
}

2.2.3.2 前序遍历

class Solution {
     
    public List<Integer> preorderTraversal(TreeNode root) {
     
        List<Integer> ans = new ArrayList<Integer>();
        TreeNode cur = root;
        TreeNode pre = null;
        while (cur != null) {
     
            if (cur.left == null) {
     
                ans.add(cur.val);
                cur = cur.right;
            }
            else {
     
                pre = cur.left;
                while (pre.right != null && pre.right != cur) pre = pre.right;
                if (pre.right == null) {
     
                    ans.add(cur.val);
                    pre.right = cur;
                    cur = cur.left;
                }
                else {
     
                    pre.right = null;
                    cur = cur.right;
                }
            }
        }
        return ans;
    }
}

2.2.3.3 后序遍历

与前序和中序不同,因为添加的指向连接只是从一个结点的左孩子访问结束后指向该结点,而后序遍历需要在访问完不止左孩子还有右孩子之后才处理父结点,因此需要一些特殊操作。

2.2.3.3.1 前序遍历反向

跟后序遍历的一般迭代法类似,同样可以利用后序遍历与前序遍历的部分逆向性,对前序遍历的Morris遍历代码进行改变。改变方法就是将前序遍历中的各类操作反向,即处理左孩子的改为处理右孩子,如找到当前结点左孩子的最右结点改为找到当前结点右孩子的最左结点。

注意:跟由前序遍历一般迭代法进行微调得到的方法一样,该方法同样使用LinkedList头插入的方式实现逆序。

PS:跟由前序遍历一般迭代法进行微调得到的方法一样,该方法同样只是利用了逆序的特点,并没有按照要求的顺序访问相应结点,如果需要按照正常遍历顺序操作相关结点的话,则该方法无法满足。

class Solution {
     
    public List<Integer> postorderTraversal(TreeNode root) {
     
        List<Integer> ans = new LinkedList<Integer>();
        TreeNode cur = root;
        TreeNode pre = null;
        while (cur != null) {
     
            if (cur.right == null) {
     
                ans.add(0, cur.val);
                cur = cur.left;
            }
            else {
     
                pre = cur.right;
                while (pre.left != null && pre.left != cur) pre = pre.left;
                if (pre.left == null) {
     
                    ans.add(0, cur.val);
                    pre.left = cur;
                    cur = cur.right;
                }
                else {
     
                    pre.left = null;
                    cur = cur.left;
                }
            }
        }
        return ans;
    }
}
2.2.3.3.2 链表逆序打印

如果纯粹的按照需要的遍历顺序使用Morris方法的话,需要比前序和中序操作增加一些额外操作。

当某个结点左孩子的最右结点的右指针指向当前结点,即当前结点是第二次访问时,因为要求的是后序,从当前结点左孩子一直到左孩子的最右结点这一路上的所有结点应该均为加入结果序列中,因此需要将这一条边上所有结点都加入结果中。但是需要注意,加入的顺序应该是从左孩子的最右子结点到左孩子,即与树的方向相反。因此我额外定义了一个反转链表的函数,把这一条路径当作一个链表进行反转操作,然后再从头打印,同时为了不过大破坏树的结构,打印之后还需要再进行一次逆序,将该边反转回正常顺序。

class Solution {
     
    private List<Integer> ans = new ArrayList<Integer>();
    public List<Integer> postorderTraversal(TreeNode root) {
     
        TreeNode cur = root;
        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 {
     
                    pre.right = null;
                    traverseEdge(cur.left);
                    cur = cur.right;
                }
            }
        }
        traverseEdge(root);
        return ans;
    }

    private void traverseEdge(TreeNode node) {
     
        TreeNode cur = reverseEdge(node);
        node = cur;
        while (cur != null) {
     
            ans.add(cur.val);
            cur = cur.right;
        }
        reverseEdge(node);
    }

    private TreeNode reverseEdge(TreeNode node) {
     
        TreeNode pre = null, cur = node;
        while (cur != null) {
     
            node = cur.right;
            cur.right = pre;
            pre = cur;
            cur = node;
        }
        return pre;
    }
}

你可能感兴趣的:(算法,Java,leetcode,二叉树,java,算法,数据结构,面试)