介绍
本文是学习《数据结构与算法之美》中关于递归的学习笔记,后面是刷了几道leetCode中跟递归相关的算法题目。
int f(int n) {
if (n == 1) return 1;
return f(n-1) + 1;
}
复制代码
递归需要满足的三个条件
- 一个问题的解可以分解成几个子问题的解
- 这个问题与分解后的子问题,除了数据规模不同,求解思路完全一样
- 存在递归终止条件
如何编写递归代码
写递归代码最关键的是写出递推公式,找到终止条件。 举例子:这里有 n 个台阶,每次你可以跨 1 个台阶或者 2 个台阶,请问走这n个台阶有多少种走法
int f(int n) {
if (n == 1) return 1;
if (n == 2) return 2;
return f(n-1) + f(n-2);
}
复制代码
递归代码的一些问题
- 要警惕堆栈溢出的问题
函数调用会使用栈来保存临时变量。每调用一个函数,都会将临时变量封装为栈帧,压入内存栈中,等函数执行完成返回时,才执行出栈。系统栈或者虚拟机栈空间一般不大,如果递归求解的调用层次很深的话,就会有堆栈溢出的风险。
避免堆栈溢出:在代码中限制递归调用的最大深度,但是这种做法并不能完全解决问题
- 重复计算的问题
为了避免重复计算的问题,可以通过一个数据结构来保存已经求解过的值f(k)。当递归调用到f(k)的时候,先看下是否已经求结过。如果是,直接从散列表中取值返回,不需要重复计算。
- 空间复杂度
在空间复杂度上, 因为递归调用一次就会在内存栈中保存一次数据,所以在分析递归代码的空间复杂度的时候,需要额外考虑这部分的开销。空间复杂度并不是O(1)。
将递归代码改成非递归代码
笼统地讲,所有的递归代码都可以改成迭代循环的非递归写法。
leetcode上的一些题目
- 258 各位相加
- 144 二叉树的前序遍历 : 递归方法实现和非递归方法的实现
- 70 爬楼梯问题
/**
* 258 各位相加
*
* 给定一个非负整数 num,反复将各个位上的数字相加,直到结果为一位数。
*
* 示例:
*
* 输入: 38
* 输出: 2
* 解释: 各位相加的过程为:3 + 8 = 11, 1 + 1 = 2。 由于 2 是一位数,所以返回 2。
*/
public class AddDigits {
public static void main(String[] args) {
System.out.println(addDigits(38));
}
/**
* 递推公式:f(n)=f(n)进行处理后的结果;
* 终止条件:f(n)的结果是一位数
* @return
*/
public static int addDigits(int num) {
return fun(num);
}
public static int fun(int n) {
if (n >= 0 && n < 10) {
return n;
}
int sum = 0;
while (n != 0) {
sum = sum + n % 10;
n = n / 10;
}
return fun(sum);
}
/**
* 不使用循环或者递归,且在 O(1) 时间复杂度内解决这个问题:
* 找出规律,进行换算:
*
* 给定一个非负整数 num,反复将各个位上的数字相加,直到结果为一位数。
* 分析:ab=a*10+b
* ab%9=(a*10+b)%9=(9a+a+b)%9=(a+b)%9
* abc=a*100+b*10+c
* 同理abc%9=(a+b+c)%9
* 观察题目规律,可以看出0…9结果为0…9
* 10…19结果为1…9…1
* ….
* 可以看出除了0以外,所有的结果都是1…9,可以num%9,比如38%9=2;但是18%9=0,18=1+8=9;我们可以做个简单的变换(num-1)%9+1;
*/
public static int addDigits2(int num){
return (num-1)%9+1;
}
}
复制代码
/**
* 前序遍历
* 递归方法:
* * 1.访问根节点
* * 2.前序遍历左子树
* * 3.前序遍历右子树
*/
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
add(result, root);
return result;
}
public void add(List<Integer> list, TreeNode root) {
if (root == null) {
return;
}
list.add(root.val);
if (root.left != null) {
add(list, root.left);
}
if (root.right != null) {
add(list, root.right);
}
}
/**
* 前序遍历:非递归方法
* 需要利用栈:
* 1)访问结点P,并将结点P入栈;
*
* 2)判断结点P的左孩子是否为空,若为空,则取栈顶结点并进行出栈操作,并将栈顶结点的右孩子置为当前的结点P,循环至1);若不为空,则将P的左孩子置为当前的结点P;
*
* 3)直到P为NULL并且栈为空,则遍历结束。
*/
public List<Integer> preorderTraversal2(TreeNode root) {
List<Integer> result = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
TreeNode node = root;
while (node != null || !stack.empty()) {
while (node != null) {
result.add(node.val);
stack.push(node);
//访问左孩子
node = node.left;
}
node = stack.pop();
node = node.right;
}
return result;
}
复制代码
/**
* 70 爬楼梯
* 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
* 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
* 注意:给定 n 是一个正整数。
*/
public class climbStairs {
public static void main(String[] args) {
System.out.println(climbStairs(10));
System.out.println(climbStairs1(10));
}
/**
* 利用递归的方法实现,不过比较耗时,leetcode运行超时
*/
public static int climbStairs(int n) {
if (n == 1) {
return 1;
}
if (n == 2) {
return 2;
}
return climbStairs(n - 1) + climbStairs(n - 2);
}
/**
* 利用临时变量来存放结果,比用临时的数组存储结果快一些
*/
public static int climbStairs1(int n) {
if (n == 1) {
return 1;
}
if (n == 2) {
return 2;
}
int a=1;
int b=2;
int result=0;
while (n>=3){
result=a+b;
a=b;
b=result;
n--;
}
return result;
}
}
复制代码