为了设计出解决问题的好算法,除了要掌握常用的数据结构工
具外,还需要掌握算法设计方法,算法设计与分析课程主要讨论分治
法、回溯法、分支限界法、贪心法和动态规划,称为五大算法策略。在
学习这些算法策略之前必须具有一定的算法设计基础,本章讨论穷
举法、归纳法、迭代法和递归法等基本算法设计方法及其递推式的计
算,学习要点和学习目标如下: 
(1)掌握穷举法的原理和算法框架。
(2)掌握归纳法的原理和从求解问题找出递推关系的方法。
(3)掌握迭代法的原理和实现迭代算法的方法。
(4)掌握递归法的原理和实现递归算法的方法。
(5)掌握递推式的各种计算方法。
(6)综合运用穷举法、归纳法、迭代法和递归法解决一些复杂的
实际问题。

3.1穷举法
3.1.1穷举法概述
1.什么是穷举法
穷举法又称枚举法或者列举法,是一种简单而直接地解决问题的方法。其基本思想是
先确定有哪些穷举对象和穷举对象的顺序,按穷举对象的顺序逐一列举每个穷举对象的所
有情况,再根据问题提出的约束条件检验哪些是问题的解,哪些应予排除。
在用穷举法解题时,针对穷举对象的数据类型而言,常用的列举方法如下。
①顺序列举:是指答案范围内的各种情况很容易与自然数对应甚至就是自然数,可以
按自然数的变化顺序去列举。
②排列列举:有时答案的数据形式是一组数的排列,列举出所有答案所在范围内的排
列为排列列举。
③组合列举:当答案的数据形式为一些元素的组合时,往往需要用组合列举。组合是
无序的。
穷举法的作用如下: 
①理论上讲穷举法可以解决可计算领域中的各种问题,尤其是处在计算机的计算速度
非常快的今天,穷举法的应用领域是非常广阔的。
②在实际应用中,通常要解决的问题规模不大,用穷举法设计的算法的运算速度是可
以接受的,此时设计一个更高效率的算法不值得。
③穷举法算法一般逻辑清晰,编写的程序简洁明了。
④穷举法算法一般不需要特别证明算法的正确性。
⑤穷举法可作为某类问题时间性能的底限,用来衡量同样问题的更高效率的算法。
穷举法的主要缺点是设计的大多数算法的效率都不高,主要适合规模比较小的问题的
求解。为此在采用穷举法求解时应根据问题的具体情况分析归纳,寻找简化规律,精简穷举
循环,优化穷举策略。
2.穷举法算法框架
穷举法算法一般使用循环语句和选择语句实现,其中循环语句用于枚举穷举对象所有
可能的情况,而选择语句判定当前的条件是否为所求的解。其基本流程如下: 
①根据问题的具体情况确定穷举变量(简单变量或数组) 。
②根据确定的范围设置穷举循环。
③根据问题的具体要求确定解满足的约束条件。
④设计穷举法程序,并执行和调试,对执行结果进行分析与讨论。
假设某个问题的穷举变量是
x 
和y,穷举顺序是先
x 
后y,均为顺序列举,它们的取值
范围分别是x∈(x1,xn 
y∈(ym 
约束条件为p(xi,对应穷举法

x2,…,),y2,…,), yj 
), 
算法的基本框架如下: 

y1,

70 


71 
void Exhaustive(x,n,y,m) { //穷举法框架 
for(int i=1;i<=n;i++) { //枚举x 的所有可能的值 
for(int j=1;j<=m;j++) { // 枚 举 y 的所有可能的值 
… 
if(p(x[i],y[j])) 
输出一个解; 
… 
} 
} 
}
从中看出,x 和y 所有可能的搜索范围是笛卡儿积,即([x1,y1],[x1,y2],…,[x1,ym ],…, 
[xn ,y1],[xn,y2],…,[xn,ym ]),这样的搜索范围可以用一棵树表示,称为解空间树,它包含
求解问题的所有解,求解过程就是在整个解空间树中搜索满足约束条件p(xi,yj)的解。
【例3-1】 鸡兔同笼问题。现有一个笼子,里面有鸡和兔子若干只,数一数,共有a 个
头、b 条腿。设计一个算法求鸡和兔子各有多少只。
解:由于有鸡和兔两种动物,每只鸡有2条腿,每只兔有4条腿,设置两个循环变量,x 
表示鸡的只数,y 表示兔的只数,那么穷举对象就是x 和y,假设穷举对象的顺序是先x 后
y(在本问题中也可以先y 后x)。
图3.1 a=3,b=8的解空间树
显然,x 和y 的取值范围都是0~a,约束条件p(x,y)=(x+y==b)&& (2x+4y=b)。
以a=3,b=8为例,对应的解空间树如图3.1所示,根结点是分支对应x 的各种取值,第二
层结点的分支为y 的各种取值,其中“×”结点是不满足约束条件的结点,带阴影的结点是
满足约束条件的结点,所以结果是x=2/y=1。对应的算法如下: 
void solve1(int a,int b) { 
for(int x=0;x<=a;x++) { 
for(int y=0;y<=a;y++) { 
if(x+y==a && 2*x+4*y==b) 
System.out.printf("x=%d,y=%d\n",x,y); 
} 
} 
} 
扫一扫
视频讲解

