《数据结构》详解精炼:递归(1)

第 2 章 算法分析

2.9 递归(1)

1. 递归

递归(英语:Recursion),又译为递回,在数学与计算机科学中,是指在函数的定义中使用函数自身的方法。

例题 2.9.1 阶乘: <math xmlns="http://www.w3.org/1998/Math/MathML"> n ! = 1 × 2 × 3 × ⋯ × n n!=1\times2\times3\times\cdots\times n </math>n!=1×2×3×⋯×n ,并规定 <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 ! = 1 0!=1 </math>0!=1 。用递归的方式表示:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> f ( n ) = { 1 i f n = 0 n ⋅ f ( n − 1 ) i f n > 0 \begin{split} f(n)=\begin{cases}1&\quad if~n=0\\n\cdot f(n-1)&\quad if ~n\gt0\end{cases} \end{split} </math>f(n)={1n⋅f(n−1)if n=0if n>0

【算法描述】

c 复制代码
long factorial(int n){
    if(n == 0){
        return 1;
    } else {
        return n * factorial(n - 1);
    }
}

在此算法描述中,即使用了递归。

但,使用递归实现阶乘,并不是最优的选择,因为上述所谓递归,可以用循环语句轻易实现。

c 复制代码
long factorial(int n) {
    long fact = 1;
    if(n == 0) return 1;
    for(int i = 1; i <= n; i++) {
        fact *= i;
    }
    return fact;
}

如果程序中出现了条件语句,其时间复杂度的判断应以运行时间长的分支为准,例如在上述任何算法中,显然 if(n == 0) 分支的时间复杂度是 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1) ,而另外一个分支,时间复杂度是 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n) ,故该算法的时间复杂度是 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n) 。

例题 2.9.2 斐波那契数列

公元1150年印度数学家 Gopala 和金月在研究箱子包装对象长宽刚好为 1 和 2 的可行方法数目时,首先描述这个数列。在西方,最先研究这个数列的人是比萨的列奥那多(意大利人斐波那契 Leonardo Fibonacci, 1175-1250),他描述兔子生长的数目时用上了这数列:

  • 第一个月初有一对刚诞生的兔子
  • 第二个月之后(第三个月初)它们可以生育
  • 每月每对可生育的兔子会诞生下一对新兔子
  • 兔子永不死去

假设在 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 月有兔子总共 <math xmlns="http://www.w3.org/1998/Math/MathML"> a a </math>a 对, <math xmlns="http://www.w3.org/1998/Math/MathML"> n + 1 n + 1 </math>n+1 月总共有 <math xmlns="http://www.w3.org/1998/Math/MathML"> b b </math>b 对。在 <math xmlns="http://www.w3.org/1998/Math/MathML"> n + 2 n + 2 </math>n+2 月必定总共有 <math xmlns="http://www.w3.org/1998/Math/MathML"> a + b a + b </math>a+b 对:因为在 <math xmlns="http://www.w3.org/1998/Math/MathML"> n + 2 n + 2 </math>n+2 月的时候,前一月( <math xmlns="http://www.w3.org/1998/Math/MathML"> n + 1 n + 1 </math>n+1 月)的 <math xmlns="http://www.w3.org/1998/Math/MathML"> b b </math>b 对兔子可以存留至第 <math xmlns="http://www.w3.org/1998/Math/MathML"> n + 2 n + 2 </math>n+2 月(在当月属于新诞生的兔子尚不能生育)。而新生育出的兔子对数等于所有在 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 月就已存在的 <math xmlns="http://www.w3.org/1998/Math/MathML"> a a </math>a 对。

斐波纳契数是帕斯卡三角形的每一条红色对角线上数字的和。

用递归方式定义斐波那契数列:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> f i b ( n ) = { 1 i f n = 1 f i b ( n − 1 ) + f i b ( n − 2 ) o t h e r s fib(n)=\begin{cases} 1&\quad if ~n=1 \\fib(n-1)+fib(n-2)&\quad~others \end{cases} </math>fib(n)={1fib(n−1)+fib(n−2)if n=1 others

【算法描述】

c 复制代码
long fib(long n){
    if(n == 1 || n == 2) return 1;
    else return fib(n-1) + fib(n-2);
}

对斐波那契数列算法的时间复杂度分析,后面会单独介绍。

上面两个例子中显示,用递归实现算法,必须要确保有穷性,这也是算法的特性之一(参考 006 节),为此,必须有递归终止的条件,例如斐波那契数列的算法描述中的第 2 行。终止条件处的结果应该是直接算出来,而不依靠递归的。这是递归的基本法则之一。

递归的基本法则:

  1. 基准情形(base case):必须要有某些基准的情形,不用递归就能求解,也就是递归的终止条件。
  2. 不断推进(making progress):对于那些需要递归求解的情形,递归调用必须总能朝着产生基准情形的方向推进。
  3. 设计法则:假设所有的递归调用都能运行。
  4. 合成效益法则(compound interest rule):在求解一个问题的同一实例时,切勿在不同的递归调用中做重复性的工作

对于斐波那契数列的递归算法描述,就违背了第 4 点,后续专门进行时间复杂度分析的时候会详解。

本文由mdnice多平台发布

相关推荐
嚣张农民15 小时前
推荐3个实用的760°全景框架
前端·vue.js·程序员
梓羽玩Python15 小时前
推荐一款用了5年的全能下载神器:Motrix!全平台支持,不限速下载网盘文件就靠它!
程序员·开源·github
梓羽玩Python16 小时前
这款一站式AI体验平台值得收藏起来!GPT-4o、GPT-4o Mini、Claude 3.5 Sonnet免费使用!
人工智能·程序员·设计
前端宝哥1 天前
10 个超赞的开发者工具,助你轻松提升效率
前端·程序员
XinZong1 天前
【VSCode插件推荐】想准时下班,你需要codemoss的帮助,分享AI写代码的愉快体验,附详细安装教程
前端·程序员
Goboy2 天前
0帧起步:3分钟打造个人博客,让技术成长与职业发展齐头并进
程序员·开源·操作系统
JaxNext2 天前
不选总统选配色,这一票投给 CSS logo
前端·css·程序员
程序员鱼皮3 天前
刚毕业,去做边缘业务,还有救吗?
计算机·程序员·互联网·求职·简历
WujieLi3 天前
独立开发沉思录周刊:vol18.AI 正在成为无处不在的基础设施
程序员·设计·创业
_祝你今天愉快3 天前
重学Android:从位运算到二进制表示(零)
算法·程序员