[二叉树遍历递归和迭代实现整理]144. 二叉树的前序遍历 94. 二叉树的中序遍历 145. 二叉树的后序遍历

[二叉树遍历递归和迭代实现整理]144. 二叉树的前序遍历 94. 二叉树的中序遍历 145. 二叉树的后序遍历

  • 前序遍历
    • 概念 + 举例
    • 习题
      • 递归实现
      • 迭代实现:只有右孩子入栈(推荐)
  • 中序遍历
    • 概念 + 举例
    • 习题
      • 递归实现
      • 迭代实现1:辅助栈,左孩子(根)入栈
      • 迭代实现2:morris遍历,线索二叉树
        • 为什么会两次遍历到同一个root?
  • 后序遍历
    • 概念 + 举例
    • 习题
      • 递归实现
      • 迭代实现
        • 写法1:前序遍历改成先右再左,res逆序输出(投机取巧,面试不适用)
        • 写法2:使用额外节点记录状态(代码框架和前序、中序迭代实现相同,推荐)

前序遍历

概念 + 举例

前序遍历:对于当前节点,先输出该节点,然后输出他的左孩子,最后输出他的右孩子。

例如:
[二叉树遍历递归和迭代实现整理]144. 二叉树的前序遍历 94. 二叉树的中序遍历 145. 二叉树的后序遍历_第1张图片
前序遍历序列:[1,2,4,6,7,3,5]

习题

题目链接:144. 二叉树的前序遍历

递归实现

二叉树遍历的递归实现都很简单,只需要按上面所述的前序遍历的概念出发编写代码即可:

先树根,然后左子树,然后右子树。每棵子树递归。

在递归函数最前面设置好递归出口:当root == null,退出当前递归。

class Solution {
    List<Integer> res = new ArrayList<>();
    public List<Integer> preorderTraversal(TreeNode root) {
        if(root == null) return res;
        //每个节点都做下面这三步
        res.add(root.val);//输出树根
        preorderTraversal(root.left);//再输出左孩子
        preorderTraversal(root.right);//最后输出右孩子

        return res;
    }
}

迭代实现:只有右孩子入栈(推荐)

前、中、后序遍历的迭代实现有很多种写法,我这里总结了一种统一的代码框架适用于三种遍历,只需要做简单的修改即可。

参考:题解 by jason

关键问题:
1、遍历思路和栈的配合
前序遍历是从根节点开始,不断向左孩子遍历,遇到一个节点就将立即将其输出,而右子树是在一层层返回时才输出的,所以迭代每一轮实际上就是递归的一层。递归有内部栈可以暂存还未访问的节点,迭代则需要我们显式地将每一层暂时访问不到的节点存入栈中。

对于前序遍历来说,每一层都是先输出根、左子树,最后再输出右子树,所以每一层都把暂时访问不到的右孩子存入栈中,在返回上一层时再取出。

选择栈作为辅助空间的原因是:栈的存储特点是先入后出,和二叉树遍历的一层层深入和一层层返回相契合。

2、什么时候是遍历的终点?
只用栈来存右孩子,所以栈为空时,有两种可能:1、初始状态;2、所有节点都已访问过。所以还要再找一个条件来确定遍历终点。

当栈为空,且在内部while循环最后执行了 root = root.left之后,如果到了遍历终点,就是root == null,所以增加一个条件:root == null。

所以当

	stack.isEmpty() && root == null 

说明到达遍历的终点。

实现代码:

class Solution {
    List<Integer> res = new ArrayList<>();
    public List<Integer> preorderTraversal(TreeNode root) {
        if(root == null) return res;
        Stack<TreeNode> stack = new Stack<>();
        while(!stack.isEmpty() || root != null){
        	//不断向左向下遍历到最左节点
            while(root != null){
                res.add(root.val);//先输出根节点
                if(root.right != null) stack.push(root.right);//将这一层暂时访问不到的右孩子存入栈中
                root = root.left;//将root更新为左孩子
            }
            //退出循环时,说明到了二叉树的最底层最左边节点
            if(!stack.isEmpty()) root = stack.pop();//取出当前层对应的右孩子,重复上面的步骤
        }
        return res;
    }
}

中序遍历

概念 + 举例

中序遍历:对于当前节点,先输出它的左孩子,然后输出节点本身,最后输出它的右孩子。

例如:
[二叉树遍历递归和迭代实现整理]144. 二叉树的前序遍历 94. 二叉树的中序遍历 145. 二叉树的后序遍历_第2张图片
中序遍历序列:[4,6,7,2,1,5,3]

习题

题目链接:94. 二叉树的中序遍历

递归实现

按上面所述的中序遍历的概念出发编写代码即可:

先左子树,然后根,然后右子树。每棵子树递归。

在递归函数最前面设置好递归出口:当root == null,退出当前递归。

class Solution {
    List<Integer> res = new ArrayList<>();
    public List<Integer> inorderTraversal(TreeNode root) {
        if(root == null) return res;
        inorderTraversal(root.left);
        res.add(root.val);
        inorderTraversal(root.right);
        return res; 
    }
}

迭代实现1:辅助栈,左孩子(根)入栈

算法设计:
因为中序遍历先访问的是左孩子,然后才是根和右孩子,又因为右孩子可以通过root.right获取到,所以只需要将每一层的根存入栈中即可。

对每个节点,都是先不断向左向下遍历,每一层节点都把它的根节点存入栈中(右孩子可以通过根节点找到,所以可以不必入栈),直到节点的左孩子为空时,说明到达了最底层的最左节点;然后才开始弹出栈顶,访问最左节点,然后取当前节点的右孩子(root = root.right,关键一步),回到 最开始的步骤:

  • 如果右孩子不为空,则将右孩子存入栈中,继续遍历以右孩子为根的左子树;(相当于遍历右子树)
  • 如果右孩子为空 ,则直接将栈的下一个栈顶弹出,得到的是当前节点的根。

遍历终点:
同前序遍历的迭代实现,当栈为空且root==null时,说明到达遍历终点。

实现代码:

class Solution {
    List<Integer> res = new ArrayList<>();
    public List<Integer> inorderTraversal(TreeNode root) {
        if(root == null) return res;
        Stack<TreeNode> stack = new Stack<>();
        while(!stack.isEmpty() || root != null){
            while(root != null){
                stack.push(root);
                root = root.left;
            }
            root = stack.pop();
            res.add(root.val);
            //关键一步
            root = root.right;//无论root.right是否为Null,都执行这一步,如果为null,则不会进入while循环寻找左孩子,直接跳到pop处,继续弹出上一层的根节点;如果不为null,则右孩子入栈,下一轮以右孩子为根继续寻找他的左孩子。
            
        }
        return res; 
    }
}

迭代实现2:morris遍历,线索二叉树

参考:官方题解的评论区 java代码,官方题解修改了树的结构,而评论处的代码恢复了原来的二叉树结构。

morris遍历,简单来说就是构造线索二叉树。morris遍历是迭代实现中序遍历,同时可以省去栈的使用,用当前节点的左子树最右节点的右指针指向当前节点,对每个节点都做这样的处理,就能得到一棵线索二叉树,在遍历到叶子节点时,通过叶子节点的right就能到达它的中序遍历下的后继节点。本质上就是把空闲的指针域利用起来,指向当前节点的中序遍历直接后继节点,在需要栈弹出该节点的时候,通过当前节点的右指针就能获取该节点。

例如:
[二叉树遍历递归和迭代实现整理]144. 二叉树的前序遍历 94. 二叉树的中序遍历 145. 二叉树的后序遍历_第3张图片

  • morris遍历的空间复杂度为O(1) 。

算法流程:(99题的官方题解)
假设当前遍历到的节点为 x:

  • 如果 x 无左孩子,则访问 x 的右孩子,即令 x = x.right。
  • 如果 x 有左孩子,则找到 x 左子树上最右的节点(即左子树中序遍历的最后一个节点,也是x 在中序遍历中的前驱节点),我们记为 pre。根据 pre 的右孩子是否为空,进行如下操作:
    • 如果 pre 的右孩子为空,则将其右孩子指向 x,然后继续访问 x 的左孩子,即 x = x.left。
    • 如果 pre 的右孩子不为空,则此时其右孩子指向的是 x,说明我们已经遍历完 x 的左子树,我们将 pre 的右孩子置空(恢复原来的树结构),然后访问 x 的右孩子,即 x = x.right。
      重复上述操作,直至访问完整棵树。

照着代码分析上述步骤思路会更清晰。

为什么会两次遍历到同一个root?

第一次和其他中序遍历的流程一样,是正常的访问,如果此时root有左子树,则还不能输出root.val;

第二次是因为在前一次遇到root时将它的前驱节pre点(root的左子树的最右节点)的右指针域指向了root,所以在该前驱节点作为当前节点root时输出并取它的右孩子(root = root.right),就第二次遇到了前一个root,也就是 pre 的后继节点。此时再次遇到root时,说明root的左子树全部输出完毕,所以 root.val 可以输出了,然后前往root的右子树重复左子树上所做的操作。

之后这个前驱节点 pre 和后继节点 root 之间的联系不会再用到了,所以需要把它的前驱节点 pre 的右指针域恢复为null,避免改变树的结构。