72 
从图3.1中看到,解空间树中共21个结点,显然结点个数越多时间性能越差,可以稍做
优化,鸡的只数最多为min(a,b/2),兔的只数最多为min(a,b/4),仍以a=3,b=8为例,x 
的取值范围是0~3,y 的取值范围是0~2。对应的解空间树如图3.2所示,共17个结点。
图3.2 a=3,b=8的优化解空间树
对应的优化算法如下: 
void solve2(int a,int b) { 
for(int x=0;x<=Math.min(a,b/2);x++) { 
for(int y=0;y<=Math.min(a,b/4);y++) { 
if(x+y==a && 2*x+4*y==b) 
System.out.printf("x=%d,y=%d\n",x,y); 
} 
} 
}
所以尽管穷举法算法通常性能较差,但可以以它为基础进行优化继而得到高性能的算
法,优化的关键是能够找出求解问题的优化点,不同的问题优化点是不相同的,这就需要读
者通过大量实训掌握一些基本算法设计技巧。3.1.2节和3.1.3节通过两个应用讨论穷举法
算法设计方法以及穷举法算法的优化过程。
3.1.2 最大连续子序列和
1.问题描述 
给定一个含n(n≥1)个整数的序列,要求求出其中最大连续子序列的和。例如,序列
(-2,11,-4,13,-5,-2)的最大子序列和为20,序列(-6,2,4,-7,5,3,2,-1,6,-9, 
10,-2)的最大子序列和为16。规定一个序列的最大连续子序列和至少是0,如果小于0, 
其结果为0。
2.问题求解1 
对于含有n 个整数的序列a[0..n-1],其中任何连续子序列为a[i..j](i≤j,0≤i≤ 
n-1,i≤j≤n-1),求出它的所有元素之和cursum,并通过比较将最大值存放在maxsum 
中,最后返回maxsum。这种解法是通过穷举所有连续子序列(一个连续子序列由起始下标
扫一扫
视频讲解

73 
i 和终止下标j 确定)来得到结果,是典型的穷举法思想。
例如,对于a[0..5]={-2,11,-4,13,-5,-2},求出的a[i..j](0≤i≤j≤5)的所有
元素之和如图3.3所示(行号为i,列号为j),其过程如下: 
图3.3 所有a[i..j]子序列(0≤i≤j≤5)的元素和
(1)i=0,依次求出j=0,1,2,3,4,5的子序列和分别为-2,9,5,18,13,11。
(2)i=1,依次求出j=1,2,3,4,5的子序列和分别为11,7,20,15,13。
(3)i=2,依次求出j=2,3,4,5的子序列和分别为-4,9,4,2。
(4)i=3,依次求出j=3,4,5的子序列和分别为13,8,6。
(5)i=4,依次求出j=4,5的子序列和分别为-5,-7。
(6)i=5,求出j=5的子序列和为-2。
其中20是最大值,即最大连续子序列和为20。
对应的算法如下: 
int maxSubSum1(int a[]) { //解法1 
int n=a.length; 
int maxsum=0,cursum; 
for(int i=0;i<n;i++){ //两重循环穷举所有的连续子序列 
for(int j=i;j<n;j++) { 
cursum=0; 
for(int k=i;k<=j;k++) 
cursum+=a[k]; 
maxsum=Math.max(maxsum,cursum); //通过比较求最大值maxsum 
} 
} 
return maxsum; 
}
【算法分析】 maxSubSum1(a,n)算法中用了三重循环,所以有: 
T(n)=Σn-1 
i=0Σn-1 
j=iΣj 
k=i1=Σn-1 
i=0Σn-1 
j=i(j-i+1)=12
Σn-1 
i=0(n -i)(n -i+1)=O(n3)

74 
3.问题求解2 
采用前缀和方法,用Sum(i)表示子序列a[0.. i-1]的元素和,即a 中前i 个元素的和, 
显然有如下递推关系:
Sum(0)=0 
Sum(i)=Sum(i-1)+a[i-1] 当i >0时 
假设j≥i,则有: 
Sum[i]=a[0]+a[1]+ …a[i-1] 
Sum[j]=a[0]+a[1]+ …a[i-1]+a[i]+ …a[j-1] 
两式相减得到Sum[j]-Sum[i]=a[i]+…a[j-1],这样i 从0到n-1、j-1从i 
到n-1(即j 从i+1到n)循环可以求出所有连续子序列a[i..j]之和,通过比较求出最大
值maxsum 即可。对应的算法如下: 
static int maxSubSum2(int a[]) { //解法2 
int n=a.length; 
int Sum[]=new int[n+1]; 
Sum[0]=0; 
for(int i=1;i<=n;i++) 
Sum[i]=Sum[i-1]+a[i-1]; 
int maxsum=0,cursum; 
for(int i=0;i<n;i++) { 
for(int j=i+1;j<=n;j++) { 
cursum=Sum[j]-Sum[i]; 
maxsum=Math.max(maxsum,cursum); //通过比较求最大值maxsum 
} 
} 
return maxsum; 
}
【算法分析】 maxSubSum2(a,n)算法中主要是两重循环,所以有: 
T(n)=Σn-1 
i=0 Σn 
j=i+11=Σn-1 
i=0(n -i)=n(n +1) 
2 =O(n2) 
4.问题求解3 
对前面的解法1进行优化。当i 取某个起始下标时,依次求j=i,i+1,…,n-1对应
的子序列和,实际上这些子序列是相关的。用Sum(a[i..j])表示子序列a[i..j]的元素和
(即表示以a[i]为起始元素的前缀和),显然有如下递推关系: 
Sum(a[i..j])=0 当j <i 时
Sum(a[i..j])=Sum(a[i..j-1])+a[j] 当j ≥i 时 
这样在连续求a[i..j]子序列和(j=i,i+1,…,n-1)时没有必要使用循环变量为k 的
第3重循环,优化后的算法如下:

