第 4章 算法
当我们试图用计算机解决一个问题时,大
体都要经历如下几个步骤,
1,把问题抽象为一个带有一般性的数学问题,即数
学化。这一步要引入一些数学概念,给出所求问题
的已知条件、所要求的结果、以及在已知条件和所
要求的结果之间存在着的隐式或显式的联系。
2,建立相应的求解方法。这一步要给出问题的求解模型,
即确定问题的数据模型并在此模型上定义一组运算,
然后借助于对这组运算的调用和控制,根据已知数
据导出所要求的结果。
3,用一种程序设计语言描述算法。即将非形式自然语
言表达的算法转变为一种程序设计语言表达的算法。
也可称做程序设计或程序编制。
4,编辑、调试和测试程序代码,直到输出所要求的
结果。
? 获得了计算方法和算法,并不等于问题可解,
是否可解取决于算法的存在性和计算的复杂性。
即是否存在求解算法,算法所需要的时间和空
间在数量级上能否被接受。
? 1900年,数学家希尔伯特在巴黎举行的世界数
学家大会上发表了至今仍著名的演说。在演说
中,他提出了 23个数学问题,这就是著名的
,希尔伯特 23个问题,,并认为它们是对下一
个世纪的挑战。其中的第 10个问题是, 丢番图
方程的可解性问题, 。其具体涵义是设计一个
算法来测试多项式是否有整数根。将该问题做
相应简化可以设,
D={p|p是有整数根的 x的多项式 },则集合 D是否
可判定? 答案是否定的 。
我们可以把多项式输入到计算机中进行处理,当
x赋值为 0,-1,1,-2,2,-3,3,…… 时,分
别求出多项式的值,一旦求得 0值,就接受,如
果 p没有整数根,则程序将永远运行下去 。
本章内容
? 4.1 算法的概念
? 4.2 算法的表示
? 4.3 常用算法介绍
? 4.4 并行算法
? 4.5 算法的评价
? 4.6 算法的设计要求
4.1.1 算法的定义
? 算法是程序设计的精髓,程序设计的实质就是
构造解决问题的算法,将其解释为计算机语言。
? 有关算法 (algorithm)的定义不只一种,其中
一种叙述为:算法是对特定问题求解步骤的一
种描述,它是指令的有限序列,其中每一条指
令表示一个或多个操作。
曾获图灵奖的著名科学家克努斯 ( D.Knuth)
对算法这一概念给出了进一步的描述 。
? 一个算法,就是一个有穷规则的集合,其中
之规则确定了一个解决某一特定类型问题的
运算序列。此外,算法的规则序列必须满足
下列五个重要条件,
( 1) 有穷性 。 算法必须总是在执行有穷步之后结束;
( 2) 确定性 。 算法的每一步骤必须被确切地定义;
( 3) 输入 。 算法有零个或多个输入;
( 4) 输出 。 算法有一个或多个输出;
( 5)能行性。算法中有待执行的运算和操作必须是相当
基本的,即它们原则上都是能够精确地进行的,而且
用笔和纸做有穷次就可以完成。
除了算法的非形式化定义外,还有算法的形式化定义,
算法是一个四元组,即( Q,I,O,F)。
其中,
1 Q 是一个包含子集 I 和的集合它表示计算的状态 。
2 I 表示计算的输入集合;
3 O 表示计算的输出集合;
4 F 表示计算的规则,它是一个由 Q 到它自身的函数,且
具有自反性,即对于任何一个元素 q∈ Q,有 F(q)=q 。
4.1.2 性质
在 4.1.1中我们曾介绍了克努斯给出的算
法定义,其中包括算法成立的五个重要条
件,实际上就是算法的规则序列必须满足
的五个特性 。
例 1 给定两个整数 m和 n,试写出它们的最大公因
子的算法。
其算法如下,
1,读入两个正整数 m和 n(设 m>n)。
2,求 m和 n的余数 r。
3,用 n的值取代 m,用 r的值取代 n。
4,判断 r的值是否为零,如果 r=0,则 m为最大公
因子,否则返回 2
5,输出 m的值。
? 我们可以看到,无论给出多大的两个数,这个
算法都会在执行一定次数之后结束。这是算法
的有穷性要求:, 算法必须总是(对任何合法
的输入值)在执行有穷步之后结束,且每一步
都可在有穷时间内完成, 。
? 给出的数值必须是确定的,不能出现诸如
,比 m大一些或比 n小一点儿, 之类的无法确
定数值的表达方式。这是算法的确定性要求:
算法中每一条指令必须被确切地定义,读者
理解时不会产生二义性。
? 算法一定是可操作的,当我们用笔和纸代替
计算机去完成计算过程时,是可以做到的。这
是算法的可行性要求:即算法中描述的操作
都是可以通过有限次的基本运算执行来实现
的。
? 一个算法有零个或多个的输入。在上述的例
子中共有两个已知变量,m和 n,否则算法将无
法执行下去,这是算法的输入要求。
? 算法必须给出结果,在本例中,结果为 m和 n
的最大公因子。这是算法的输出要求:一个
算法有一个或多个的输出,这些输出是与输
入有着某些特定关系的量。
4.2 算法的表示
? 算法不能直接被计算机执行,使用它的
目的就是把你的思想表达出来。可以使
用专门的工具进行描述。常用的描述工
具有:自然语言、流程图、决策表及算
法描述语言等。
? 关于最大公因子的描述方法使用的就是
自然语言,即人们 日常交流所用的语言
进行描述。
1 自然语言的歧义性容易导致算法执行的不
确定性;
2 自然语言的语句一般太长从而导致了所描
述的算法太长;
3 由于自然语言表示的串行性,因此当一个算
法中循环和分支较多时就很难清晰地表示出
来;
4 自然语言表示的算法不能直接被计算机接
受。
由于自然语言具有二义性和不确定性而难以严
格表示算法的逻辑流程,因此一般不用自然语
言描述算法。
? 流程图是用一种图形工具描述算法或程序结
构。 流程图是描述算法的常用工具,它采用
美国国家标准化协会 ANSI ( American
National Standard Institute) 规定的一
组图形符号来表示。 算法流程图可以很方便
地表示顺序选择和循环结构,而任何程序的
逻辑结构都可以用顺序选择和循环结构来表
示,因此流程图可以表示任何程序的逻辑结
构。
例如,用矩形表示处理,用菱形表示判断,用
平行四边形输入 /输出,用带箭头的折线表示
流程等等。
将例 1用流程图表示如下,
begin
end
write m
read m,n
n=r
m=n
r=mod(m,n)
r=0
T
F
从上例中我们看到, 只要掌握了流程图
中各种符号所代表的含义, 即可以知道
要完成的操作 。 下面让我们再看一个例
子,
例 2 求解 1到 100的和的算法流程图
开始
结束
X=X+Y
Y=Y+1
Y=2
X=1
Y>100
write x
T
F
? 决策表又称为判断表,它列出了对于所
有可能的条件应采取的动作。适用于描
述一个复杂的判断过程。
? 算法描述语言是一种把自然语言与程序
设计语言相结合起来的算法描述工具。
保留了程序设计语言严谨的结构、语句
的形式和控制成分,忽略了繁琐的变量
说明。
例1的算法描述语言,
PROCDURE Euclid’s algorithm;
BEGIN
READ( m,n) ;
REPEAT;
r:=mod(m,n);
m:=n;
n:=r;
UNTIL r=0;
WRITE (m)
END
? 上面的这种描述示方法与计算机程序
非常相似,但又不是正式的程序,需
要进一步加工才能在计算机中实现。
例 1演示
例 3 给出小于 100的斐波那契数的算法描述 。
首先,我们给出斐波那契数列(, 兔子问
题, ), 0,1,1,2,3,5,8, 13,
21,34,…
假设一对刚出生的兔子一个月后就能长大,
再过一个月就能生下一对兔子,并且此后每
个月都能生一对兔子,且新生的兔子在第二
个月后,也是每个月生一对兔子。问一对兔
子一年内可繁殖成多少对兔子?
其算法可描述如下,
PROCEDURE Fibonacci;
BEGIN
n1:=0;
n2:=1;
n3:=0;
WRITE (n1,n2);
WHILE n3<100 DO
BEGIN
n3:=n1+n2;
WRITE(n3);
n1:=n2;
n2:=n3;
END
END
例 3演示
? 算法是供人来阅读的,必须牢记这一点。算
法中语句的书写格式采用缩进规则,保留字
用大写,其余标识符小写,以提高算法的易
读性。
4.3 常用算法介绍
? 算法包括数值计算算法和非数值计算算
法两类,后者尤为重要。关于算法的概
念及应用,更深入的内容将在算法设计
与分析等课程中学习。在此简单介绍分
治法、递归法、迭代法等。
4.3.1递归
? 所谓递归是指:一个过程用自身的简单情况来
定义自身,再自己调用自己则称它们是递归的,
或者是递归定义的。
递归是一种强有力的数学工具,它可使问
题的描述和求解变得简洁和清晰。递归算法常
常比非递归算法更易设计,尤其是当问题本身
或所涉及的数据结构是递归定义的时候,使用
递归算法特别合适。
例 1 非负整数 n的阶乘可递归定义为,
n!=
1 n=0
n*(n-1) n=0
? 递归算法的设计步骤
第一步骤(递归步骤):将规模较大的原问
题分解为一个或多个规模更小、但具有类似
于原问题特性的子问题。即较大的问题递归
地用较小的子问题来描述,解原问题的方法
同样可用来解这些子问题。
第二步骤:确定一个或多个无须分解、可直
接 求解的最小子问题(称为递归的终止
条件)。
例 2 函数 f(n)可递归定义为,
f(n)=
n*f(n- 1) (n>0)
1 (n=0)
( 1)递归边界条件。也就是所描述问题的最简
单情况,它本身不再使用递归的定义。如上例,
当 n=0时,f(n)=1,不使用 f(n-1)来定义。
( 2)递归定义:使问题向边界条件转化的规则。
递归定义必须能使问题越来越简单。 如上例:
f(n)由 f(n-1)定义,越来越靠近 f(0),也即边界条
件。最简单的情况是,f(0)=1。
裴波那契数的形式化定义, f(n)=f(n-1)+f(n-
2); f(0)=1; f(1)=2。事实上,这种定义方法
就是递归。它所对应的递归程序为,
Function fib(n, integer), integer;
Begin
If n = 0 then fib,= 1 { 递归边界 }
Else if n = 1 then fib,= 2
Else fib,= fib(n-2) + fib(n-1) { 递归 }
End;
? 递归算法能使一个蕴含递归关系且结构
复杂的程序简捷精炼,增加可读性。特
别是在难于找到从边界到解的全过程的
情况下,如果把问题推进一步,没有结
果仍维持原问题的关系,则采用递归算
法编程比较合适 。
用递归算法解决汉诺塔问题。假设有三
个塔,每个塔都堆放 n 个盘子。开始
时,所有盘子均在塔 A上,并且,盘从上
到下,按直径增大的次序放置。此题目
的目的是设计一个盘子移动的序列,使
得塔 A 上的所有盘子借助于塔 B 移动
到塔 C 上。
其中, 有两个限制条件, 1,一次只能搬动一
个盘子 。 2,任何时候不能把盘子放在比它小
的盘子的上面 。
第 1步, 把 n-1 个盘子从塔 A 搬到塔 B。
第 2步, 将剩下的一只盘 (也就是最大的一只 )
直接从 A 塔搬到那个仍然空着的塔 C 。
第 3步:用第一步描述的方法,再次将 B
塔上的盘搬到 C 塔。操作方法和第一步一
样,但这一步实际上是由 n-1的盘子组成。
这一步是没有问题的,因为塔 C上仅有的一只
盘是最大的。
算法演示
main() /* 读入要搬动的盘的个数 */
{
int disks; /*定义变量 disks,用于存放盘的个数 */
void towers(int,char,char,char); /*函数 tower()声明
*/
printf("Number of disks,");
scanf("%d",&disks); /*输入盘的数目 */
towers(disks,'A','B','C'); /*调用函数
towers() 用于完成递归算法 */
}
void move_disk(char src,char dst)
{
printf("%c ====> %c",src,dst);
}
void towers(int n,char src,char mid,char dst)
{
void move_disk(char,char);
if (n==1)
{
move_disk(src,dst);/*调用函数 move_disk()打印盘从一个
塔到另一个塔的搬动过程 */
return;
}
towers(n-1,src,dst,mid);
move_disk(src,dst);
towers(n-1,mid,src,dst);
}
4.3.2 迭代
? 迭代法是一种逐次逼近法。迭代一词按
其字面解释,,迭, 代表屡次和反复的
意思,,代, 表示替换的意思,合起来
迭代就是反复替换的意思。在程序设计
中为了处理重复性计算的问题,最常用
的方法就是迭代方法。主要是循环迭代。
? 迭代与递归有着密切的联系,甚至前面提到
的 X0=a,Xn+1=f(n)的递归关系也可以看作是
数列的一个迭代关系,可以证明迭代程序都
可以转换为与它等价的递归程序,反之则不
然,就效率而言,递归程序的实现要比迭代
程序的实现耗费更多的时间和空间,因此在
具体实现时又希望尽可能将递归程序转化为
等价的迭代程序。
有的问题既可以用递归,也可以用迭代
方法完成,但并非全部,像汉诺塔问题
只能用递归方法实现,不能用迭代方法
解决。
用迭代方法求解 斐波那契数 问题的自然语言描
述如下,
1) 如果 n=0,那么将 0 赋值给 Y,并输出 Y,
转步骤 11,否则继续执行;
2) 将 0 赋给 X, 将 1 赋值给 Y;
3) 输出 X,Y;
4) 将 1 赋值给 I;
5) 如果 I 大于 n-1,则转到步骤 11,否则
继续执行;
6)将 X 和 Y 的和赋值给 Z;
7) 将 Y 赋值给 X;
8) 将 Z 赋值给 Y;
9) 将 Y 输出;
10) 将 I 加 1,转步骤 5 继续执行;
11) 算法结束。
4.3.3 分治法
? 分治法的设计思想是将一个难以直接解决的
大问题,分割成一些规模较小的相同问题,
以便各个击破,含, 分而治之, 之意。
? 如果原问题可分割成 k个子问题,1<k≤n,且
这些子问题都可解,并可利用这些子问题的
解求出原问题的解,那么这种分治法就是可
行的。
? 由分治法产生的子问题往往是原问题的较小
模式,这就为使用 递归技术 提供了方便。在
这种情况下,反复应用分治手段,可以使子
问题与原问题类型一致而其规模却不断缩小,
最终使子问题缩小到很容易直接求出其解。
这自然导致递归过程的产生。分治与递归像
一对孪生兄弟,经常同时应用在算法设计之
中,并由此产生许多高效算法。
例如,求 n个元素集合 S的最大元素和最小元
素问题。可以把 S分成大小相等的两个子集 S1
和 S2,递归的求出 S1及 S2的最大元素和最小
元素。只要从两个最大元素中选出一个较大
的,从两个最小元素中选出一个较小的,则
它们分别是 S的最大元素和最小元素。
后续将要介绍的二分查找法,也运用了分治法
的思想。例如,给定一排好序的整型序列,查
找元素 x是否在该序列中。这时,需将待查元
素与 中值元素 进行比较,即把一个长度为 n的
有序表分成两个长度小于等于 n/2的有序段,
依次进行查找,如果查找不成功,则需查找剩
下的另一段。
4.4 并行算法
? 并行算法是指一次可执行多个操作的算法。对
并行算法的研究现在已发展为一个独立的研究
领域。很多用串行算法解决的问题也已经有了
相应的并行算法。
这里介绍的并行算法将用一种流行的理论模
型即并行随机存取计算机 (PRAM)来描述。因
为很多关于数据结构的概念,如数组、表、
树和图的并行算法都可以很容易地用 PRAM
模型来描述,所以,这里对 PRAM模型做一
简单介绍。
1,PRAM模型
图 4.3说明了 PRAM的基本结构。其中有 n个普
通的串行处理器 p0,p1,...,pn-1共享一个全局
存储器。所有处理器都可以, 并行地, (同时 )
从全局存储器读出信息或向全局存储器写入
信息。各处理器也可以并行地执行各种算术
和逻辑操作。
P0
Shared
memory
P1
P2
Pn1
..,
..,
? 并行算法的运行时间既依赖于执行算法的处理
器数目,也依赖于输入问题本身的规模。一般
来说,在分析 PRAM算法时我们必须同时讨论
其时间和处理器数目,这与串行算法完全不同,
在串行算法中我们主要集中于对时间的分析。
? 并发存储器存取方式与互斥存储器存取方式
并发读算法是指在算法执行过程中多处理器
可以同时对共享存储器的同一位置进行读操
作的一种 PRAM算法。互斥读算法是指在算
法执行中任何两个处理器都不能同时对共享
存储器的同一位置进行读操作的一种 PRAM
算法。
? EREW:互斥读且互斥写
? CREW:并发读且互斥写
? ERCW:互斥读且并发写
? CRCW:并发读且并发写
? 同步与控制
PRAM算法必须高度同步以保证其正确执
行。如何获得这一同步特征?另外,
PRAM算法中的处理器常常必须测试循环
的终止条件,而这一终止条件又往往取决
于所有处理器的状态。如何实现这种控制
功能?
许多并行计算机采用了一种连接各处理器的
控制网络,以帮助实现同步和对终止条件进
行测试。在特定情况下,控制网络实现这些
功能的速度可以与路径选择网络实现对全局
存储语句的速度一样快。
为方便起见,我们假设各个处理器都有严格
同步的特征。所有处理器同时执行相同的语
句。处理器执行代码的进度也保持一致。
4.5 算法的评价
? 对算法的研究,第一步是设计算法,第二
步是复杂性分析。对同一问题,若有两种
不同的算法,两种算法的优劣要根据复杂
性分析的结果来判断。一般认为需要的时
间较短,存取单元较少的算法是较好的。
4.5.1 算法的时间复杂度
? 算法的时间复杂度( Time Complexity)
是依据算法编制成程序后在计算机中运
行时所耗费的时间的大小来决定的。一
个程序在计算机上运行时所耗费的时间
取决于程序运行时输入的数据量、对源
程序编译所需要的时间、执行每条指令
所需的时间以及程序中语句执行的次数。
其中一个最重要的指数是程序中语句重
复执行的次数。
? 算法的时间复杂度 T(n)实际上是表示当
问题的规模 n充分大时该程序所占用时间
的一个数量级。例如,若某程序运行时
的时间复杂度为 T(n)=2n3+3n2+2n+1,则表
明程序运行所需要的时间与 n3成正比。
引入符号,O”,则有 T( n) =O(n3)。
4.5.2 算法的空间复杂度
例 1,在三个整数中求最大值。其中,算法(一)如下,
max(int a,int b,int c) /*给出三个整数 */
{ if (a>b) /*如果 a>b*/
{ if(a>c) return a; /*如果 a>c,返回 a*/
else return c; /*否则返回 c*/
}
else
{ if(b>c) return b; /*如果 b>c,则返回 b*/
else return c; /*否则返回 c*/
}/*运算过程无需额外存储空间,只需两次比较 */
算法(二)如下,
max(int a[3])
{ int c,int i;
c=a[0];
for(i=1;i<3;i++)
if (a[i]>c) c=a[i];
return c;
} /*需要两个额外的存储空间,两次比较,至少一次
赋值,共需 5个整型数空间 */
当用算法(一)实现求 100个整数中的最大值时,算法
几乎是无法描述的,但算法(二)只要稍做改动即可。
max(int a[100])
{ int c,int i;
c=a[0];
for(i=1;i<100;i++)
if (a[i]>c) c=a[i];
return c;
} /*共需 102个整型数空间 */
例 1(二 )
? 算法的空间复杂度 (Space Complexity)是指
依据算法编制成程序后在计算机中运行时所
占用的空间的大小。一个程序在计算机上运
行所占用的空间同样也是 n的一个函数,称为
算法的空间复杂度,记为 S( n)。如果
S(n)=O(n2),表示运行时所占用的空间与 n2
成正比。
4.5.3 算法的易理解性
? 一个算法如果难以理解,将给应用和评价带来
一定的困难,易理解性是衡量一个算法优劣的
重要标准。因为给出算法的目的是让别人能读
懂,理解并编写相应的程序进行维护和修改。
因此,具有良好的结构、易理解、易维护的算
法是人们追求的目标。
4.6 算法设计的要求
1 正确
算法应当满足具体问题的需求。正确性
是设计和评价一个算法的首要条件。如果
一个算法不正确,其他方面就无从谈起。
一个正确的算法是指在合法的数据输入情
况下,能在有限的运行时间内得出正确的
结果。选用或设计的算法首先应该保证是
正确的。
2 可读
算法最主要目的是为了阅读与交流,并将它
转换成可实现的程序在计算机中执行。可读
性好不仅有助于人们对算法的理解,而且也
有利于程序的调试与修改。在保证算法正确
的前提下,应十分强调算法的可读性。
3 健壮
健壮是指算法对各种可能的情形都考虑的
非常完善。当输入数据非法时,算法也能
适当地作出反应或进行处理。而不会发生
异常或输出莫名期妙的结果。例如,输入
三角形的三条边长时,若其二边之和小于
另一条边,则算法执行中应输出相应的通
告信息而不是发生异常。
4 高效
它是指算法的执行效率要高。算法的效率包
括时间效率与空间两个方面。时间效率是指
算法的执行所需要的时间,而空间效率是指
执行该算法所需的存储量。对应同一个问题
和同一问题的规模,若采用不同的算法进行
处理,其执行的效率就可能不相同。对于一
种算法来说,如果执行的时间越短,而所需
的存储量越小,则其效率就越高。
在许多情况下各种因素是互相制约的。要
求算法的可读性好,则其效率就不一定理
想;如果要求执行时间短,则所需的存储
量就可能比较大,这种场合是以空间为代
价来换取时间的;如果要求使用的存儲量
较小,则执行时间就可能较大,这种场合
是以时间为代价来换取空间的。
本章关键术语,
算法的
定义
算法的
特性
算法的
表示
流程图 算法描述
语言
递归 迭代 分治 并行算
法
时间复杂
度
空间复
杂度
可读性 健壮性 可行性
思考与练习,
1) 什么是算法?它具有哪些特性?
2) 描述算法的常用工具有哪几种?
3) 如何衡量一个算法的优劣?
4) 比较 斐波那契数列的递归算法和迭代
算法,有哪些区别?