《数据结构》详解精炼:线性递归

第 2 章 算法分析

2.10 线性递归

以 014 节的例题 2.9.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);
    }
}

在此算法描述中,即使用了递归。对此递归的分析如下:

  • 满足算法的"有穷性"要求:首先判断 n == 0 的条件,满足此条件时,return 1 ,即终止递归的条件或基准情形,从而避免递归无限进行下去。
  • n * factorial(n - 1) 使得递归朝着基准情形进行自我调用(不断推进),且每一递归实例对自身的调用为一次(合成效益法则),即 <math xmlns="http://www.w3.org/1998/Math/MathML"> f ( n ) , f ( n − 1 ) , f ( n − 2 ) , ⋯   , f ( 1 ) , f ( 0 ) f(n),f(n-1),f(n-2),\cdots,f(1),f(0) </math>f(n),f(n−1),f(n−2),⋯,f(1),f(0) ,从而构成了一个线性的次序关系,因此称这类递归模式为线性递归。这是递归的最基本形式。

线性递归的模式,是实现减治算法((decrease and conquer))的一种策略,即:递归每深入一层,待求解问题的规模都缩减一个常数,直至最终达到基准情形。

例题 2.10.1 用递归方式,计算给定 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 个整数的总和

在 011 节的例题 2.6.3 中,用迭代的方式实现了计算给定 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 个整数的总和的问题,此处用递归方式实现:

c 复制代码
int sum(int A[], int n){ //数组求和算法(线性递归版)
    if (n < 1)  //基准情形
        return 0;
    else
        return sum(A, n-1) + A[n - 1]; //递归:前 n-1 项之和,再累计第 n-1 项 
}

1. 递推公式

以例题 2.10.1 为例,说明递归的时间复杂度的一种常用分析方法:递推公式法。

假设计算长度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 的数组中各个数字之和的时间复杂度是 <math xmlns="http://www.w3.org/1998/Math/MathML"> T ( n ) T(n) </math>T(n) ,那么计算长度为 <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"> T ( n − 1 ) T(n-1) </math>T(n−1) 。根据上述算法中的第 5 行,sum(A, n-1) + A[n - 1] 的结果为 sum(A, n) ,而计算 A[n - 1] 的时间复杂度是常数阶,即:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> T ( n ) = T ( n − 1 ) + O ( 1 ) = T ( n − 1 ) + c 1 (1) T(n) = T(n-1)+O(1)=T(n-1)+c_1\tag{1} </math>T(n)=T(n−1)+O(1)=T(n−1)+c1(1)

当到达基准情形时,即计算 sum(A, 0) 时,直接返回 <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 0 </math>0 ,return 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" display="block"> T ( 0 ) = O ( 1 ) = c 2 (2) T(0)=O(1)=c_2\tag{2} </math>T(0)=O(1)=c2(2)

其中,(1)式和(2)式中的 <math xmlns="http://www.w3.org/1998/Math/MathML"> c 1 , c 2 c_1,c_2 </math>c1,c2 均为常数。

由(1)式得:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> T ( n ) = T ( n − 1 ) + c 1 = T ( n − 2 ) + c 1 + c 1 = T ( n − 2 ) + 2 c 1 = T ( n − 3 ) + c 1 + 2 c 1 = T ( n − 3 ) + 3 c 1 ⋯ = T ( 1 ) + ( n − 1 ) c 1 = T ( 0 ) + c 1 + ( n − 1 ) c 1 = T ( 0 ) + n c 1 ( 代入 ( 2 ) 式 ) = c 2 + n c 1 = O ( n ) \begin{split} T(n)&=T(n-1)+c_1 \\&=T(n-2)+c_1+c_1=T(n-2)+2c_1 \\&=T(n-3)+c_1+2c_1=T(n-3)+3c_1 \\&\quad\cdots \\&=T(1)+(n-1)c_1 \\&=T(0)+c_1+(n-1)c_1=T(0)+nc_1\quad(代入(2)式) \\&=c_2+nc_1=O(n) \end{split} </math>T(n)=T(n−1)+c1=T(n−2)+c1+c1=T(n−2)+2c1=T(n−3)+c1+2c1=T(n−3)+3c1⋯=T(1)+(n−1)c1=T(0)+c1+(n−1)c1=T(0)+nc1(代入(2)式)=c2+nc1=O(n)

递推法,是一种常用的方法。

在本节对应的练习题中,可以使用递归公式,对阶乘算法计算其时间复杂度。

2. 递归跟踪分析

对于线性递归而言,对递归过程进行跟踪,并不是很复杂的过程,因此也可以通过跟踪过程分析时间复杂度。

以例题 2.10.1 为例,其算法执行过程如图 2.10.1 所示:


图 2.10.1

  • 首先调用参数 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n ,再转向 <math xmlns="http://www.w3.org/1998/Math/MathML"> n − 1 , n − 1 , ⋯ 0 n-1,n-1,\cdots 0 </math>n−1,n−1,⋯0 ,直到参数为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 0 </math>0 ,到达了终止条件,不再递归。
  • 将基准情形的解(长度为 0 数组的总和 0)返回给对参数 1 的调用;累加上 A[0] 之后,再返回给对参数 2 的调用;累加上 A[1] 之后,继续返回给对参数 3 的调用;...;如此依次返回,直到最终返回给对参数 n 的调用,此时,只需累加 A[n - 1] 即得到整个数组的总和。

从上述过程可知,每一个递归实例中非递归部分涉及到三类计算:① 判断 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 是否为 0;② 累加 sum(n-1)A[n - 1] ;③ 返回当前的和。并且,在一个递归实例中,这三类计算各执行一次。它们也都是基本操作。所以,每个递归实例的运行时间是常数阶 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1) 。

结合图 2.10.1 可知,共计有 <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"> n + 1 n+1 </math>n+1 ,所以整个算法的时间复杂度是: <math xmlns="http://www.w3.org/1998/Math/MathML"> T ( n ) = ( n + 1 ) × O ( 1 ) = O ( n ) T(n)=(n+1)\times O(1)=O(n) </math>T(n)=(n+1)×O(1)=O(n) 。

相关推荐
灵感__idea9 小时前
JavaScript高级程序设计(第5版):无处不在的集合
前端·javascript·程序员
dmy11 小时前
n8n内网快速部署
运维·人工智能·程序员
憨憨睡不醒啊13 小时前
如何让LLM智能体开发助力求职之路——构建属于你的智能体开发知识体系📚📚📚
面试·程序员·llm
程序员岳焱14 小时前
Java 程序员成长记(二):菜鸟入职之 MyBatis XML「陷阱」
java·后端·程序员
liangdabiao16 小时前
让AI写出真正可用的图文并茂的帖子(微信公众号,小红书,博客)
程序员
安妮的心动录16 小时前
人是习惯的结果
面试·程序员·求职
小兵张健17 小时前
笔记本清灰记录
程序员
陈随易19 小时前
Univer v0.8.0 发布,开源免费版 Google Sheets
前端·后端·程序员
陈随易2 天前
Element Plus 2.10.0 重磅发布!新增Splitter组件
前端·后端·程序员
陈随易2 天前
2025年100个产品计划之第11个(哆啦工具箱) - 像哆啦A梦口袋一样丰富的工具箱
前端·后端·程序员