自己实现的LeetCode相关题解代码库:https://github.com/Yuri0314/Leetcode
自己实现的LintCode相关题解代码库:https://github.com/Yuri0314/LintCode
二叉树的前序、中序和后序遍历历来都是学习数据结构和算法必须要学习的一个很令人头疼的部分,往往还没有搞清楚某种解法的原理,就已经被网上各式各样的解法搞得头晕脑胀。因此我决定对二叉树的前中后序遍历的各类各种解法进行汇总分类,并各自进行相应说明,以期为大家选择适合自己的方法提供参考,同时也方便自己后面进行总结复习。
对应二叉树的前序、中序和后序遍历的题目地址,leetcode为144、94和145题,lintcode为66、67和1783题
在leetcode中,前序和中序遍历的难度均为中等,而后序遍历为困难,由此见得,后序遍历相比于其他两种有一定的难度,那么后序遍历的难点在哪呢?
对于上图中的树,不考虑输出结果时,正常的访问顺序(只按顺序访问树的所有结点)是从根节点出发,依次进行下列步骤:
事实上,上述步骤就是这三种遍历顺序递归实现的基本思路。但通常面试时,只使用递归往往过于简单,因此实际面试时常常要求使用非递归的方式来解决这个问题。仔细观察不难看出,访问完当前结点的孩子时,需要再回到当前结点上,这满足了栈的使用场景,因此实际实现非递归方法时,常常使用栈来进行操作。而这里面后序遍历的难点就在于,当访问完当前结点的左孩子时,如果从栈中取出当前结点,那么继续访问完当前结点的右孩子后将无法再找到原有的当前结点;而如果访问完当前结点左孩子不从栈中取出当前结点,反而继续对其右孩子进行压栈操作,那么问题就在于访问了其左右孩子后无法知道当前结点的左右孩子确实已经访问过,这样将会陷入循环再次访问这个结点的左右孩子。
根据各自遍历顺序的特点,网上提出了各种各样的解决思路,在此我对他们进行如下分类:
其中非递归法里,不同遍历顺序对应于不同的解法也有不同的实现,下面我分情况进行讨论说明。
下面每类方法里将按照中序、前序、后序的顺序进行说明。
递归法实现非常简单,按照我上面说的思路即可实现,不再作说明。
中序遍历
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);
}
}
之所以我把这类解法称为一般解法,是因为绝大多数现有标准教材和资料都提供的是该类解法,即使用一个栈根据各自遍历的特点进行实现。
中序遍历的的一般迭代法思路很简单:
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;
}
}
方法1
因为前序遍历访问顺序是“中-左-右”,所以可以先将根结点压栈,然后按照下列步骤执行:
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;
}
}
方法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;
}
}
只掌握各遍历方法的一般迭代法应付面试时往往会把自己弄的昏头转向,那么有没有一种像递归法一样,只改一改三个语句的顺序就能统一实现三种遍历方法的实现呢?答案是肯定的。只要我们为访问的每一个结点附加上一个可以用来判断其是否是第二次访问的信息即可。这种统一模板的方法有两种。
顾名思义,这种方法就是给每一个结点附加一个额外的标志位,如果没访问过,标志位置为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;
}
}
}
与设置结点访问标志法一样,这种方法也是通过引入某种信息来判断某一个在栈内的结点是否已访问过,不过该方法没有定义一个新类,而是对于每一个未访问过的结点,除了将其本身入栈外,还额外压栈一个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;
}
}
上述所有非递归方法都使用了一个额外的栈结构,因此其空间复杂度均为O(N)。那么是否有一种不使用额外空间,使得空间复杂度降为O(1)的遍历方法呢?确实存在这样的方法,这就是Morris遍历法,这也是线索二叉树所使用的结构,详见Threaded binary tree.
该方法的思路简单说就是,对于每一个结点,找到它左孩子的最右子结点,因为按照正常访问顺序,其左孩子的最有子节点访问完后就应该访问其本身了,因此将其左孩子最右子节点的右指针指向它。
基本步骤如下:
注意:该方法虽然保证了O(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;
}
}
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;
}
}
与前序和中序不同,因为添加的指向连接只是从一个结点的左孩子访问结束后指向该结点,而后序遍历需要在访问完不止左孩子还有右孩子之后才处理父结点,因此需要一些特殊操作。
跟后序遍历的一般迭代法类似,同样可以利用后序遍历与前序遍历的部分逆向性,对前序遍历的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;
}
}
如果纯粹的按照需要的遍历顺序使用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;
}
}