【二叉树 1.1】递归的写法,二叉树遍历,用递归写二叉树的三种遍历

151 阅读10分钟

文章目录

递归三要素

这次我们要好好谈一谈递归,为什么很多同学看递归算法都是“一看就会,一写就废”。

主要是对递归不成体系,没有方法论,「每次写递归算法 ,都是靠玄学来写代码」,代码能不能编过都靠运气。

「本篇将介绍前后中序的递归写法,一些同学可能会感觉很简单,其实不然,我们要通过简单题目把方法论确定下来,有了方法论,后面才能应付复杂的递归。」

这里帮助大家确定下来递归算法的三个要素。「每次写递归,都按照这三要素来写,可以保证大家写出正确的递归算法!」

  1. 「确定递归函数的参数和返回值:」其一,确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数;其二,明确每次递归的返回值是什么,进而确定递归函数的返回类型。
  2. 「确定递归的终止条件:」写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
  3. 「确定单层递归的逻辑:」确定每一层递归需要处理的信息,在这里也就会重复调用自己来实现递归的过程。

递归实践,前序遍历

好了,我们确认了递归的三要素,接下来就来练练手:

「以下以前序遍历为例:」

「确定递归函数的参数和返回值」:
参数vec:因为要打印出前序遍历节点的数值,所以参数里需要传入vector在放节点的数值,
参数root:这个和链表的p指针一样,不断移动,要做参数传递
除了这一点就不需要在处理什么数据了也不需要有返回值,所以递归函数返回类型就是void,代码如下:

void traversal(TreeNode* cur, vector<int>& vec)

「确定终止条件」:在递归的过程中,如何算是递归结束了呢,当然是当前遍历的节点是空了,那么本层递归就要要结束了,所以如果当前遍历的这个节点是空,就直接return,代码如下:

if (cur == NULL) return;

「确定单层递归的逻辑」:前序遍历是中左右的循序,所以在单层递归的逻辑,是要先取中节点的数值,代码如下:

vec.push_back(cur->val);    // 中
traversal(cur->left, vec);  // 左
traversal(cur->right, vec); // 右

单层递归的逻辑就是按照中左右的顺序来处理的,这样二叉树的前序遍历,基本就写完了。

144.二叉树的前序遍历

问题描述

给你二叉树的根节点 root ,返回它节点值的 前序 遍历。

示例 1:
输入:root = [1,null,2,3]
输出:[1,2,3]

示例 2:
输入:root = []
输出:[]

示例 3:
输入:root = [1]
输出:[1]

示例 4:
输入:root = [1,2]
输出:[1,2]

示例 5:
输入:root = [1,null,2]
输出:[1,2]

解决方案1(前序递归,两种方式时间空间复杂度都是相同的)

时间复杂度:O(n),其中 n 是二叉搜索树的节点数。每一个节点恰好被遍历一次。

空间复杂度:O(n),为递归过程中栈的开销,平均情况下为 O(logn),最坏情况下树呈现链状,为 O(n)。

class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
      List<Integer> list = new ArrayList<>();
      preorderTraversal(root,list);
      return list;
    }
    public void preorderTraversal(TreeNode root,List<Integer> list){
        if(null == root) return;  // 这里只要返回一个结束标志就好了,不一定要返回值
        // 这个null==root的必须放在前面,防止后面三句 root.val root.left root.right出现空指针
        list.add(root.val);   // 中
        preorderTraversal(root.left,list);  // 左
        preorderTraversal(root.right,list); // 右
    } 
}

为什么这里不直接使用给定的preorderTraversal(TreeNode root)做递归,因为参数不允许,给定方法的参数中没有一个list,这个list是单次递归必须发生改变的变量。

class Solution {
    List<Integer> list = new ArrayList<>();
    public List<Integer> preorderTraversal(TreeNode root) {
        if(null == root) return list;  // 这里只要返回一个结束标志就好了,不一定要返回值
        // 这个null==root的必须放在前面,防止后面三句 root.val root.left root.right出现空指针
        list.add(root.val);   // 中   修改list
        preorderTraversal(root.left);  // 左  修改list   
        preorderTraversal(root.right); // 右  修改list
        return list;
    }
}

提供三点:
1、这样写更好,只要一个方法,把list提取出来,当成类变量就可以了,用给定方法递归;
2、其实,preorderTraversal(root.left); 和写成 list=preorderTraversal(root.left); 是一样的,在leetcode都是可以提交通过的,因为list是类变量,单向递归中list.add(root.val)已经修改了这个类变量list,所以接不接收返回值一个样,接收了也是修改类变量,不接收类变量list也已经被修改了。
3、当root为空的时候,即二叉树没有节点的时候:写成 if(null == root) return list; 而不是 if(null == root) return null; 当输入为[],前者可以正确返回 [] ,后者会返回null,返回结果出错。
4、当root不为空的时候,即二叉树至少有一个节点的时候:关于 if(null == root) return list; 是没关系的,是安全的,因为如果递归的时候遇到 if(null == root) return list; 只会返回给递归子调用(虽然子调用接不接收无所谓),不会直接跳出整个Solution方法。