实现代码:

class Solution {
    List<Integer> res = new ArrayList<>();
    public List<Integer> inorderTraversal(TreeNode root) {
        if(root == null) return res;
        //当前节点
        TreeNode cur = root;
        //前驱节点
        TreeNode pre = null;
        while(root != null){
            //如果当前节点root没有左子树,则输出该节点,然后进入右子树(如果有右子树则进入右子树,没有右子树则取之前构造的直接后继节点)
            if(root.left == null){
                res.add(root.val);
                root = root.right;
            }
            //如果当前节点root有左子树,则:
            else{
                pre = root.left;//左子树的根节点
                //寻找左子树的最右节点(前驱节点)
                while(pre.right != null && pre.right != root){
                    pre = pre.right;
                }

                //如果前驱节点的right没有被赋值过,则对它的右指针域赋值(第一次访问root)
                if(pre.right == null){
                    pre.right = root;//将最右节点的右指针域指向root,pre就是root的前驱,root是pre的后置。
                    root = root.left;//当前节点root进入左子树
                }
                //如果前驱节点的right被赋值过,则在第二次遍历到当前节点root时,该节点的前驱的右指针域要恢复回null,避免修改树的结构(第二次访问root,可以输出root)
                if(pre.right == root){//整棵线索二叉树只有这个前驱节点的right指向当前的root,所以可以用这个条件判断pre是否是root的前驱节点
                    pre.right = null;//恢复前驱节点的右指针域
                    res.add(root.val);//第二次遇到root,说明root的左子树已经输出完毕,根据中序遍历的特点此时可以输出root.val
                    root = root.right;//进入右子树
                }

            }
        }

        return res; 
    }
}

后序遍历

概念 + 举例

后序遍历:对于当前节点,先输出它的左孩子,然后输出它的右孩子,最后输出节点本身。

例如:
[二叉树遍历递归和迭代实现整理]144. 二叉树的前序遍历 94. 二叉树的中序遍历 145. 二叉树的后序遍历_第4张图片
后序遍历序列:[7,6,4,2,5,3,1]

习题

题目链接:145. 二叉树的后序遍历

递归实现

按上面所述的中序遍历的概念出发编写代码即可:

先左子树,然后右子树,然后根。每棵子树递归。

在递归函数最前面设置好递归出口:当root == null,退出当前递归。

class Solution {
    List<Integer> res = new ArrayList<>();
    public List<Integer> postorderTraversal(TreeNode root) {
        if(root == null) return res;
        postorderTraversal(root.left);
        postorderTraversal(root.right);
        res.add(root.val);
        return res;
    }
}

迭代实现

写法1:前序遍历改成先右再左,res逆序输出(投机取巧,面试不适用)

节点 cur 先到达最右端的叶子节点并将路径上的节点入栈;
然后每次从栈中弹出一个元素后,cur 到达它的左孩子,并将左孩子看作 cur 继续执行上面的步骤。

从算法流程可以发现,是前序遍历将先左后右改成先右后左即可,

前序:根->左->右,
修改成:根->右->左,
最后逆序输出:左->右->根,

得到逻辑上的后序遍历,但实际上并不是真正的后序遍历。

最后将结果反向输出即可:

Collections.reverse(res);

实现代码:

class Solution {
    List<Integer> res = new ArrayList<>();
    public List<Integer> postorderTraversal(TreeNode root) {
        if(root == null) return res;
        Stack<TreeNode> stack = new Stack<>();
        TreeNode last = null;
        while(!stack.isEmpty() || root != null){
            while(root != null){
                res.add(root.val);
                if(root.left != null) stack.push(root.left);
                root = root.right;
            }
            if(!stack.isEmpty()) root = stack.pop();
        }
        //将结果逆序输出
        Collections.reverse(res);
        return res;
    }
}
写法2:使用额外节点记录状态(代码框架和前序、中序迭代实现相同,推荐)

后序遍历最开始要从根节点不断深度遍历到最左节点,这个过程中root不断更新为自身的左孩子root.left,所以二叉树每一层的当前root都要入栈,直到到达最左节点。

到达最左节点后开始向上返回,弹出栈顶,也就是当前的root。如果节点还有右孩子,则还要继续遍历右子树,令root=root.right(和中序遍历相同);右子树已遍历过,或者没有右子树,才输出当前节点。所以可以发现有两种返回当前root的情况:

  1. 右子树还未遍历:令root=root.right,继续遍历右子树;
  2. 右子树已遍历或没有右子树:输出root.val:res.add(root.val)。

关键问题:如何判断返回当前root的是哪一种情况?
方案1:使用队列记录状态变量(比较麻烦)

