《数据结构》详解精炼:递归(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多平台发布

相关推荐
集成显卡3 小时前
别局限于 Oh-My-Posh,试试 Rust 编写的 starship:极简超快且无限可定制的命令行提示符
程序员·代码规范·命令行
陈随易3 小时前
我也曾离猝死很近
前端·后端·程序员
SimonKing7 小时前
IntelliJ IDEA 配置与插件全部迁移到其他盘,彻底释放C盘空间
java·后端·程序员
程序员cxuan1 天前
说点掏心窝子的话
后端·程序员
本末倒置1831 天前
告别"话痨"提交记录!Git 压缩 Commit 实战指南,代码洁癖党狂喜
面试·程序员·代码规范
程序员鱼皮1 天前
刚刚,微信终于能用 OpenClaw 了!安卓 iOS 都行,附保姆级教程
ai·程序员·编程·ai编程·openclaw
孟陬1 天前
国外技术周刊第 2 期 — 本周热门 🔥 YouTube 视频 TED 演讲 AI 如何能够拯救(而非摧毁)教育
前端·后端·程序员
陈随易1 天前
深度拆解技术架构的三大鸿沟:企业级Claw vs OpenClaw的工程差异
前端·后端·程序员
得物技术1 天前
Claude Code + OpenSpec 正在加速 AICoding 落地:从模型博弈到工程化的范式转移|得物技术
程序员·ai编程·claude
SimonKing1 天前
OpenClaw,再见!
java·后端·程序员