解决方案2(前序迭代,两种方式时间空间复杂度都是相同的)

我们也可以用迭代的方式实现方法一的递归函数,两种方式是等价的,区别在于递归的时候隐式地维护了一个栈,而我们在迭代的时候需要显式地将这个栈模拟出来,其余的实现与细节都相同,具体可以参考下面的代码。

时间复杂度:O(n),其中 nn 是二叉搜索树的节点数。每一个节点恰好被遍历一次。

空间复杂度:O(n),为迭代过程中显式栈的开销,平均情况下为 O(logn),最坏情况下树呈现链状,为 O(n)。

class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        if(root==null)
            return list;

        TreeNode node = root;
        Stack<TreeNode> stack=new Stack<>();
        while (node!=null || !stack.isEmpty()) {  // 这里一定要写成node!=null 因为node相当于p指针,是不断改变的,root相当于head,是不变的
            while (node != null) {   // 这里一定是while,不是if
                list.add(node.val);  // 中一句
                stack.push(node);   // 左两句,移动指针一定要放在后面
                node = node.left;
            }
            // 右两句,移动指针一定要放在后面
            node = stack.pop();  // 此时node==null,所以从stack中弹出一个来,就可以取到node.right了,否则空指针
            node = node.right;
        }
        return list;
    }
}

类似二叉树:先将中间节点及其左孩子全部入栈,知道最左端,没有元素了,再开始pop push pop push

解决方案3(前序遍历,先pop中间元素,在push两个孩子,先右孩子,再左孩子)

因为是直接push两个孩子,然后在pop出孩子,所以push和pop孩子顺序完全相反,先push右孩子,再push左孩子

 class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        Stack<TreeNode> stack=new Stack<>();
        if(root!=null)
           stack.push(root);    

        while (!stack.isEmpty()) { 
           TreeNode node=stack.pop();
           if(null != node){
               list.add(node.val);  // 先list中加入一个,然后push右孩子、push左孩子
               stack.push(node.right);
               stack.push(node.left);
           }
        }
        return list;
    }
}

解决方案4(前序迭代,统一模板)

全部入栈,最后遇到null(即没有了元素才出栈)

 class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        Stack<TreeNode> stack=new Stack<>();
        List<Integer> list=new ArrayList<>();
        if (root!=null) stack.push(root);
        while(!stack.isEmpty()){
            TreeNode node=stack.peek();  // 还是使用node操作,不使用root操作
            if (node!=null){
                stack.pop();
                // 先pop,在push四个
                if (node.right!=null) stack.push(node.right);
                if (node.left!=null) stack.push(node.left);
                stack.push(node);     // stack中放入的永远是节点,list中放入的才是节点val
                stack.push(null); 
            }else{
                stack.pop();
                node=stack.pop();
                list.add(node.val); // 出栈才放入的到list中
            }
        }
        return list;
    }
}

关于统一遍历法两个要注意的点:
第一,先全部入栈,直到没有了,直到为null才开始出栈,所以入栈顺序和出栈顺序完全相反
前序遍历顺序:中左右,所以前序入栈顺序:右左中;
中序遍历顺序:走中右,所以中序入栈顺序:右中左;
后序遍历顺序:左右中,所以后序入栈顺序:中右左。
第二,不断入栈,直到node==null,全部元素入栈完毕才是出栈,开始pop,开始result.add

94.二叉树的中序遍历

问题描述

给定一个二叉树的根节点 root ,返回它的 中序 遍历。

示例 1:
输入:root = [1,null,2,3]
输出:[1,3,2]

示例 2:
输入:root = []
输出:[]

示例 3:
输入:root = [1]
输出:[1]

示例 4:
输入:root = [1,2]
输出:[2,1]

示例 5:
输入:root = [1,null,2]
输出:[1,2]

解决方案1(中序递归,两种方式时间空间复杂度都是相同的)

class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
      List<Integer> list = new ArrayList<>();
      inorderTraversal(root,list);
      return list;
    }
    public void  inorderTraversal(TreeNode root,List<Integer> list){
        // 这里面的root是直接移动的,不借用node工作指针
        if (root==null) return;
        inorderTraversal(root.left,list);
        list.add(root.val);
        inorderTraversal(root.right,list);
    }
}

可以修改为一个方法,更好,如下:

class Solution {
    List<Integer> list = new ArrayList<>();
    public List<Integer> inorderTraversal(TreeNode root) {
        // 这里面的root是直接移动的,不借用node工作指针
        if (root==null) return list;
        inorderTraversal(root.left);
        list.add(root.val);
        inorderTraversal(root.right);
        return list;
    }
}

解决方案2+解决方案3(中序迭代,两种方式时间空间复杂度都是相同的)

我们也可以用迭代的方式实现方法一的递归函数,两种方式是等价的,区别在于递归的时候隐式地维护了一个栈,而我们在迭代的时候需要显式地将这个栈模拟出来,其余的实现与细节都相同,具体可以参考下面的代码。

时间复杂度:O(n),其中 nn 是二叉搜索树的节点数。每一个节点恰好被遍历一次。

空间复杂度:O(n),为迭代过程中显式栈的开销,平均情况下为 O(logn),最坏情况下树呈现链状,为 O(n)。

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

解决方案4(中序迭代,统一模板)

class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        Stack<TreeNode> stack=new Stack<>();
        List<Integer> list=new ArrayList<>();
        if (root!=null) stack.push(root);
        while(!stack.isEmpty()){
            TreeNode node=stack.peek();  // 还是使用node操作,不使用root操作
            if (node!=null){
                stack.pop();
                // 先pop,在push四个
                if (node.right!=null) stack.push(node.right);
                stack.push(node);     // stack中放入的永远是节点,list中放入的才是节点val
                stack.push(null); 
                if (node.left!=null) stack.push(node.left);
            }else{
                stack.pop();
                node=stack.pop();
                list.add(node.val); // 出栈才放入的到list中
            }
        }
        return list;
    }
}

145.二叉树的后序遍历

问题描述

给定一个二叉树,返回它的 后序 遍历。

示例:

输入: [1,null,2,3]
输出: [3,2,1]

解决方案1(后序递归,两种方式时间空间复杂度都是相同的)

class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> list=new ArrayList<>();  // 返回值为list,但是参数中没有list,所以必须新建一个list盛放返回值
        postorderTraversal(root,list);
        return list;
    }
    public void postorderTraversal(TreeNode root,List list){   // 递归方法,root是不断移动的指针,每次递归都要改变的,list是存放遍历结果的集合,每次递归都要改变的
        if (null == root) return;  // null==root判断要在三个之前,防止空指针   return;返回一个完成标志就好了
        postorderTraversal(root.left,list);  // 左
        postorderTraversal(root.right,list);  // 右
        list.add(root.val);  // 中
    }
}

合并成一个方法,更好,如下:

class Solution {
    List<Integer> list=new ArrayList<>();  // 返回值为list,但是参数中没有list,所以必须新建一个list盛放返回值
    public List<Integer> postorderTraversal(TreeNode root) {
        if (null == root) return list;  // null==root判断要在三个之前,防止空指针   return;返回一个完成标志就好了
        postorderTraversal(root.left);  // 左
        postorderTraversal(root.right);  // 右
        list.add(root.val);  // 中
        return list;
    }
}

解决方案2(后序迭代,两种方式时间空间复杂度都是相同的,略)

我们也可以用迭代的方式实现方法一的递归函数,两种方式是等价的,区别在于递归的时候隐式地维护了一个栈,而我们在迭代的时候需要显式地将这个栈模拟出来,其余的实现与细节都相同,具体可以参考下面的代码。

时间复杂度:O(n),其中 nn 是二叉搜索树的节点数。每一个节点恰好被遍历一次。

空间复杂度:O(n),为迭代过程中显式栈的开销,平均情况下为 O(logn),最坏情况下树呈现链状,为 O(n)。

解决方案3(后序遍历,前序遍历的第三种方案,然后反转)

  class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        Stack<TreeNode> stack=new Stack<>();
        if(root!=null)
           stack.push(root);    

        while (!stack.isEmpty()) { 
           TreeNode node=stack.pop();
           if(null != node){
               list.add(node.val);  // 先list中加入一个,然后push右孩子、push左孩子
               stack.push(node.left);
               stack.push(node.right);
           }
        }
        // 执行前:中右左,指向后:左右中
        Collections.reverse(list);
        return list;
    }
}

解决方案4(后序迭代,统一模板)

class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        Stack<TreeNode> stack=new Stack<>();
        List<Integer> list=new ArrayList<>();
        if (root!=null) stack.push(root);
        while(!stack.isEmpty()){
            TreeNode node=stack.peek();  // 还是使用node操作,不使用root操作
            if (node!=null){
                stack.pop();
                stack.push(node);     // stack中放入的永远是节点,list中放入的才是节点val
                stack.push(null);
                if (node.right!=null) stack.push(node.right);
                if (node.left!=null) stack.push(node.left);
            }else{
                stack.pop();
                node=stack.pop();
                list.add(node.val); // 出栈才放入的到list中
            }
        }
        return list;
    }
}