数据结构与算法分析(五)--- 递推与递归 + 减治排序

本文深入探讨了递推和递归的概念,通过实例解释了递推的正向思维方式以及递归在解决实际问题中的应用,如递归求解斐波那契数列和递归实现插入排序。文章还讨论了尾递归优化,以及如何通过递推方式提高算法效率,展示了递归设计在数据结构和算法分析中的重要性。
摘要由CSDN通过智能技术生成

人有人的思维,计算机有计算机的思维,它们很不相同。如果你要问其中最大的不同是什么,那就是一种被称为递归(recursive)的逆向思维。相比之下,人的正向思维被称为递推(iterative)。要了解什么是递归,我们先了解什么是递推。

一、递推

递推是人本能的正向思维,比如我们在学习解方程时,先学习解一元方程,再学习解二元方程,之后才学解三元方程,最后推广到有任意未知数的方程,就是所谓的线性方程组,这种循序渐进、由易到难、由小到大、由局部到整体等正向思维方式就是递推。

如果用递推的方法计算一个整数的阶乘,比如5! = 12345,那么做法是从小到大一个个乘起来,如果算n!,那么要从1乘到n,使用递推计算n!的函数实现代码如下所示:

// algorithm\recursive.c

#include <stdio.h>

int factorial_iterative(int n)
{
   
    if(n < 0)
        return 0;
    
    int i, res = 1;
    
    for(i = 1; i <= n; i++)
        res *= i;
    
    return res;
}

上面的代码逻辑符合我们从小到大、自底向上的递推思维方式,我们从来不觉得它有什么问题。事实上,我们在中学里学的数学归纳法就是递推方法的典型应用。

计算机思维正相反,它是自顶向下,从整体到局部的递归思维。什么是递归呢?直接解释概念很难讲清楚,下面以一个面试题为例来说明:

我们俩来做一个游戏,第一个人先从1和2中挑一个数字,第二个人可以在对方的基础上选择加1,或者加2。然后又轮到了第一个人,他可以再次选择加1,或者加2,之后把选择权交给对方。就这样双方交替地选择加1或者加2,谁要是正好加到20,谁就赢了。用什么策略可以保证一定能赢?

我们先简化上面的问题,把加到20改为加到10。按照递推思维,假如让你先选,你选2,我加2到4,你加1到5,我再加2到7,接下来你不论选加1到8还是加2到9,我都赢定了。再假如我先选,我选1,你选加2到3,我选加1到4,你选加2到6,我选加1到7,又回到第一次最后的状态,我还是赢定了。

可能你已经从上面的例子中想清楚这道题里面的技巧了,如果仅仅抢到10,情况并不复杂,你即使想不清楚它的道理,试几次也能找到规律,但是如果是抢20,情况就复杂多了,如果是抢30甚至50呢?就不能通过穷举法这种笨办法解决问题了,就必须找到它的规律。

可能你已经看出来了,要想抢到20,就需要抢到17,因为抢到了17,无论对方加1还是加2,你都可以加到20。而要想抢到17,就要抢到14,依此类推,就必须抢到11、8、5、2,因此对于这道题,只要第一个人抢到了2,他就赢定了。这里面的核心在于看清楚,无论对方选择1还是2,你都可以让第一轮两个人加起来的数值等于3,于是你就可以牢牢控制整个过程了。

这道看似是智力题的面试题是要考察候选人的什么技能呢?就是对计算机递归思想的理解。对于一般人,让他们数到20,他们会从小到大数,也就是正向的递推思维。但是这道题的解题思想正好相反,它是要寻找20,就要先寻找17,至于怎么从17到20,方法你是知道的,接下来要寻找17,就要寻找14,依此类推,这就是递归思想。

二、递归

上面这道面试题,可能有点过于简单,但是面试官其实还留有后手。比如他会问面试者,按照上述方法,从1开始加到20,一个有多少种不同的递加过程?

解这道题的技巧也在于使用递归,如果你从1、2、3开始找规律就难了。我们假定数到20有F(20)种不同的路径,那么到达20这个数字,前一步只有两个可能的情况,即从18直接蹦到20,或者从19数到20,由于这两种情况彼此是不同的,因此走到20的路径数量,其实就是走到18的路径数量,加上走到19的路径数量,也就是说F(20) = F(19) + F(18),类似的,F(19) = F(18) + F(17),这就是递推公式。

最后,F(1)只有一个可能性,就是F(1) = 1,F(2)有两个可能性,要么直接蹦到2,要么从1走到2,所以F(2) = 2。知道了F(1)和F(2),就可以知道F(3),然后再倒着推导回去,一直到F(20)即可。

数学比较好的朋友可能已经看出来了,这就是著名的斐波那契数列,如果我们认为F(0) 也等于1,那么这个数列就是这样的1(=F(0))、1、2、3、5、8、13、21…,这个数列几乎按照几何级数的速度递增,到了F(20),就已经是10946了。