75 
int maxSubSum3(int a[]) { //解法3 
int n=a.length; 
int maxsum=0,cursum; 
for(int i=0;i<n;i++) { 
cursum=0; 
for(int j=i;j<n;j++) { 
cursum+=a[j]; 
maxsum=Math.max(maxsum,cursum); //通过比较求最大值maxsum 
} 
} 
return maxsum; 
}
【算法分析】 maxSubSum3(a,n)算法中只有两重循环,所以有: 
T(n)=Σn-1 
i=0Σn-1 
j=i1=Σn-1 
i=0(n -i)=n(n +1) 
2 =O(n2) 
5.问题求解4 
对前面的解法2继续优化。将maxsum和cursum初始化为0,用i 遍历a,置cursum+= 
a[i],也就是说cursum 累计到a[i]时的元素和,分为两种情况: 
① 若cursum≥maxsum,说明cursum 是一个更大的连续子序列和,将其存放在
maxsum 中,即置maxsum=cursum。
② 若cursum<0,说明cursum 不可能是一个更大的连续子序列和,从下一个i 开始继
续遍历,所以置cursum=0。
在上述过程中先置cursum+=a[i],后判断cursum 的两种情况。a 遍历完后返回
maxsum 即可。对应的算法如下: 
int maxSubSum4(int a[]) { //解法4 
int n=a.length; 
int maxsum=0,cursum=0; 
for(int i=0;i<n;i++) { 
cursum+=a[i]; 
if(cursum>=maxsum) / /通 过 比 较 求 最大值maxsum 
maxsum=cursum; 
if(cursum<0) // 若 c ur su m <0 ,最 大连续子序列从下一个位置开始 
cursum=0; 
} 
return Math.max(maxsum,0); 
}
【算法分析】 maxSubSum4(a,n)算法中只有一重循环,所以时间复杂度为O(n)。
从中看出,尽管仍采用穷举法思路,但可以通过各种优化手段降低算法的时间复杂度。

76 
解法2的优化点是采用前缀和数组,解法3的优化点是找出a[i..j-1]和a[i..j]子序列的
相关性,解法4的优化点是进一步判断cursum 的两种情况。
思考题:对于给定的整数序列a,不仅要求出其中最大连续子序列的和,还需要求出这
个具有最大连续子序列和的子序列(给出其起始和终止下标),如果有多个具有最大连续子
序列和的子序列,求其中任意一个子序列。
3.1.3 实战———最大子序列和(LeetCode53★) 
1.问题描述 
给定一个含n(1≤n≤105)个整数的数组nums,设计一个算法找到一个具有最大和的
连续子数组(子数组最少包含一个元素),返回其最大和。例如,nums={-2,1,-3,4,-1, 
2,1,-5,4},答案为6,对应的连续子数组是{4,-1,2,1}。要求设计如下方法: 
public int maxSubArray(int[]nums) { } 
2.问题求解 
本题是求nums的最大连续子序列和,但该序列至少包含一个元素,也就是说最大连续
子序列和可能为负数,因此不能简单采用3.1.2节中解法4的优化点,需要做两点修改: 
(1)将表示结果的maxsum 初始化为nums[0]而不是0。
(2)在求当前以nums[i]结尾的最大连续子序列和cursum 时分为两个部分,一是
cursum+nums[i],另外一部分是只有nums[i](即无论nums[i]是否小于0,它都有可能
构成一个仅含一个元素的最大连续子序列),然后在这样的两个部分中取最大值得到
cursum,即cursum=max(cursum+nums[i],nums[i])。
在每次得到一个连续子序列和时通过比较将最大值存放到maxsum 中。对应的程序
如下: 
class Solution { 
public int maxSubArray(int[]nums) { 
int n=nums.length; 
if(n==1) return nums[0]; 
int maxsum=nums[0],cursum=0; 
for(int i=0;i<n;i++) { 
cursum=Math.max(cursum+nums[i],nums[i]); 
maxsum=Math.max(maxsum,cursum); 
} 
return maxsum; 
} 
}
上述程序提交时通过,运行时间为1ms,内存消耗为48.7MB。
扫一扫
视频讲解

3.归法
2 
纳

3.2.1 
归纳法概述
1. 
什么是数学归纳法
谈到归纳法很容易想到数学归纳法,数学归纳法是一种数学证明方法,典型地用于确定
一个表达式在所有自然数范围内是成立的,分为第一数学归纳法和第二数学归纳法两种。

(1)第一数学归纳法的原理:若{P(1),P(2),P(3),P(4),…} 是命题序列且满足以下
两个性质,则所有命题均为真。
①P(1)为真。