方案2:对res进行判断(基于对输出结果的理解)
以下面的二叉树为例:

    1
  2   3
 4 5 6 7

假设此时已输出4,返回上一层的2,即root=2,
因为此时还没有访问2的右子树,所以要继续将2压入栈中,然后令root=root.right以便回到之前的while循环遍历右子树。
输出右孩子5之后,此时的res[4,5],弹出栈顶2,相当于又返回root=2的情况,此时2的右子树已访问完毕,所以可以输出2,。

可以发现如果没有访问过右子树,res的最后一个元素不是root.right.val,而如果访问过,则根据后序遍历的特点,在返回root时,res的最后一个元素一定是root.right.val。所以可以通过res.get(res.size() - 1) == root.right.val来判断是否访问过右子树。

前面说到过还有一种情况:当前root没有右子树。也要归入访问过右子树的条件中。

所以后序遍历迭代实现的关键部分整理得:

  root = stack.pop();//取出栈顶
  if(root.right == null || res.get(res.size()) == root.right.val){
      res.add(root.val);
      root=null;//(易遗漏!!)
  }
  else{
      stack.push(root);//头结点要重新压回栈中
      root = root.right;
  }
  • 令root = null 的目的:为了在进入下一轮循环时能够跳过内层while循环,继续弹出栈顶,而不是再次进入内层while循环重新对root的左子树做遍历。(易遗漏!!!

实现代码:

class Solution {
    List<Integer> res = new ArrayList<>();
    public List<Integer> postorderTraversal(TreeNode root) {
        if(root == null) return res;
        Stack<TreeNode> stack = new Stack<>();
        TreeNode last = null;
        while(!stack.isEmpty() || root != null){
            while(root != null){
                stack.push(root);
                root = root.left;
            }
            //取出栈顶
            root = stack.pop();
            //如果root没有右子树或右子树被访问过
            if(root.right == null || (res.size() > 0 && res.get(res.size() - 1) == root.right.val)){
                res.add(root.val);
                root = null;//(易遗漏!!)
            }
            //如果root的右子树还未被访问过
            else{
                stack.push(root);//root重新入栈(易遗漏)
                root = root.right;//继续遍历右子树
            }
        }
        return res;
    }
}

方法3:设置辅助节点指向遍历的前一个节点
设置一个节点last,指向当前root的右孩子,初始值设为null。
从左子树返回root时last == null 且root.right!=null,所以会继续遍历右子树,在遍历右子树时更新last为root.right;
从右子树返回root后,因为last==root.right,说明是从右子树返回的,可以输出root.val。

以下面的二叉树为例:

    1
  2   3
 4 5 6 7

此时已输出4,返回上一层的2,即root=2,此时last=null,
可以认为此时还没有访问2的右子树,所以要继续将2压入栈中,然后令last=root.right,root=root.right以便回到之前的while循环遍历右子树。
输出右孩子5之后,弹出栈顶2,相当于又返回root=2的情况,此时2的右子树已访问完毕,即last==root.right,所以可以输出2。

实际上方法3和方法3本质相同,都是基于后序遍历的特点,后序遍历输出root的前一个节点必定是root.right(前提:不为null)。

前面说到过还有一种情况:当前root没有右子树。也要归入访问过右子树的条件中。

所以整理得:

    root = stack.pop();//取出栈顶
    if(root.right == null || last == root.right){
        res.add(root.val);
        last = root;//更新last为当前节点
        root=null;//这一步是为了能够继续弹出栈顶,而不是重新回到之前的while循环重复对root的左子树做遍历。
    }
    else{
        stack.push(root);//头结点要重新压回栈中
        root = root.right;
    }

实现代码:

class Solution {
    List<Integer> res = new ArrayList<>();
    public List<Integer> postorderTraversal(TreeNode root) {
        if(root == null) return res;
        Stack<TreeNode> stack = new Stack<>();
        TreeNode last = null;
        while(!stack.isEmpty() || root != null){
            while(root != null){
                stack.push(root);
                root = root.left;
            }
            //取出栈顶
            root = stack.pop();
            //如果root没有右子树或右子树被访问过
            if(root.right == null || last == root.right){
                res.add(root.val);
                last = root;更新last为当前节点,易遗漏!!
                root = null;
            }
            //如果root的右子树还未被访问过
            else{
                stack.push(root);//root重新入栈(易遗漏)
                root = root.right;//继续遍历右子树
            }
        }
        return res;
    }
}

你可能感兴趣的:([二叉树遍历递归和迭代实现整理]144. 二叉树的前序遍历 94. 二叉树的中序遍历 145. 二叉树的后序遍历)