斐波那契数列其实反映出一个物种自然繁衍,或者一个组织自然发展过程中成员的变化规律。斐波那契数列最初是这样描述的:有一对兔子,它们生下了一对小兔子,前面的成为兔一代,后面的称为兔二代,然后这两代兔子各生出一对儿兔子,这样就有了第三代。这时第一代兔子老了,就生不了小兔子了,但是第二、第三代还能生,于是它们生出了第四代,然后它们不断繁衍下去,请问第N代兔子有多少对儿?

斐波那契数列增长有多快呢?我们假设F(n)表示数列中的第n个数,F(n+1)表示数列中的第n+1个数,我们用Rn = F(n+1)/F(n)表示数列增长的相对速率,简单计算下即可得知,Rn很快趋近于1.618,这恰好是黄金分割的比例。黄金分割比例是个神奇的数字,或许反映了宇宙自身的一个常数,比如自然界中的蜗牛壳、龙卷风、星系的形状都符合等角螺旋线,也被称为自然生长螺旋线,就是由黄金分割的几何相似性绘出的。

上面这个比率(Rn = 1.618)几乎也是一个企业扩张时能够接受的最高的员工数量增长速率,如果超过这个速率,企业的文化就很难维持了。企业在招入新员工时,通常要由一个老员工带一个新员工,缺了这个环节,企业的人一多就各自为战了。而当老员工带过两三个新员工后,他们会追求更高的职业发展道路,不会花太多时间继续带新人了,因此带新员工的人基本也就是职级中等偏下的人,这很像上面的兔子繁殖,只有那些已经性成熟而且还年轻的在生育。

上面那道面试题,将数到20的不同路径扩展到数到n的不同路径,实际上就是求斐波那契数列的第N个数,根据上面的递推公式可以扩展得到F(n) = F(n-1) + F(n-2)。有了递推公式,还需要递归边界,也即前面提到的F(0) = F(1) = 1。递推公式可以将求解F(n)的未知解自顶向下转换为求解F(n-1)与F(n-2)的未知解,直到达到递归边界的已知解,再通过递归边界的已知解自底向上递推(或回归),求得F(n)的已知解。

使用递归计算斐波那契数列第N个数的函数实现代码如下:

// algorithm\recursive.c

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

从上面的解题过程可以总结:递归就在于使用计算机自顶向下、从整体到局部的思维方式分析问题,找到把原问题自顶向下层层展开(或分解)的递推公式,通过不断重复使用递推公式,把原问题展开(或分解)到有已知解的递归边界处,再从递归边界的已知解,自底向上递推(或回归)求得原问题的解。

递归可以说是计算机科学的精髓,包含自顶向下和自底向上两个递推过程(可以把其中一个称为回归或回溯过程),递归的实现需要找到递推公式与递归边界两个部分:

  • 递归边界:子问题展开或分解的尽头;
  • 递推公式:将原问题分解为若干个子问题的方式。

为了更直观的了解递归过程,我们看下递归调用示意图,先从最开始简单的求n!为例,先给出使用递归方法求解n!的函数实现代码如下:

// algorithm\recursive.c

int factorial_recursive(int n)
{
   
    if(n < 0)
        return 0;
    else if(n == 0)
        return 1;
    else
        return n*factorial_recursive(n-1);
}

跟递推方式求解的函数代码相比更简洁些,以求解3!为例,给出递归求解阶乘的过程示意图如下:
递归求解阶乘的过程示意图
从上图可以明显看出递归求解自顶向下与自底向上两个递推过程,这个过程有点类似于堆栈的后进先出结构。计算机实际执行递归时,确实使用了计算机的系统堆栈,由此可以看出,如果递归层级过深,就会导致系统堆栈不够用而出现错误。

我们再看斐波那契数列的递归求解过程,对此应该更有体会,以F(4)为例,斐波那契数列递归求解示意图如下:
斐波那契数列递归求解过程
由上图可以看出斐波那契数列求解的递归调用过程比阶乘求解的递归调用复杂得多,如果阶乘求解的递归调用复杂度为O(n),斐波那契数列的递归调用复杂度就是O(2^n),后者的递归调用复杂度呈指数增长,采用上面的函数实现方式只能求解比较小的斐波那契数列,比如前40项。

斐波那契数列递归求解为何会有这么大的复杂度呢?再仔细看其递归求解过程示意图,发现再递归求解过程中有大量的重复计算。要想提高算法的运行效率自然要让计算机尽可能少做事,现在计算机做了大量重复计算,效率自然大大降低。如何避免这种大量重复计算呢?

最简单的就是将中间计算结果保存起来,在下次使用时直接取用,不用再重新计算,比如在外部建一个数组专门用来保存中间结果,按这种方式实现的斐波那契数列递归求解函数代码如下:

// algorithm\recursive.c

#define MAXN    1000

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

流云IoT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值