②任何命题均可以从它的前一个命题推导得出
n
。
(
例如,采用第一数学归纳法证明1+2+…+n=n+1)/2成立的过程如下
:
当n=1时,左式=1,右式=(1×2)/2=1,左、右两式相等,等式成立
。
假设当n=k-有1+2+…+(1)k(1)/2
。
1时等式成立, k-=k
当n=
k 
时, 1+2+…+(1)+k=k-k(等式成立。

左式=k-k(1)/2+k=k+1)/2, 

即证
(
。
2)第二数学归纳法的原理:若{P(1),P(2),P(3),P(4),…} 是满足以下两个性质的
命题序列,则对于其他自然数,该命题序列均为真。

①P(1)为真。

②任何命题均可以从它的前面所有命题推导得出
。
用数学归纳法进行证明主要是两个步骤
:
①证明当取第一个值时命题成立。
②假设当前命题成立,证明后续命题也成立。
数学归纳法的独到之处便是运用有限个步骤就能证明无限多个对象,而实现这一目的
的工具就是递推思想。第①步是证明归纳基础成立,归纳基础成为后面递推的出发点,没有
它递推成了无源之水;第②步是证明归纳递推成立,借助该递推关系,命题成立的范围就能
从开始向后面一个数一个数地无限传递到以后的每一个正整数,从而完成证明,因此递推是
实现从有限到无限飞跃的关键。

【例3-2】给定一棵非空二叉树,采用数学归纳法证明如果其中有
n 
个叶子结点,则双
分支结点个数恰好为n-即P(1。

1, n)=n
证明:当n=1时,这样的二叉树恰好只有一个结点,该结点既是根结点又是叶子结点, 
没有分支结点,则P(1)=0成立。
k-k--1=k假设叶子结点个数为k-1时成立,即P(1)=(1)2。由二叉树的结构
可知,想要在当前的二叉树中增加一个叶子结点,对其中某种类型的结点的操作如下。

