文章目录
递归三要素
这次我们要好好谈一谈递归,为什么很多同学看递归算法都是“一看就会,一写就废”。
主要是对递归不成体系,没有方法论,「每次写递归算法 ,都是靠玄学来写代码」,代码能不能编过都靠运气。
「本篇将介绍前后中序的递归写法,一些同学可能会感觉很简单,其实不然,我们要通过简单题目把方法论确定下来,有了方法论,后面才能应付复杂的递归。」
这里帮助大家确定下来递归算法的三个要素。「每次写递归,都按照这三要素来写,可以保证大家写出正确的递归算法!」
- 「确定递归函数的参数和返回值:」其一,确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数;其二,明确每次递归的返回值是什么,进而确定递归函数的返回类型。
- 「确定递归的终止条件:」写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
- 「确定单层递归的逻辑:」确定每一层递归需要处理的信息,在这里也就会重复调用自己来实现递归的过程。
递归实践,前序遍历
好了,我们确认了递归的三要素,接下来就来练练手:
「以下以前序遍历为例:」
「确定递归函数的参数和返回值」:
参数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;
}
}