【Algo】递归算法 recursion algorithm

Backto Algo Index


定义 Definitions

递归(recursion)是一种编程技巧, 体现的是分治思想. 一个函数, 层层下去, 一直到最下面的终止条件, 然后调头向上, 把结果一层一层再上来. 从而得到最终解.

递归是很多数据结构和算法的基础,

  • 比如数据结构对应 栈(Stack)一层一层压下去,LIFO最后出来的就是最终结果,
  • 算法中对应树, 比如二叉树的前中后序遍历.

一个可以使用递归算法求解的问题,需要满足三个条件:

  1. 一个问题可以分为为几个子问题的解. 子问题解决了, 大问题的解自然而然也就有了.
  2. 原问题与子问题, 解决思路一样, 只有数据规模的不同. 原问题的数据可以划分几个部分, 而每个部分可以再划分, 然后所有的划分都可以用相同的思路来求解.
  3. 存在递归终止条件. 不能无穷循环下去, 要有终止条件来停止问题向下的过程, 然后开启向上把结果的过程.

写递归代码, 同上也是分三步走

  1. 分析问题, 写出递推公式
  2. 找到终止条件
  3. 翻译成代码

递归代码, 写完之后通常很好理解, 代码也很简介优美, 但是函数调用会使用栈来保存临时变量, 没调用一个函数, 都会将临时变量封装为栈帧压入内存栈, 等函数执行完成返回时, 才出栈. 如果递归求解数据规模很大, 调用层次很深, 一直 push 入栈, 就会有堆栈溢出的风险. 为了避免堆栈溢出, 通常的做法有:

  1. 设置递归深度, 超过此深度抛出异常.
  2. 优化流程, 去掉重复递归的步骤:因为分解的过程中有的过程会被计算多次. 不值得. 可记录成 HashTable
  3. 把递归代码改成非递归代码, 使用的方式就是用固定的几个变量, 从终止条件开始, 向上循环, 存储/迭代每次的结果, 直到到达目标值. 而且二者可以认为是等价的, 也就是说基本所有的递归都可以写成循环迭代的方式。因此,小数据量用递归OK,简洁优美。大数据量要改写成循环迭代的方式。

栗子

讲述递归,最常用的栗子就是求解斐波那契数列

long long Fibonacci(unsigned int n)
{
	if(n <= 0)
		return 0;
	
	if(n == 1)
		return 1;

	return Fibonacci(n-1) + Fibonacci(n-2);
}

复杂度分析

Fibonacci 是讲解递归算法的最佳栗子,但是这并不代表递归算法是求解 Fibonacci 的最佳方法。分析一下时间复杂度和空间复杂度,就会发现递归解法实现起来就是一种灾难。

  • 时间复杂度, 2 n 2^n 2n, 指数增长
  • 空间复杂度, 2 n 2^n 2n, 极其容易爆栈

复杂度如此高的原因,重要的一点就是为了保证如此简洁优美的代码形式,里面包含列很多的重复计算和程序堆栈开辟。把这些浪费的资源去掉,把中间结果保存起来,防止多次运算。

O(n) 改进

long long Fibonacci(unsigned int n) {
	if(n <= 0)
		return 0;	
	if(n == 1)
		return 1;	
	int result_k_minus_two = 0;
	int result_k_minus_one = 1;
	int result_k = 1;
	for(int i=2; i <= n; ++i ) {
	    result_k = result_k_minus_one + result_k_minus_two;
	    result_k_minus_two = result_k_minus_one;
	    result_k_minus_one = result_k;
	}	
	return result_k;
}

O(log n) 改进

Fibonacci 数列的求解,用矩阵的形式表达出来就是

[ f ( n ) f ( n − 1 ) f ( n − 1 ) f ( n − 2 ) ] = [ 1 1 1 0 ] n − 1 \begin{bmatrix}f(n)&amp;f(n-1)\\f(n-1)&amp;f(n-2)\\ \end{bmatrix} = \begin{bmatrix}1&amp;1\\1&amp;0\\ \end{bmatrix}^{n-1} [f(n)f(n1)f(n1)f(n2)]=[1110]n1,

