第5章循环结构程序设计
本章介绍循环的概念,循环结构设计的基本方法和技术,利用分支和转向语句书写循环程序,利用专门的循环语句书写循环程序。
5.1 循环结构
循环结构分为无条件循环和有条件循环。无条件循环就是无休止地反复执行一个程序段,而有条件循环就是每次执行程序段之前需要根据设置的条件判断是否继续循环。
5.1.1 循环结构的构成
例如对例4.3提出的判断肥胖问题。如果现在要求考察5个人的体重情况,考虑算法时,不能像写流水账一样,输入第1个人的身高、体重,判断第一个人的体重情况;输入第2个人的身高、体重,判断第二人的体重情况;…;输入第5个人的身高、体重,判断第5个人的体重情况。
这样的算法显然是不行的。既然每个人体重情况的处理过程都是一样的,则可以将体重情况的处理过程描述为:“输入身高、体重(每次输入的身高、体重不同!),判断体重情况并给出提示信息”,可以加一条流线将图4.7修改得到问题要求的流程图5.1。从图中可以看出形成了循环结构,被循环执行的部分就是处理一个人的身高、体重数据的操作,称为循环体。
图5.1 重复判断的流程示意图
为了描述流线表示的操作,FORTRAN语言提供了GOTO语句,即无条件转向语句。
1)GOTO 语句
格式:GOTO <标号>
功能:转向“标号”所指的语句去执行。
说明:“标号”是合法的1~5位正整数,并且应该是本程序单位中某语句的语句标号。
根据流程图5.1,可以写出对应程序如下:
REAL H,W0
5 READ*,H,W0
W1=H-110
IF(ABS(W0-W1).LE.5)THEN
PRINT *,'标准!'
ELSEIF(W0.GT.W1) THEN
PRINT *,'过胖!'
ELSE
PRINT *,'过瘦!'
ENDIF
GOTO 5
END
由于在END语句前增加了一条GOTO语句,在READ语句和GOTO语句之间就形成了一个循环过程。执行该程序时,用户每次输入一组H和W0值,程序就给出体重情况的提示。但是该循环结构是无条件循环结构,因此程序成为一个无条件循环程序,执行时将进入“死循环”。从流程图中也可以看出没有“结束”框,程序中的END语句永远都不会被执行。
无条件循环实际是一个不正确的循环结构,因为计算机执行这样的程序会出现“死循环”现象,程序永远不会结束,计算机永远不会停机(除非用户强制性地终止程序执行!)。
解决程序“死循环”的办法就是采用“条件循环”,即根据问题提供的条件为循环结构人为“设置出口”,当问题得到解答后程序自动结束,退出程序执行状态,返回操作系统。
因此,当用户设计循环结构时,必须考虑三个要素:
(1)初始化。为进入循环结构作好准备,对变量赋初值。
(2)设计循环体。常用的方法是递推、迭代、穷举。
(3)设置循环出口。采用“计数”或“设置条件”等方法。
2)为循环结构设置出口采用的两种方法
(1)用“计数”方法设置循环出口
如果问题中已经提供了“循环体”将要循环的次数N,则可以使用“计数”的方式为循环结构设置出口,具体方法如下:
任意设置一个变量I(当然也可以是其它变量)作为计数器,I的初值设为0,每执行一次“循环体”,计数器就计数一次(I=I+1),然后判断I的值是否已经达到N(是否已经计满?),如果没有计满则返回“循环体”继续执行,否则不再返回“循环体”,直接跳出循环结构。计数器的值逐步递增称为“正向计数”,如果将I的初值设为N,然后I的值逐步递减,直到I的值变为0,则称为“反向计数”。
对本节开始的“判断肥胖问题”,要求考察5个人的体重情况,则可以用计数方式为循环结构设置出口,避免出现“死循环”。
设计数器I=0,画出流程图如图5.2和5.3所示的判断过程。图5.2和图5.3都能自动地控制5次要求输入5个人的身高H、体重W0,正确判断体重情况。
(2)用“条件”设置循环出口
有的问题不能提供循环体执行的次数,程序设计者可以针对问题的特点人为设置“条件”,约定其作为跳出循环结构的信号。
假如上述问题要求是“判断一批人的肥胖情况”。问题中既没有说明这一批人的具体人数,也没有提供其它“条件”,采用什么方法为循环结构设置出口呢?
图5.2 用“正向计数”控制循环的流程图
图5.3 用“反向计数”控制循环的流程图
可以与用户约定用某个特定的输入数据作为“结束标志”,即计算机如果得到了某个特定的数据就跳出循环结构。由于人的身高H、体重W0都不可能为“负数”,所以可以选择用“负数”作为特定的“结束标志”。求解该问题的流程图如图5.4所示。
图5.4 用“条件”控制循环的流程图
用户必须遵照约定的条件,当把“一批人的体重情况”都判断完后,最后必须输入一组“负数”,让机器跳出循环结构。
采用这种办法设置循环结构的出口,要求“结束标志”必须“远离”问题涉及的有效数据,即不能与问题中涉及的有效数据混淆。因为,如果将“结束标志”约定为问题中可能出现的有效数据,则当该数据出现时,机器已经将其作为结束标志结束循环了,导致程序结果不正确。
3)构造循环体
设计循环结构时首先要考虑怎样构成循环体,本节提出的例子中循环体的工作很简单,就是“输入数据,处理数据”,不需要作特别的技术处理,但在解决很多实际问题时构造循环体的工作并不容易,需要将一个复杂的过程进行细化,提炼出最基本的操作用递推、迭代、穷举等方法进行处理。
(1)用“递推”的方法构造循环体
采用“递推”的方法时,要求提供“初始条件”,根据“递推关系(公式)”,由前面的结果得到后面的结果。
【例5.1】已知一个数列的第1、2项为1,从第3项开始,以后各项的值都为其前两项的和。产生该数列的前20项,或者当某项的值超过105为止。
分析:根据已知的第1、2项,依次得到后续各项,可以采用“递推”的方法构造循环体。如果设第1、2项为A,B,则第3项C=A+B;然后将B的值传给A(A←B),C的值传给B(B←C),新的第1、2项又准备好了,即可以得到新的第3项C=A+B;……;一直到产生所有的数列项。
根据上述思想,可以得到用“计数”方式控制循环出口的流程图,见图5.5。
因为已经产生并输出了第一和第二项,故I的初值设为2,只要I的值不等于20,就说明还没有完成20项。
图5.6是用“条件”设置循环出口的流程图。利用问题中的“当某项的值超过105为止”作为循环出口的条件。
图5.5 用“计数”方式控制循环出口
图5.6 用“条件”方式控制循环出口
(2)用“穷举”的方法构造循环体
有些问题中,循环体的构造找不出递推关系,不能用递推方法实现,往往需要配合循环控制过程,对所有可能的情况一一列出进行分析、判断。这种将对象一一列出的方法叫“穷举”。
【例5.2】任意输入两个正整数M,N,求它们的最大公约数。
分析:根据数学知识,两个数的最大公约数即它们的共同约数中最大的那个数。极端的情况是M、N 中较小者就是最大公约数。可以用1、2、3……K(M,N中较小者)依次去考察是否它们的公约数,找到一个就保留一个,最后保留的公约数就是最大公约数。流程图如图5.7所示。
流程图中穷举的1、2、3…..K,一方面在循环体中充当被考察的对象,另一方面又用K作为设置循环出口的计数终值。
图5.7 “穷举法”求最大公约数的算法流程图
4)初始化
有些工作必须在进入循环结构之前作好,以便循环结构正常循环起来,比如计数器清0,累加器清0等,这些工作称为初始化。到底初始化需要做些什么工作要视具体问题而定。
5.1.2 循环结构的两种类型
1)当型循环
从语句书写顺序看,循环结构中设置的循环出口是在循环体之前的话,实际表示的控制关系是:当“条件成立时执行循环体”。故称为当型循环。
如图5.3、图5.4、图5.5、图5.6、图5.7构成的循环结构都是当型循环结构。“当型”循环结构都可以概括为如图1.7(a)所示的控制关系。
2)直到型循环
从语句书写顺序看,循环结构中设置的循环出口是在循环体之后的话,实际表示的控制关系是:执行循环体,直到“条件不成立时结束循环”。故称为直到型循环。
图5.2构成的循环结构就是“直到型”循环。“直到型”循环结构都可以概括为图1.7(b)所示的控制关系。
通过上面介绍的例子,读者对循环结构设计方法有了初步认识,知道了设计循环结构应该关注的几个问题。对于同样的问题,其解决的方法和控制过程不是唯一的。
在构造循环结构时,到底设计成“当型”或“直到型”,并不作强行要求。可以根据问题的特点和程序设计者的喜好来决定,有些问题既可以设计成“当型”也可以设计成“直到型”。
5.2 用GOTO语句设计的循环程序
通过对问题的分析,提出构造循环体的办法和控制循环过程的方式,可以画出求解问题的流程图,为了将流程图转换成计算机能执行的程序,可以利用FORTRAN语言提供的相应功能的语句得到FORTRAN语言的源程序。
5.2.1 块IF与GOTO 语句实现的循环
用块IF结构与GOTO语句配合实现循环结构,一般是针对当型循环,其基本形式为:
S1 IF(逻辑表达式)THEN
循环体
GOTO S1
ENDIF
根据第四章介绍的块IF结构的功能,以及本章开始介绍的GOTO语句的功能,可以将图5.3、图5.4、图5.5、图5.6、图5.7构成的循环结构翻译成FORTRAN语言的源程序。
图5.3对应的FORTRAN语言源程序:
PROGRAM MAIN
I=0
5 IF(I.LT.5)THEN
READ *,H,W0
W1=110-W0
IF(ABS(W0-W1).LE.5)THEN
PRINT *,‘标准’
ELSE IF(W0.GT.W1)THEN
PRINT *,‘过胖’
ELSE
PRINT *,‘过瘦’
ENDIF
I=I+1
GOTO 5
ENDIF
END
图5.4对应的FORTRAN语言源程序:
PROGRAM MAIN
READ *,H,W0
5 IF(H.GT.0.AND.W0.GT.0)THEN
W1=110-W0
IF(ABS(W0-W1).LE.5)THEN
PRINT *,‘标准’
ELSEIF(W0.GT.W1)THEN
PRINT *,‘过胖’
ELSE
PRINT*,‘过瘦’
ENDIF
READ *,H,W0
GOTO 5
ENDIF
END
图5.5对应的FORTRAN语言源程序:
PROGRAM MAIN
DATA A,B/2*1.0/
PRINT *,A,B
I=2
5 IF(I.LT.20)THEN
C=A+B
PRINT *,C
A=B
I=I+1
GOTO 5
ENDIF
END
图5.6对应的FORTRAN语言源程序:
PROGRAM MAIN
DATA A,B/2*1.0/
PRINT *,A,B
C=A+B
5 IF(C.LE.1E5)THEN
PRINT *,C
A=B
B=C
C=A+B
GOTO 5
ENDIF
END
图5.7对应的FORTRAN语言源程序:
PROGRAM MAIN
INTEGER FCH
READ *,M,N
K=MIN(M,N)
I=1
5 IF(I.LE.K)THEN
IF(MOD(M,I).EQ.0.AND.MOD(N,I).EQ.O)FCH=I
I=I+1
GOTO 5
ENDIF
WRITE(*,100)M,‘和’,N,‘的最大公约数是:’,FCH
100 FORMAT(1X,I4,A,I4,A,I4)
END
5.2.2 逻辑IF语句与GOTO 语句设计的循环程序
用逻辑IF语句与GOTO语句实现循环一般是针对“直到型”循环,一般格式是:
S1 循环体
IF(逻辑表达式) GOTO S1
根据逻辑IF语句的功能,配合GOTO语句可以写出图5.2对应流程图的FORTRAN语言源程序。
PROGRAM MAIN
I=0
5 READ *,H,W0
W1=110-W0
IF(ABS(W0-W1).LE.5)THEN
PRINT *,‘标准’
ELSE IF(W0.GT.W1)THEN
PRINT *,‘过胖’
ELSE
PRINT *,‘过瘦’
ENDIF
IF(I.LT.5)GOTO 5
END
5.3 用循环语句书写循环程序
FORTRAN语言还提供了专门的循环语句,将循环过程的“判断”、“计数”、“转向”等固定动作包含在循环语句中,因此,利用循环语句可以更方便、快捷地描述循环结构,使程序更加清楚、简练。
5.3.1 DO-WHILE循环
对当型循环可以很方便地使用FORTRAN语言提供的DO-WHILE语句实现。
1)语句格式
DO 标号[,] WHILE(逻辑表达式)
循环体
标号终端语句
或者:
DO WHILE(逻辑表达式)
循环体
ENDDO
2)语句功能
(1)计算〈逻辑表达式〉的值;
(2)如果结果为“真”则执行“循环体”直到遇到终端语句,返回①;
(3)如果结果为“假”则跳出循环结构,执行终端语句的后续语句。
DO-WHILE语句的功能完全可以用图1.7(a)表示。这种循环结构只表述了“当条件成立执行循环体”的“当型”循环结构,既可以用于用“条件”设置出口的循环,也可以用于用“计数方式”设置出口的循环。如果用“计数方式”设置出口不要忘了需要设置计数器。
3)说明
(1)DO-WHILE语句表述的循环结构中没有专门的转向语句,转向的功能是隐含在终端语句中的,因此,终端语句除了完成本身的功能外,还向计算机提示循环体结束,并使控制自动转向DO-WHILE语句。
(2)由DO-WHILE语句构成的“当型”循环结构程序中,DO-WHILE语句执行的次数不止一次。
(3)原则上终端语句可以是一般的执行语句,如打印语句、赋值语句、输入语句等,但是受其特性的限制,下列语句不能作终端语句:GOTO、块IF、ELSE、ELSEIF、ENDIF、END、STOP、RETURN和所有的非执行语句。另外,如果逻辑IF语句(行IF语句)作为终端语句时,它不应该包括DO、块IF、ELSE IF、ELSE、ENDIF、END和另一个逻辑IF语句。下面的语句段是不正确的:
DO 10 WHILE(I.LE.20)
K=I*I
PRINT *,I,K
10 IF(K.GT.100) GOTO 5
总之,循环终端语句的功能应该是明确的,不能是不定的。
为了使循环体的起止范围清晰,使终端语句与一般的执行语句有所区别,可以使用FORTRAN语句提供的继续语句作为循环体的终端语句。继续语句的一般格式为:
CONTINUE
它的功能就是执行一个空操作,因此CONTINUE又称为空语句。
用CONTINUE语句作循环体的终端语句有下列优点:
①使循环体的范围清晰,终端语句规范化,凡是DO-WHILE语句构成的循环结构都用CONTINUE结束,
容易识别。
②不再使用一般的执行语句作循环终端语句,避免出现不符合语法要求的错误。
③用户不用再去记忆那些语句可以作终端语句,那些语句又不能作终端语句,减轻用户负担。
例如:
DO 10 WHILE(I.LE.20)
K=I*I
PRINT*,I,K
I=1+1
10 CONTINUE
程序结构非常清晰,在DO-WHILE和CONTINUE之间就是循环体。或者写成:
DO WHILE(I.LE.20)
K=I*I
PRINT*,I,K
I=1+1
ENDDO
4)DO-WHILE语句应用
如图5.3、图5.4、图5.5、图5.6、图5.7所示的当型循环结构,如果用DO-WHILE语句书写,程序会更简便,清楚。
图5.3对应的FORTRAN语言程序:
PROGRAM MAIN
I=0
DO 10 WHILE(I.LT.5)
READ *,H,W0
W1=110-W0
IF(ABS(W0-W1).LE.5)THEN
PRINT *,‘标准’
ELSE IF(W0.GT.W1)THEN
PRINT *,‘过胖’
ELSE
PRINT *,‘过瘦’
ENDIF
I=I+1
10 CONTINUE
END
可以看出,流程图中对应的“转向”工作,用DO-WHILE语句书写时就不需要专门用GOTO语句了,因为DO-WHILE语句本身表示了循环的关系,即自动包含了“转向”的工作。
图5.4对应的FORTRAN语言源程序:
PROGRAM MAIN
READ *,H,W0
DO 10 WHILE(H.GT.0.AND.W0.GT.0)
W1=110-W0
IF(ABS(W0-W1).LE.5)THEN
PRINT *,‘标准’
ELSE IF(W0.GT.W1)THEN
PRINT *,‘过胖’
ELSE
PRINT *,‘过瘦’
ENDIF
READ *,H,W0
10 CONTINUE
END
图5.5对应的FORTRAN语言源程序:
PROGRAM MAIN
DATA A,B/2*1.0/
PRINT *,A,B
I=2
DO 10 WHILE(I.LT.20)
C=A+B
PRINT *,C
A=B
B=C
I=I+1
10 CONTINUE
END
图5.6对应的FORTRAN语言源程序:
PROGRAM MAIN
DATA A,B/2*1.0/
PRINT *,A,B
C=A+B
DO 10 WHILE(C.LE.1E5)
PRINT *,C
A=B
B=C
C=A+B
10 CONTINUE
END
图5.7对应的FORTRAN语言源程序:
PROGRAM MAIN
INTEGER FCH
READ *,M,N
K=MIN(M,N)
I=1
DO 10 WHILE(I.LE.K)
IF(MOD(M,I).EQ.0.AND.MOD(N,I).EQ.O)FCH=I
I=I+1
10 CONTINUE
WRITE(*,100)M,‘和’,N,‘的最大公约数是:’,FCH
100 FORMAT(1X,I4,A,I4,A,I4)
END
DO-WHILE语句可以描述所有“当型”的循环结构程序,特别对用“条件”设置循环出口的“当型”循环,可以直接套用DO-WHILE语句。对于采用“计数”方式设置循环出口的“当型”循环,计数器的“计数一次”操作则应该属于循环体的一个动作(如图5.3、图5.5、图5.7对应的程序中的I=I+1),千万不能漏掉。
其实,对于采用“计数”方式设置循环出口的“当型”循环结构,FORTRAN语言还专门提供了一个循环语句来对应描述,这就是下面介绍的DO循环结构语句。
5.3.2 DO循环结构
1)DO语句格式
DO循环由一个DO语句和循环体组成,其一般形式为:
DO s [,] v=e1,e2 [,e3]
S:表示循环终端语句标号,s是statement的缩写。
v:表示循环变量,可以是任何合法的变量名,v是variable的缩写。
e1,e2,e2:分别表示循环初值表达式、循环终值表达式、循环步长值表达式,e是expression的缩写。
方括号“[ ]”中的内容为可选项。下面形式的DO语句都是合法的形式:
DO 20,I=1,20,2
DO 10,N=1,5
DO 100 X=1.2,3.5,1.0
DO 200 T=1.5*2,100.0/2.0,2.0
DO 5 M=100,0,-2
DO循环语句也可以写成下面形式,但必须与ENDDO配对使用:
DO v=e1,e2 [,e3]
循环体
ENDDO
2)DO语句功能
DO语句描述的是用“计数”方式设置出口的“当型”循环结构,执行过程包含下面的步骤:
(1)计算e1,e2,e3各表达式的值,并将它们转换成循环控制变量的类型。
(2)将初值e1赋给循环变量,即相当于执行一个赋值语句:v=e1。
(3)计算应该执行的次数r=INT((e2-e1+e3)/e3)。
(4)检查循环次数,若r=0(或r〈0),则跳出循环体,执行循环终端语句下面的一个执行语句。如果r>0,则执行循环体。
(5)执行循环终端语句时,循环变量v增加步长值,即执行v=v+e3。
(6)循环次数r减1,即执行r=r-1,得到还要循环的次数。
(7)返回(4),重复执行(4)、(5)、(6)、(7)。
DO语句的功能也可以用图5.8和图5.9所示的流程图直观地描述出来,它们的作用是相同的。
利用DO 语句可以方便地将图5.7对应的程序改写如下: INTEGER FCH READ *,M ,N K=MIN (M ,N ) DO 10 I=1,K ,1
IF (MOD (M ,I ).EQ.0.AND.MOD (N ,I ).EQ.0)FCH=I 10 CONTINUE
WRITE (*,100)M ,‘和’,N ,‘的最大公约数是:’,FCH 100 FORMAT (1X ,I4,A ,I4,A ,I4) END 由于I=1,判断I.LE.K ,I=I+1这几个操作都自动地包含在DO 循环结构语句中了,与前面的程序相比,用DO 循环结构语句书写的程序更加精练、清晰。
应当特别指出的是:循环体不包括DO 循环语句,DO 循环语句在循环结构中只执行一次,不是r 次。 3)关于DO 语句的说明
(1)在DO 语句的一般形式中,表达式e3(即步长表达式)是可选项,如果不写e3,则表示e3的值默认为1,即循环变量的递增量(步长值)为1。例如下面两个语句的意义相同,
DO 10 I=1,50,1 DO 10 I=1,50
(2)循环变量初值、终值和步长值e1,e2,e3可以分别是常量、变量或表达式。如果是变量则它应该已经获得值,如果是表达式,则先计算出表达式的值,总之,e1,e2,e3最终应该是一个确定的数值。例如:
DO 200 T=1.5*2,100.0/2.0,2.0 相当于下面的语句:DO 200 T=3.0,50.0,2.0 (3)循环体执行的次数可以从e1,e2,e3计算出来,计算公式如下:
r=INT ((e2-e1+e3)/e3)
图5.8 DO 循环的执行过程(1)
图5.9 DO 循环的执行过程(2)
例如: DO 20 ,I=1,20,1 的循环次数是r=INT((20-1+1)/1)=20次。
DO 20 ,T=1,20,2的循环次数是r=INT((20-1+2)/2)=10次。
DO语句书写以后,机器编译时就决定了r,所以,如果想在循环体中改变循环变量的值(比如赋值)来改变循环的次数是不行的,换句话说,在循环体中改变循环变量的值将会出错。
(4)e3不能为0,因为在求循环次数时r时,e3为分母项,r会趋于无穷大。直观地说,循环变量本应该从其初值e1为起点,每次以e3的值为增量变化,直到超过e2的值为止。如果e3的值为0,则循环控制变量的值永远不会变化,也就永远不能超过终值e3,循环也就会无休止地进行,出现死循环。
(5)e1,e2,e3的值可以为正数、负数,e1,e2的值还可以为零。
(6)当e3>0时,循环变量v的值沿着正方向(递增)变化,结束循环的条件是v>e2。
当e3<0时,循环变量v的值沿着负方向(递减)变化,结束循环的条件是v<e2。
(7)如果计算出循环次数r<0时,则按r=0处理,即一次也不执行循环体。例如:DO 10 I=20,0,2
计算循环次数r=INT((0-20+2)/2)=-9,即一次也不执行循环体。直观地看,由于步长为2,结束循环的条件为I>0,事实上I=20(初值)时就大于终值0了,因此一次也不执行循环体。同理:DO 10 I=1,10,-1
也是一次也不执行循环体。因为循环控制变量I的值为1(初值)时,已经小于终值10了。
(8)循环变量v和循环初值e1,终值e2、步长e3可以是整型、实型或双精度型。如果循环变量的类型和e1,e2,e3的类型不一致,要求按赋值的规则处理,即先将e1,e2,e3的类型化成循环变量的类型,然后再进行处理。
例如:
DO 10 I=1.5,3.6,1.2
不要根据r=INT((3.6-1.5+1.2)/1.2)=2而认为循环次数为2,而应当先将实型量转化为整型量,即变成下面形式DO语句:
DO 10 I=1,3,1
再计算循环次数r=INT((3-1+1)/1=3,即DO 10 I=1.5,3.6,1.2语句确定的循环次数为3次,而不是2次。
但是如果将I换成实型变量X,则语句DO 10 X=1.5,3.6,1.2确定的循环次数就是2次,而不是3次。
特别提请注意的是:为避免错误,应尽量使循环控制变量类型与初值、终值和步长值的类型一致。由于实型数在运算和存储时有误差,当用实型变量作循环控制变量时,循环次数的理论值与实际值之间将会出现一些误差,例如有循环程序段:
DO 10 X=0.0,50.0,0.1
10 PRINT*,X
按公式计算r=INT((50.0-0.0+0.1)/0.1)=501,故应该循环501次,但实际上在许多计算机上它只能循环500次。原因是X从初值0.0开始每次增量0.1,但0.1在内存中的存储是有误差的(无法用一个有限的二进制数准确地表示十进制数0.1),因此每次增加的值不是0.1而是与0.1相接近的一个数,每次的误差积累,在执行完500次循环后,理论上X的值为50.0,由于未超过终值,所以应再执行一次循环体,共501次。但在实际上,由于上述误差累积,到执行完500次循环后,X的值可能已经超过50.0,因而停止执行循环,循环次数比理论上少一次。这种情况在程序设计中常有发生,并且比较隐蔽不易发现。如果用循环来进行计算,则少一次循环会影响计算结果。所以,在设计循环程序时应该尽量避免用实型变量作循环控制变量。用整型变量作循环变量的话,计算出来的循环次数是绝对准确的。如果在循环体中需要用到实型的e1,e2,e3时,可以想办法进行转换。对上述的循环程序可以修改为:
DO 10 I=0,500
X=I/10.0
10 PRINT*,X
这样既可以保证执行循环体501次,又能满足打印各个X值(实数)的要求。用I来控制循环次数,建立I与X的关系,注意不要写成“X=I/10”(整型量相除的结果仍然是整数!)。
从DO循环结构语句的功能来看,它描述的是用“计数”方式设置出口的“当型”循环结构,循环变量v就是“计数器”,e1是计数器的初值,e3是计数器递增的值、e2是计数的终值。今后凡是对用“计数”方式设置出口的“当型”循环结构,都可以直接套用DO语句书写。待大家慢慢熟悉了循环结构,就可以不画流程图而直接写程序了。
4)DO 循环的终端语句
DO语句构成循环结构时,循环体的最后一条语句就是终端语句,终端语句除了完成该语句本身的功能外,还向计算机提示循环体结束,使计算机完成2个工作:
(1)使循环变量增加步长值(v=v+e3);
(2)使循环次数r减1。
与DO-WHILE语句相同,原则上终端语句可以是一般的执行语句,如打印语句、赋值语句、输入语句等,但是受其特性的限制,下列语句不能作终端语句:GOTO、块IF、ELSE、ELSEIF、ENDIF、END、STOP、RETURN和所有的非执行语句。另外,如果逻辑IF语句(行IF语句)作为终端语句时,它不应该包括DO、块IF、ELSEIF、ELSE、ENDIF、END和另一个逻辑IF语句。
总之,循环终端语句的功能应该是明确的,不能是不定的。
为了使循环体的起止范围清晰,使终端语句与一般的执行语句有所区别,仍然建议使用继续语句作为循环体的终端语句。
5)DO循环的一些规定
(1)循环变量可以在循环体中被引用,但不能被赋值。
下面程序段是正确的:
DO 20 N=1,20
M=2*N
PRINT*,M
20 CONTINUE
它的功能是打印出2,4,6,8,….40。而下面的程序段是错误的:
DO 20 N=1,20
N=2*N
PRINT*,N
20 CONTINUE
由于N是循环变量,在循环体内被赋值,破坏了原有的循环过程,循环变量只能在执行终端语句时由机器自动增值而不能在循环体中通过赋值语句来改变。
(2)循环结构循环的次数是在进入循环结构之前就确定的,在执行循环体期间不可能通过改变某些变量的值而改变。
如有下面的程序段:
DATA K,J,M/1,100,2/
DO 30 I=K,J,M
K=2*K
J=J+1
M=M/2
PRINT*,K,J,M
30 CONTINUE
在进入循环结构之前,计算的r=INT((j-k+m)/m)=50,尽管在循环体中改变了K,J,M的值,循环体仍然执行50次,输出50组数据。
(3)可以用转向语句(GOTO、逻辑IF等)从循环体内转到循环体外,也可以在循环体内转向,但不
允许从循环体外转到循环体内。
例如下面程序段是合法的:
DO 10 I=1,50
……
IF(MOD(I,5).EQ.O) GOTO 5
……
10 CONTINUE
……
5 PRINT*,I*I
而下面程序段是不合法的:
IF(MOD(N,2).EQ.0) GOTO 5
DO 20 I=1,50
……
5 PRINT*,I
……
20 CONTINUE
程序段中,如果从循环结构外直接进入循环结构内,DO循环结构语句就无法确定循环的初值,终值,步长值等,无法控制循环过程。
(4)未执行完应执行的次数而跳出循环结构的称为“非正常出口”,执行完全部应执行的循环次数而跳出循环的称为“正常出口”。
6)DO循环结构语句应用
如图5.3、图5.5所示的“计数”方式设置循环出口的“当型”循环结构,如果用DO循环结构语句书写程序会更简便,清楚。
图5.3对应的FORTRAN语言程序:
PROGRAM MAIN
DO 10 I=1,5
READ *,H,W0
W1=110-W0
IF(ABS(W0-W1).LE.5)THEN
PRINT *,‘标准’
ELSE IF(W0.GT.W1)THEN
PRINT *,‘过胖’
ELSE
PRINT *,‘过瘦’
ENDIF
10 CONTINUE
END
为了和DO循环结构语句判断的条件“I.LE.5”相吻合,所以程序中循环变量的初值改为1(流程图中计数器的初值为0,判断条件为“I.LT.5”)。
可以看出,流程图中对应的“计数器设初值,判断,计数器递增”等工作已经自动地包含在DO循环结构语句中了,书写程序时不需要再写专门语句。
图5.5对应的FORTRAN语言源程序如下,为了与DO语句功能吻合,仍然将I的初值设为3。
PROGRAM MAIN
DATA A,B/2*1.0/
PRINT *,A,B
DO 10 I=3,20
C=A+B
PRINT *,C A=B B=C 10 CONTINUE END
【例5.3】 求
∑=100
1
n n ,即求S=1+2+3+4+…..+100
分析:这是一个等差数列求和的问题,如果使用等差数列求和公
式当然可以很轻松地求出结果,但是现在不用公式,而是采用循环结构程序解决。 解决“求和”问题,往往采用“累加”的方式。可以设置一个累
加器S ,初值为0,循环体的操作为:“得到一个加数N ,完成累加S=S+N ”,
循环体执行的次数为100(用“计数”方式设置循环出口),计数器的
初值为0(表示一次都没累加)。在循环体内只要变化加数N ,累加操作的对象也随之变化。 根据上述思路,画出流程图如图5.10所示。 由于计数器初值为0,“当型”循环结构的判断条件为“I <100”,
为了与DO 循环结构语句中的判断条件(I ≤100)对应,应该使计数器初值为1,这样直接就可以写出程序如下: PROGRAM MAIN S=0
DO 10 I=1,100
READ *,N S=S+N 10 CONTINUE PRINT *,‘S=’,S END
程序中试图通过每次读入一个加数N 实现“得到加数”操作,逻辑上没有问题,其实就这道题来讲,每次要得到的加数就是当前的I ,另外,如果将循环次数通过READ 语句在执行程序时确定,可以将这个程序修改成更有通用性的程序。
PROGRAM MAIN S=0
READ *,M DO 10 I=1,M S=S+I 10 CONTINUE PRINT *,‘S=’,S END
执行程序时,如果输入的M=100,则S=1+2+3+...+100,如果输入的M=500,则S=1+2+3+. (500)
【例5.4】 求
∑=100
1
!n n
图5.10 例5.3算法流程图
分析:可以设置一个累加器S=0,加数项FACT=1,可以用递推方
式依次得到加数项1!,2!,…,100!,循环体循环的次数是100次。画出流程图如图5.11所示。FORTRAN 语言源程序如下:
PROGRAM MAIN REAL S ,FACT
DATA S ,FACT/0.0,1.0/ READ *,M DO 10 N=1,M FACT=FACT*N S=S+FACT 10 CONTINUE PRINT *,‘结果为:’,S END
5.4 结构化的循环结构
按照结构化程序设计的思想,在流程图中任何一个顺序结构、分支结构、循环结构都必须只有一个入口和一个出口,如果出现了两个或者两个以上的出口,可以采用一些技巧将其改变成只有一个出口。
在循环结构中,当出现了“非正常出口”时,则出现了非结构化的问题,可以采用一些技巧将其改变成结构化的形式。
【例5.5】 判断键盘输入的数M 是否素数。
分析:根据数学知识,整数M 如果是素数,则它只能有1和M 两个约数。换一种说法,整数M 是否素数,可以考察它是否能被2,3,…..,M-1整除,如果2,3,….M-1都不能整除M ,则M 是素数,否则M 不是素数。另外根据数的特性,其实可以考察2,3,…,M/2或M 。流程图如图5.12所示。 从图上可以看出,当发现某个数I 是M 的约数时将不再继续循环(非正常出口,此时还有未考察到的数!),直接跳出循环结构。由于循环结构出现了两个出口,为了判断到底是哪个出口出来的,所以紧接着还要判断一次,如果此时“I ≤K ”成立,则说明是找到了M
的一个约数,是非正常出口出来的,M 不是素数,否则,M 是素数。对应程序如下:
PROGRAM MAIN READ *,M
K=M/2(或者K= SQRT (REAL (M )) DO 10 I=2,K
IF (MOD (M ,I ).EQ.0) GOTO 5 10 CONTINUE
5 IF (I.LE.K )THEN PRINT*,M ,‘不是素数!’ ELSE
PRINT*,M ,‘是素数!’
ENDIF
END
在程序中只要找到了一个M 的约数就直接跳出循环体,虽然节省了执行时间,但出现了“非正常出口”,为了避免“非正常出口”,可以设置一个逻辑变量(或整型变量)作为是否找到M 的约数的标志,如果找到了就赋一个标志数据图5.11 例5.4算法流程图
给该变量,但还要继续考察所有的数据,最后从正常出口结束循环。下面是修改的程序:
PROGRAM MAIN LOGICAL P P=.FALSE. READ *,M K=SQRT (M ) DO 10 I=2,K
IF (MOD (M ,I ).EQ.0) p=.true. 10 CONTINUE IF (p )THEN PRINT*,M ,‘不是素数!’ ELSE
PRINT*,M ,‘是素数!’ ENDIF END
程序阻止了循环结构的非正常出口,但当找到了M 的约数后还要继续判断剩余的数,其实这是多余的工作,影响了程序的效率,如果将循环改成用条件“.NOT.P.AND.I.LE.K ”控制出口,将解决所有的问题。 LOGICAL P P=.FALSE. READ *,M K=SQRT (M ) I=2
DO 10 WHILE (.NOT.P.AND.I.LE.K ) IF (MOD (M ,I ).EQ.0)P=.TRUE. 10 CONTINUE IF (P )THEN PRINT*,M ,‘不是素数!’ ELSE
PRINT*,M ,‘是素数!’ ENDIF END
程序改成了用“条件”控制循环过程,所以只能用DO-WHILE 语句书写。
5.5 循环嵌套
在循环体中又完整地包含另一个循环结构,称为循环的嵌套。例如: DO 10 I=1,20
……
DO 20 J=1,10
…… 20 CONTINUE
……
10 CONTINUE
按照循环结构的意义,循环嵌套时内循环应当完整地嵌套在外循环内,即内循环应该是外循环体中的一部分。循环结构不能发生交叉。
【例5.6】求3~100之间的全部素数。
分析:判断一个数是否素数的程序已经介绍过,只是在外层套上一个DO 循环语句。 Program main LOGICAL P DO 20 M=3,100
P=.FALSE.
外
循
环
内循环
K=SQRT(M)
DO 10 I=2,K
IF(MOD(M,I).EQ.0)P=.TRUE.
10 CONTINUE
IF(P)THEN
PRINT*,M,‘不是素数!’
ELSE
PRINT*,M,‘是素数!’
ENDIF
20 CONTINUE
END
【例5.7】按格式打印“九九表”。
分析:数学课用的“九九表”,形式应该如下:
1*1=1
1*2=2 2*2=4
1*3=3 2*3=6 3*3=9
…………
1*9=9 2*9=18 3*9=27……….9*9=81
每个算式中的第二个乘数用I表示,则I=1,9,表示一共有9行,处于外层循环,算式中的第二个乘数用J表示,则J=1,….I,表示每一行有1,2,….,I列,处于内层循环,再配合格式输出,写出程序如下: Program main
DO 10 I=1,9
DO 20 I=1,I
WRITE(*,100)J,I,I*J
20 CONTINUE
PRINT *,‘’
10 CONTINUE
100 FORMAT(1X,I1,‘*’,I1,‘=’,I2,‘’,$)
END
由于FORMAT语句中使用了$符号,执行WRITE语句后将不会换行,当一行上的所有算式都输出完后(即内层循环结束)需要安排一个输出空符号语句,去掉该行上最后一个算式后的不换行格式。
循环嵌套结构的意义很简单,也很容易掌握,结合打印“乘法九九表”的例子具体看一下循环嵌套的执行过程:
(1)先计算外层循环的循环次数r1,外循环变量赋初值。
(2)如果r1>0,则执行外循环体,如r1≤0,则结束外循环,转向外循环终端语句的下一条语句。
(3)在执行外循环体各语句时,遇到DO语句,则计算内循环次数r2,内循环变量取初值。
(4)如果r2>0,则执行内循环体各语句,共执行r2次,每执行一次,内循环变量加一次增量,r2的值减1,如果r2≤0,则结束内循环,从“正常出口”跳出内循环。
(5)接着执行内循环终端语句下面的外循环体内的语句,直到遇到外循环终端语句。
(6)外循环控制变量加一次增量,r1的值减1,返回(2)。
在循环嵌套结构中,外层循环执行一次,内层循环变量的值就要从头至尾变化一圈。如果外层循环的次数是r1的话,则内层循环就要做R1圈,所以内层循环体的执行次数,应该是其所有外层循环次数与内层循环次数的乘积值。
在循环嵌套结构中,内层、外层的概念是相对的,第一层循环是第二层循环的外层,第二层循环又是第三层循环的外层。只要对基本的循环结构执行过程弄清楚了,要处理多层循环结构也就比较容易了。
【例5.8】搬砖问题。
36块砖,36人搬;男搬4,女搬3,两个小孩抬一砖。要求一次搬完,问男、女、小孩各若干?
分析:设男士X人,女士Y人,小孩Z人。根据题意可以得到方程式: