数据结构与算法之美 | 学习笔记07 ——递归

一、递归

1.实现递归

去的过程为递,回的过程为归。递归要满足的三个条件:

  1. 一个问题可以分解为几个子问题的解;
  2. 问题与子问题,除了数据规模不同,求解思路完全一样
  3. 存在递归终止条件

关键写出递推公式,找到终止条件。
示例:n个台阶,每次可以跨1或2个台阶,请问走这n个台阶有多少种走法?

求解
递推公式:根据第一步的走法把所有走法分两类:第一类是第一步走了1个台阶,之后的走法就是n-1个台阶的走法;第二类是第一步走了2个台阶,之后的走法就是n-2个台阶的走法,因此递归公式为:

f(n) = f(n-1)+f(n-2)

终止条件:先设置终止条件为f(1) = 1,用比较小的数试验一下:n = 2时,f(2) = f(1) + f(0),不符合逻辑,所以加上f(2) = 2。再拿n = 3,n = 4验证一下是否正确。

因此最后递归代码为(有重复计算的问题):

int f(int n) {
     
  if (n == 1) return 1;
  if (n == 2) return 2;
  return f(n-1) + f(n-2);
}

空间复杂度为 O ( n ) O(n) O(n)
对于较复杂的递归问题,例如A分为B, C, D,考虑问题时可以假设子问题已经解决,隐藏掉细节。

2. 警惕堆栈溢出

函数调用会使用栈来保存实时变量,而系统栈或虚拟机栈空间一般不大,当递归数据规模大、调用层次深时,可能有堆栈溢出的风险。如果最大深度较小,例如10、50,可以在递归函数中可以增加判断报错的代码。

3. 警惕重复计算

数据结构与算法之美 | 学习笔记07 ——递归_第1张图片
上面的台阶代码有重复计算的问题,可以通过一个数据结构(例如散列表)来保存已经求解过的f(k):

public int f(int n) {
     
  if (n == 1) return 1;
  if (n == 2) return 2;
  
  // hasSolvedList可以理解成一个Map,key是n,value是f(n)
  if (hasSolvedList.containsKey(n)) {
     
    return hasSolvedList.get(n);
  }
  
  int ret = f(n-1) + f(n-2);
  hasSolvedList.put(n, ret);
  return ret;
}

二、将递归代码改为非递归代码

递归的空间复杂度高,有堆栈溢出风险,可能存在重复计算,过多的函数调用耗时过多,所以依据实际情况决定将递归代码改为非递归代码。
对于台阶问题,修改后为:

int f(int n) {
     
  if (n == 1) return 1;
  if (n == 2) return 2;
  
  int ret = 0;
  int pre = 2;
  int prepre = 1;
  for (int i = 3; i <= n; ++i) {
     
    ret = pre + prepre;
    prepre = pre;
    pre = ret;
  }
  return ret;
}

递归的应用

DFS深度优先搜索,前中后序二叉树遍历等。

递归的调试

  1. 打印日志发现,递归值。
  2. 结合条件断点进行调试。

你可能感兴趣的:(数据结构,数据结构,算法)