①双分支结点:无法增加孩子结点,不能达到目的。
②单分支结点:可以增加一个孩子结点(为叶子结点), 此时该单分支结点变为双分支
结点,也就是说叶子结点和双分支结点均增加一个, k)k-=2+1
这样P(=P(1)+1k-= 

77

k-1,结论成立。
③叶子结点:增加一个孩子结点,总的叶子结点个数没有增加,不能达到目的。
④叶子结点:增加两个孩子结点(均为叶子结点), 此时该叶子结点变为双分支结点, 
也就是说叶子结点和双分支结点均增加一个,这样P(k)=P(k-1)+1=k-2+1=k-1, 
结论成立。
凡是能够达到目的的操作都会使结论成立,根据第一数学归纳法原理,问题即证。
2.什么是归纳法
从广义上讲,归纳法是人们在认识事物的过程中所使用的一种思维方法,通过列举少量
的特殊情况,经过分析和归纳推理寻找出基本规律。归纳法比枚举法更能反映问题的本质, 
但是从一个实际问题中总结归纳出基本规律并不是一件容易的事情,而且归纳过程通常也
没有一定的规则可供遵循。归纳法包含不完全归纳法和完全归纳法,不完全归纳法是根据
事物的部分特殊事例得出的一般结论的推理方法,即从特殊出发,通过实验、观察、分析、综
合和抽象概括出一般性结论的一种重要方法。完全归纳法是根据事物的所有特殊事例得出
的一般结论的推理方法。
在算法设计中归纳法常用于建立数学模型,通过归纳推理得到求解问题的递推关系,也
就是采用递推关系表达寻找出的基本规律,从而将复杂的运算化解为若干重复的简单运算, 
以充分发挥计算机擅长重复处理的特点。在应用归纳法时一般用n表示问题规模(n为自
然数), 并且具有这样的递推性质———能从已求得的问题规模为1~n-1或者n/2等的一
系列解构造出问题规模为n的解,前者均称为“小问题”,后者称为“大问题”,而大、小问题
的解法相似,只是问题规模不同。利用归纳法产生递推关系的基本流程如下: 
①按推导问题的方向研究最初、最原始的若干问题。
②按推导问题的方向寻求问题间的转换规律,即递推关系,使问题逐步转化成较低层
级或简单的且能解决的问题或者已解决的问题。
递推算法根据推导问题的方向分为顺推法和逆推法两种。所谓顺推法是从已知条件出
发逐步推算出要解决问题的结果,如图3.4(a)所示。所谓逆推法是从已知问题的结果出发
逐步推算出问题的开始条件,如图3.4(b)所示。
图3.4顺推法和逆推法
前面讨论的数学归纳法和归纳法有什么关系呢? 实质上数学归纳法与归纳法没有逻辑
联系,按著名数学家波利亚的说法是数学归纳法这个名字是随便起的。数学归纳法虽不是
归纳法,但它与归纳法有着一定程度的关联,在结论的发现过程中,往往先对大量个别事实
进行观察,通过不完全归纳法归纳形成一般性的结论,最终利用数学归纳法对结论的正确性
予以证明。3.2节~4节通过几个示例讨论归纳法在算法设计中的应用。

2.3.
2.
78 


79 
3.2.2 直接插入排序
1.问题描述 
有一个整数序列R[0..n-1],采用直接插入排序实现R 的递增有序排序。直接插入排
序的过程是,i 从1到n-1循环,每次循环时将R[i]有序插入R[0.. i-1]中。
2.问题求解 
采用不完全归纳法产生直接插入排序的递推关系。例如R=(2,5,4,1,3),这里n=5, 
用[]表示有序区,各趟的排序结果如下: 
初始:([2],5,4,1,3) 
i=1:([2,5],4,1,3) 
i=2:([2,4,5],1,3) 
i=3:([1,2,4,5],3) 
i=4:([1,2,3,4,5]) 
设f(R,i)用于实现R[0.. i](共i+1个元素)的递增排序,它是大问题;f(R,i-1)实
现R[0.. i-1](共i 个元素)的排序,它是小问题。对应的递推关系如下: 
f(R,i)≡ 不做任何事情当i=1时
f(R,i)≡f(R,i-1);将R[i]有序插入R[0.. i-1]中; 其他 
显然f(R,n-1)用于实现R[0..n-1]的递增排序。这样采用不完全归纳法得到的结
论(直接插入排序的递推关系)是否正确呢? 采用数学归纳法证明如下: 
① 证明归纳基础成立。当n=1时直接返回,由于此时R 中只有一个元素,它是递增
有序的,所以结论成立。
② 证明归纳递推成立。假设n=k 时成立,也就是说f(R,k-1)用于实现R[0..k-1] 
的递增排序。当n=k+1时对应f(R,k),先调用f(R,k-1)将R [0..k-1]排序,再将
R[k]有序插入R[0..k-1]中,这样R[0..k]变成递增有序序列了,所以f(R,k)实现R[0..k] 
的递增排序,结论成立。
根据第一数学归纳法的原理,问题即证。按照上述直接插入排序的递推关系得到对应
的算法如下: 
void Insert(int R[],int i) { //将R[i]有序插入R[0..i-1]中 
int tmp=R[i]; 
int j=i-1; 
do{ //找R[i]的插入位置 
R[j+1]=R[j]; //将关键字大于R[i]的元素后移 
j--; 
} while(j>=0 && R[j]>tmp); / /直 到R[j]<=tmp 为止 
R[j+1]=tmp; // 在 j +1 处插入R[i] 
}
void InsertSort1(int R[]) { // 迭 代算法: 直接插入排序 
int n=R.length;

80 
for(int i=1;i<n;i++) { 
if(R[i]<R[i-1]) //反序时 
Insert(R,i); 
} 
}
在实际中有一些采用不完全归纳法得到的结论明显是正确的,或者已经被人们证明是
正确的,可以在算法设计中直接使用这些结论。
3.2.3 实战———不同路径(LeetCode62★★) 
1.问题描述 
一个机器人位于一个m ×n(1≤m ,n≤100)网格的左上角(起始点标记为“Start”)。机
器人每次只能向下或者向右移动一步。设计一个算法求机器人到达网格的右下角(标记为
“Finish”)总共有多少条不同的路径。例如,m =3,n=3,对应的网格如图3.5所示,答案为
6。要求设计如下方法: 
public int uniquePaths(int m, int n) {} 
2.问题求解 
在从左上角到右下角的任意路径中,一定是向下走m -1步、向右走n-1步,不妨置
x=m -1,y=n-1,路径长度为x+y。例如对于图3.5,这里x=2,y=2,所有路径长度为
4,6条不同的路径如下: 
图3.5 3×3的网格
① 右右下下
② 右下右下
③ 右下下右
④ 下右右下
⑤ 下右下右
⑥ 下下右右
归纳起来,不同路径条数等于从x+y 个选择中挑选x 个“下”或者y 个“右”的组合数, 
即Cxx+y 或者Cyx+y (实际上从数学上推导有Cxx+y =Cyx+y ),为了方便,假设x ≤y,结果
取Cxx+y 。
Cxx+y = (x +y)! 
x!y! = (x +y)(x +y -1)…(y -1)y! 
x!y! 
= (x +y)× … × (y -1) 
x × (x -1)× … ×2×1 
上式中分子、分母均为x 个连乘,可以进一步转换为: 
x +y 
x ×x +y -1 x -1 × … ×y -1 
1 
由于除法的结果是实数,而不同的路径数一定是整数,所以最后需要将计算的结果向上
取整得到整数答案。对应的程序如下: 
扫一扫
视频讲解

81 
class Solution { 
public int uniquePaths(int m, int n) { //求解算法 
return comp(m-1,n-1); 
} 
int comp(int x,int y) { 
int a=x+y,b=Math.min(x,y); 
double ans=1.0; 
while(b>0) 
ans*=(double)(a--)/(double)(b--); 
ans+=0.5; 
return (int)ans; 
} 
}
上述程序提交时通过,运行时间为0ms,内存消耗35.1MB。
说明:在求Cxx+y 时如果先计算(x+y)!,再计算x!y!,最后求二者相除的结果,当x、
y 较大时会发生溢出。
3.2.4 猴子摘桃子问题
1.问题描述 
猴子第1天摘下若干个桃子,当即吃了一半又一个。第2天又把剩下的桃子吃了一半
又一个,以后每天都吃前一天剩下的桃子的一半又一个,到第10天猴子想吃的时候,只剩下
一个桃子。求猴子第1天一共摘了多少桃子。
2.问题求解 
采用归纳法中的逆推法。设f(i)表示第i 天的桃子数,假设第n(题目中n=10)天只剩
下一个桃子,即f(n)=1。另外题目中隐含前一天的桃子数等于后一天桃子数加1的两倍: 
f(10)=1 
f(9)=2(f(10)+1) 
f(8)=2(f(9)+1) 
… 
即f(i)=2(f(i+1)+1)。这样得到递推关系如下: 
f(i)=1 当i=n 时
f(i)=2(f(i+1)+1) 其他
其中f(i)是大问题,f(i+1)是小问题。最终结果是求f(1)。对应的算法如下: 
int peaches(int n) { //第n 天的桃子数为1,求第1 天的桃子数 
int ans=1; 
for(int i=n-1;i>=1;i--) 
ans=2*(ans+1); 
return ans; 
}

82 
上述算法求出f(1)为1534。
3.3 迭 代 法
3.3.1 迭代法概述
迭代法也称辗转法,是一种不断用变量的旧值推出新值的过程。通过让计算机对一组
指令(或一定步骤)进行重复执行,在每次执行这组指令(或这些步骤)时都从变量的原值推
出它的一个新值。
如果说归纳法是一种建立求解问题数学模型的方法,则迭代法是一种算法实现技术。
一般先采用归纳法产生递推关系,在此基础上确定迭代变量,再对迭代过程进行控制,基本
的迭代法算法框架如下: 
void Iterative() { //迭代法框架 
迭代变量赋初值; 
while(迭代条件成立) { 
根据递推关系式由旧值计算出新值; 
新值取代旧值,为下一次迭代做准备; 
} 
}
实际上3.2节中所有的算法均是采用迭代法实现的。
如果一个算法已经采用迭代法实现,那么如何证明算法的正确性呢? 由于迭代法算法
包含循环,对循环的证明引入循环不变量的概念。所谓循环不变量是指在每轮迭代开始前
后要操作的数据必须保持的某种特性(例如在直接插入排序中,排序表前面的部分必须是有
序的)。循环不变量是进行循环的必备条件,因为它保证了循环进行的有效性,有助于用户
理解算法的正确性。如图3.6所示,对于循环不变量必须证明它的3个性质。
初始化:在循环的第一轮迭代开始之前,应该是正确的。
保持:如果在循环的第一次迭代开始之前正确,那么在下一次迭代开始之前它也应该
保持正确。
终止:当循环结束时,循环不变量给了用户一个有用的性质,它有助于表明算法是正
确的。
图3.6 利用循环不变量证明算法的正确性
这里的推理与数学归纳法相似,在数学归纳法中要证明某一性质是成立的,必须首先证
明归纳基础成立,这里就是证明循环不变量在第一轮迭代开始之前是成立的。证明循环不

变量在各次迭代之间保持成立,就类似数学归纳法中证明归纳递推成立。循环不变量的第
三项性质必须成立,与数学归纳法不同,在归纳法中归纳步骤是无穷地使用,在循环结束时
才终止“归纳”。
【例3-3】采用循环不变量方法证明3.2.2节中直接插入排序算法的正确性。
证明:直接插入排序算法中循环不变量为R[0..i-1]是递增有序的。
初始化:循环时i从1开始,循环之前R[0..0]中只有一个元素,显然是有序的。所以
循环不变量在循环开始之前是成立的。
保持:需要证明每一轮循环都能使循环不变量保持成立。对于R[i]排序的这一趟,之
前R[0..i-1]是递增有序的: 
①如果R[i]≥R[i-1],即正序,则该趟结束,结束后循环不变量R[0..i]显然是递增
有序的。
②如果R[i]<R[i-1],即反序,则在R[0..i-1]中从后向前找到第一个R[j]≤R[i], 
将R[j+1..i-1]均后移一个位置,并且将原R[i]放在R[j+1]位置,这样结束后循环不变
量R[0..i]显然也是递增有序的。
终止:循环结束时i=n,在循环不变量中用i替换n,就有R[0..n-1]包含原来的全部
元素,但现在已经排好序了,也就是说循环不变量也是成立的。
这样证明了上述直接插入排序算法的正确性。
3.3.2节~3.3.5节将重点放在迭代法算法的设计上而不是算法的正确性证明上,通过
几个经典应用予以讨论。
3.3.2简单选择排序
1.问题描述
有一个整数序列R[0..n-1],采用简单选择排序实现R的递增有序排序。简单选择排
序的过程是,i从0到n-2循环,R[0..i-1]是有序区,R[i..n-1]是无序区,并且前者的所
有元素均小于或等于后者的任意元素,每次循环在R[i..n-1]无序区采用简单比较找到最
小元素R[minj],通过交换将其放在R[i]位置。
2.问题求解
采用不完全归纳法产生简单选择排序的递推关系。例如R=(2,5,4,1,3),这里n=5, 
用[]表示有序区,各趟的排序结果如下: 
初始:([]2,5,4,1,3)
i=0:([1],5,4,2,3)
i=1:([1,2],4,5,3)
i=2:([1,2,3],5,4)
i=3:([1,2,3,4],5)
设f(R,用于实现R[1]( i个元素) 它是大问题, R,

i) i.
n-共n-的递增排序, 则f(i+1)
i+1.
n-i

实现R[1](共n-1个元素)的排序,它是小问题。对应的递推关系如下: 
f(R,i)≡不做任何事情当i=n-1时
f(R,i.
i]位置

i)
≡ 
在R[n-1]中选择最小元素交换到R[其他
f(i+1)

R,

83 


84 
显然f(R,0)用于实现R[0..n-1]的递增排序。对应的算法如下: 
void Select(int R[],int i) { //在R[i..n-1]中选择最小元素交换到R[i]位置 
int minj=i; //minj 表示R[i..n-1]中最小元素的下标 
for(int j=i+1;j<R.length;j++) { // 在 R[ i. . n- 1] 中找最小元素 
if(R[j]<R[minj]) minj=j; 
} 
if(minj!=i) { //若最小元素不是R[i] 
int tmp=R[minj]; //交换 
R[minj]=R[i]; R[i]=tmp; 
} 
}v
oid SelectSort1(int R[]) { // 迭 代 法 : 简 单选择排序 
for(int i=0;i<R.length-1;i++) //进行n-1 趟排序 
Select(R,i); 
} 
3.3.3 实战———多数元素(LeetCode169★) 
1.问题描述 
见2.12.3节,这里采用迭代法求解。
2.问题求解 
依题意nums中一定存在多数元素。通过观察可以归纳出这样的结论,删除nums中
任意两个不同的元素,则删除后多数元素依然存在且不变。假设nums中的多数元素为c, 
即c出现的次数cnt大于n/2,现在删除nums中任意两个不同的元素得到nums1(含n-2 
个元素): 
① 若两个元素均不是c,则c在nums1中出现的次数仍然为cnt,由于cnt>n/2> 
(n-2)/2,所以c是nums1中的多数元素。
② 若两个元素有一个是c,则c在nums1中出现的次数为cnt-1,由于(cnt-1)/(n-2)> 
(n/2-1)/(n-2)=1/2,也就是说c在nums1中出现的次数超过一半,所以c是nums1中
的多数元素。
既然上述结论成立,设候选多数元素为c=nums[0],计数器cnt表示c出现的次数(初
始为1),i 从1开始遍历nums,若两个元素(nums[i],c)相同,cnt增1,否则cnt减1,相当
于删除这两个元素(nums[i]删除一次,c也只删除一次),如果cnt为0说明前面没有找到
多数元素,从nums[i+1]开始重复查找,即重置c=nums[i+1],cnt=1。遍历结束后c就
是nums中的多数元素。对应的迭代法算法如下: 
class Solution { 
public int majorityElement(int[]nums) { 
int n=nums.length; 
if(n==1) return nums[0]; 
扫一扫
视频讲解

85 
int c=nums[0],cnt=1; 
int i=1; 
while(i<n) { 
if(nums[i]==c) //选择两个元素(nums[i],c) 
cnt++; //相同时累加次数 
else 
cnt--; //不相同时递减cnt,相当于删除这两个元素 
if(cnt==0) { //cnt 为0 时对剩余元素从头开始查找 
i++; 
c=nums[i]; 
cnt++; 
} 
i++; 
} 
return c; 
} 
}
上述程序提交时通过,运行时间为1ms,内存消耗为44.3MB。对应算法的时间复杂度
为O(n),空间复杂度为O(1)。
说明:与2.12.3节的程序相比,上述程序的时间性能得到大幅度提高。
3.3.4 求幂集
1.问题描述 
给定正整数n(n≥1),请给出求{1~n}的幂集的递推关系和迭代算法。例如,n=3时, 
{1,2,3}的幂集为{{},{1},{2},{1,2},{3},{1,3},{2,3},{1,2,3}}(子集的顺序任意)。
2.问题求解 
以n=3为例,求1~3的幂集的过程如图3.7所示。
图3.7 求{1,2,3}的幂集的过程
扫一扫
视频讲解

86 
对应的步骤如下: 
① {1}的幂集M1={{},{1}}。
② 在M1 的每个集合元素的末尾添加2得到A2={{2},{1,2}},将M1 和A2 的全部
元素合并起来得到M2={{},{1},{2},{1,2}}。
③ 在M2 的每个集合元素的末尾添加3得到A3={{3},{1,3},{2,3},{1,2,3}},将
M2 和A3 的全部元素合并起来得到M3={{},{1},{2},{1,2},{3},{1,3},{2,3},{1,2, 
3}}。
归纳起来,设Mi 表示{1~i}(i≥1,共i 个元素)的幂集(是一个两层集合),为大问题, 
则Mi-1是{1~i-1}(共i-1个元素)的幂集,为小问题。显然有M1={{},{1}}。
考虑i>1的情况,假设Mi-1已经求出,定义运算appendi(Mi-1,i)返回在Mi-1中每
个集合元素的末尾添加整数i 的结果,即: 
appendi(Mi-1,i)= ∪ s∈Mi-1add(s,i) 
则: 
Mi =Mi-1 ∪Ai 
其中Ai=appendi(Mi-1,i)。
这样求{1~n}的幂集的递推关系如下: 
M1 ={{},{1}} 
Mi =Mi-1 ∪Ai 当i >1时 
幂集是一个两层集合,采用List<List<Integer> > 类对象存放,其中每个List 
<Integer>元素表示幂集中的一个集合。大问题即求{1~i}的幂集,用Mi 变量表示,小问
题即求{1~i-1}的幂集,用Mi_1变量表示,首先置Mi_1={{},{1}}表示M1,迭代变量i 
从2到n 循环,每次迭代将完成的问题规模由i 增加i+1。对应的迭代法算法如下: 
class Solution { 
List<List<Integer>>deepcopy(List<List<Integer>>A) { //返回A 的深复制 
List<List<Integer>>B=new ArrayList<>(); 
for(List<Integer>x: A) 
B.add(new ArrayList<>(x)); 
return B; 
} 
List<List<Integer>>appendi(List<List<Integer>>Mi_1,int e) { 
//向Mi_1 中每个集合元素的末尾添加e 
List<List<Integer>>Ai=Mi_1; //浅复制 
for(List<Integer>x: Ai) 
x.add(e); 
return Ai; 
} 
public List<List<Integer>>subsets(int n ) { // 迭代法: 求1 到n 的幂集 
List<List<Integer>>Mi=new ArrayList < >( ); / /存 放 幂 集 
List<List<Integer>>Mi_1=new ArrayList<>(); 
Mi_1.add(new ArrayList<>()); / / 添 加 一 个 {} 
ArrayList<Integer>tmp=new ArrayList<>(); 
tmp.add(1);

87 
Mi_1.add(tmp); //再添加一个{1},即Mi_1={{},{1}} 
if(n==1) return Mi_1; //处理特殊情况 
for(int i=2;i<=n;i++) { 
Mi=deepcopy(Mi_1); 
List<List<Integer>>Ai=appendi(Mi_1,i); 
for(List<Integer>x: Ai) //将Ai 中的所有集合元素添加到Mi 中 
Mi.add(x); 
Mi_1=deepcopy(Mi); //用新值替换旧值 
} 
return Mi; 
} 
} 
3.3.5 实战———子集(LeetCode78★★) 
1.问题描述 
给定一个含n(1≤n≤10)个整数的数组nums,其中所有元素互不相同。设计一个算法
求该数组的所有可能的子集(幂集),结果中不能包含重复的子集,但可以按任意顺序返回幂
集。例如,nums={1,2,3},结果为{{},{1},{2},{1,2},{3},{1,3},{2,3},{1,2,3}}。要求
设计如下方法: 
public List<List<Integer>>subsets(int[]nums) { } 
2.问题求解 
将数组nums看成一个集合(所有元素互不相同),求nums的所有可能的子集就是求
nums的幂集,与3.3.4节的思路完全相同,仅需要将求1~n 的幂集改为求nums[0..n-1] 
的幂集。定义运算appendi(Mi-1,nums[i])返回在Mi-1中每个集合元素的末尾插入元素
nums[i]的结果,即: 
appendi(Mi-1,nums[i])= ∪ s∈Mi-1push_back(s,nums[i]) 
则: 
Mi =Mi-1 ∪Ai 
其中Ai=appendi(Mi-1,nums[i])。
这样求nums的幂集的递推关系如下: 
M0 ={{},{nums[0]}} 
Mi =Mi-1 ∪Ai 当i >0时 
对应的迭代法算法如下: 
class Solution { 
public List<List<Integer>>subsets(int[]nums) { //迭代法: 求nums 的幂集 
List<List<Integer>>Mi=new ArrayList<>(); / /存 放 幂 集 
List<List<Integer>>Mi_1=new ArrayList<>(); 
扫一扫
视频讲解

88 
Mi_1.add(new ArrayList<>()); //添加一个{} 
ArrayList<Integer>tmp=new ArrayList<>(); 
tmp.add(nums[0]); 
Mi_1.add(tmp); //再添加一个{nums[0]} 
if(nums.length==1) return Mi_1; //处理特殊情况 
for(int i=1;i<nums.length;i++) { 
Mi=deepcopy(Mi_1); 
List<List<Integer>>Ai=appendi(Mi_1,nums[i]); 
for(List<Integer>x: Ai) //将Ai 中的所有集合元素添加到Mi 中 
Mi.add(x); 
Mi_1=deepcopy(Mi); //用新值替换旧值 
} 
return Mi; 
} 
List<List<Integer>>appendi(List<List<Integer>>Mi_1,int e) { 
//向Mi_1 中每个集合元素的末尾添加e 
List<List<Integer>>Ai=Mi_1; //浅复制 
for(List<Integer>x: Ai) 
x.add(e); 
return Ai; 
} 
List<List<Integer>>deepcopy(List<List<Integer>>A) { //返回A 的深复制 
List<List<Integer>>B=new ArrayList<>(); 
for(List<Integer>x: A) 
B.add(new ArrayList<>(x)); 
return B; 
} 
}
上述程序提交时通过,执行用时为1ms,内存消耗为38.5MB。
3.4 递 归 法
3.4.1 递归法概述
1.什么是递归 
递归算法是指在算法定义中又调用自身的算法。若p算法定义中调用p算法,称为直
接递归算法;若p算法定义中调用q算法,而q算法定义中又调用p算法,称为间接递归算
法。任何间接递归算法都可以等价地转换为直接递归算法,所以本节主要讨论直接递归
算法。