递归是一种编程技巧,在许多数据结构和算法中都用了递归进行实现,如果要学习后面相对比较复杂的数据结构与算法,掌握递归非常重要。
我们先来看一个例子,比如我们在电影院看电影,想知道自己坐在第几排,但是电影院太黑没法自己数,于是我们问前面的人他在第几排,但是前面的人也不知道,所以他也问前面的人,依次类推,直到问到第一排的人,然后再一排一排的把数字传回来,每次加 1,最后你就知道具体在哪一排了。
这就是一个非常标准的递归求解问题的分解过程,去问的过程叫做 “递”,回答的过程叫做 “归”。
大部分递归问题都可以用递归公式进行表示,以上面电影院的例子来说。递归公式如下:
f ( n ) = f ( n − 1 ) + 1 f(n) = f(n-1)+1 f(n)=f(n−1)+1 其中, f ( 1 ) = 1 f(1) = 1 f(1)=1
有了上面的公式,我们就可以写出递归的代码
private static int f(int n) {
if (n == 1) {
return 1;
}
return f(n - 1) + 1;
}
那究竟什么样的问题可以用递归来解决呢?只要同时满足以下三个条件,就可以用递归来解决。
1.一个问题的解可以分解为几个子问题的解
何为子问题?子问题就是数据规模更小的问题。比如,前面讲的电影院的例子,你要知道,“自己在哪一排”的问题,可以分解为“前一排的人在哪一排”这样一个子问题。
2.这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一样
比如电影院那个例子,你求解“自己在哪一排”的思路,和前面一排人求解“自己在哪一排”的思路,是一模一样的。
3.存在递归终止条件
把问题分解为子问题,把子问题再分解为子子问题,一层一层分解下去,不能存在无限循环,这就需要有终止条件。
还是电影院的例子,第一排的人不需要再继续询问任何人,就知道自己在哪一排,也就是 f ( 1 ) = 1 f(1)=1 f(1)=1,这就是递归的终止条件。
有了上面的理论,我们再分析一个递归的例子,假设有 n 个台阶,每次可以跨 1 个台阶或者 2 个台阶,走完这个 n 个台阶有多少种走法?
我们先分析最简单的
假设 n = 1,那么我们只有一种走法,走 1 个台阶。
假设 n = 2,那么我们只有两种走法,走两次 1 个台阶或者一次走 2 个台阶。
假设 n = 3,我们现在已经走了第一步了,如果第一步走了 1 个台阶,那么我们需要考虑剩下的 2 个台阶的走法,其实就是 n=2 的走法,如果第一步走了 2 个台阶,那么我们只能走一步,也就是 n=1的走法,所以可以得到公式: f ( 3 ) = f ( 2 ) + f ( 1 ) f(3) =f(2)+f(1) f(3)=f(2)+f(1)。
假设现在有 n 阶台阶,我们的走法,就是 n-1 阶的走法加上 n-2 阶的走法
用递归公式表示
f ( n ) = f ( n − 1 ) + f ( n − 2 ) f(n)=f(n-1)+f(n-2) f(n)=f(n−1)+f(n−2)
有了递归公式,接下来我们需要找到递归终止的条件
终止条件有两个
最终的代码如下:
private static int f(int n) {
if (n == 1) {
return 1;
}
if (n == 2) {
return 2;
}
return f(n - 1) + f(n - 2);
}
我们是否可以将递归代码改写成非递归代价呢?
private static int f2(int n) {
int res = 0;
for (int i = 0; i < n; i++) {
res++;
}
return res;
}
private static int f2(int n) {
if (n == 1) {
return 1;
}
if (n == 2) {
return 2;
}
int res = 0;
int n_minus_1 = 2;// 假设 n=3,n-1=2 个台阶有2种走法
int n_minus_2 = 1;// 假设 n=3,n-2=1 个台阶有1种走法
for (int i = 3; i <= n; i++) {
res = n_minus_1 + n_minus_2;
n_minus_2 = n_minus_1;// 假设 n=4,第二次循环,4-2 = 2 有2种走法
n_minus_1 = res;// 假设 n=4,第二次循环,4-1=3 有 3(上一次的2+1) 种走法
}
return res;
}
通过上面两个例子可以看出,非递归的写法,不仅多出很多代码,而且非常难以理解,但是本质上和递归是一样的,只是"手动"进行递归。
递归有利有弊,利是递归代码的表达力很强,写起来非常简洁;而弊就是空间复杂度高、有堆栈溢出的风险、存在重复计算、过多的函数调用会耗时较多等问题。