而计算右边的矩阵的 k k k次方,可以使用 a n = ( a n / 2 ) 2 a^n = (a^{n/2})^2 an=(an/2)2 来减小一倍运算量,如此迭代,复杂度降为 O ( l o g n ) O(log n) O(logn)

O(1) 改进

就是查表解决啦,把常用的 Fibonacci 数列用一个 array 存储起来,那么每次调用的时候直接 array[n] 就出来了。 比如说存储前 100 个数,0 ~ 99 都是直接查表,从 100 开始再用前面 O ( n ) O(n) O(n) 的算法求解。这样,如果 99.99 % 的调用都是可以直接查表的话,那么这个算法可以认为是平均 O ( 1 ) O(1) O(1)

Fibonacci 数列的另外一个变种问题就是, 青蛙跳台阶问题. 一直青蛙在井底, 井底到地面有 n n n 个台阶, 青蛙可以一次跳一格, 也可以一次跳两格, 那么请问青蛙有多少种方式跳到地面?

首先分解问题, 青蛙起始的状态是井底, l a y e r = 0 layer=0 layer=0, 目标是跳到 l a y e r = n layer = n layer=n, 而中间过程 l a y e r = k layer = k layer=k 的求解和 l a y e r = n layer=n layer=n 是完全一样的. 因此, 可以考虑用递归求解.

然后, 用递归的思路重新分析问题, 假设青蛙已经上来了 l a y e r = n layer=n layer=n, 那它上一步有几种情况呢? 根据题意, 有两种, 一是从 n − 1 n-1 n1 上跳 1 1 1格上来, 二是从 n − 2 n-2 n2上跳2格上来. 只有这两种情况, 没有其他情况了. 然后我们在分析, n − 1 n-1 n1, n − 2 n-2 n2 这些台阶也都是有两种情况达到, 下面两阶跳两格上来或者下面一阶跳1 格上来. 那么终止条件是什么呢? 就是 l a y e r = 2 layer = 2 layer=2 的时候, 有两种情况, 一步一格两步上来或者一步两格直接上来. l a y e r = 1 layer = 1 layer=1 的时候, 只有 1 种情况, 就是一步走了一格. 所以, 递推公式和终止条件就是 f ( n ) = { f ( n ) = f ( n − 1 ) + f ( n − 2 ) f ( 2 ) = 2 f ( 1 ) = 1 f(n) = \begin{cases} f(n) = f(n-1)+f(n-2) \\ f(2)=2\\f(1)=1 \end{cases} f(n)=f(n)=f(n1)+f(n2)f(2)=2f(1)=1

翻译成递归代码就是,

int GetSteps(int n) {
    if(n <= 0) return 0;
    if(n == 1) return 1;
    if(n == 2) return 2;
    return GetSteps(n-1) + GetSteps(n-2);
}

简单画一下图就会发现, 求解 f ( 6 ) f(6) f(6) 会计算 f ( 5 ) &amp; f ( 4 ) f(5) \&amp; f(4) f(5)&f(4), 而计算 f ( 7 ) f(7) f(7) 会计算 f ( 6 ) &amp; f ( 5 ) f(6) \&amp; f(5) f(6)&f(5), 很明显 f ( 5 ) f(5) f(5) 计算了两次, 可以通过存入HashTable 来避免重复计算

int GetSteps(int n) {
    if(n <= 0) return 0;
    if(n == 1) return 1;
    if(n == 2) return 2;
    
    if(hasSolvedSet.containsKey(n))
        return hasSolvedSet.get(n);
    
    int ret = f(n-1) + f(n-2);
    hasSolvedSet.put(n, ret);
    return ret;
}

当然这还不够彻底, 更彻底的方式是直接去掉递归形式, 仅保留递归的想法.

int GetSteps(int n) {
    if(n <= 0) return 0;
    if(n == 1) return 1;
    if(n == 2) return 2;
    
    int ret = 0;
    int pre1step = 2;
    int pre2step = 1;
    for(int i = 3; i <= n; ++i) {
        ret = pre1Step + pre2step;
        pre2step = pre1step;
        pre1step = ret;
    }
    return ret;
}

Ref

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值