第六章 低级程序设计语言与伪代码
本章重点:如何使用计算机系统
一、计算机操作
计算机是能够存储、检索和处理数据的可编程电子设备。
二、机器语言
计算机真正执行的程序设计指令是用机器语言编写的指令,这些指令固定在计算机的硬件中。
- 计算机语言(machine language):由计算机直接使用的二进制编码指令构成的语言。
每种处理器都有自己专用的机器指令集合,这些指令是处理器唯一真正能够执行的指令。
1、Pep/9:一台虚拟机
- 虚拟机(virtual computer(machine)):为了模拟真实机器的重要特征而设计的假想机器。
- Pep/9有40个机器语言指令。
(1)Pep/9的基本特性
Pep/9的内存单元由65536字节的存储空间构成。这些字节从0到65536(十进制)进行编号。Pep/9的字长是2字节或者16位。这样向算数/逻辑单元(ALU)流入的数据或从算数/逻辑单元流出的数据在长度上就是16位。
- 寄存器: 中央处理器算数/逻辑单元的一小块存储区域,它用来存储特殊的数据和中间值。
Pep/9有七个存储器,我们重点研究以下三个:
- 程序计数器(PC),其中包含下一条即将被执行的指令的地址。
- 指令寄存器(IR),其中包含正在被执行指令的一个副本。
- 累加器用来存储数据和运算的结果。
累加器是用来保存操作的数据和结果。
(2)指令格式
一条指令由两部分组成,即8位的指令说明符和(可选的)16位的操作数说明符。说明指令符(指令的第一个字节)说明了要执行什么操作(如把一个数加到一个已经存储在寄存器中的值上)和如何解释操作数的位置。操作数说明符(指令的第二和第三字节)存放的是操作数本身或者操作数的地址。有些指令没有操作数说明符。
指令说明符的格式根据表示一个具体操作所用的比特数的不同而不同。在Pep/9中,操作代码(称为操作码)的长度从4比特到8比特不等。我们在这里所用的操作码长度是4比特或5比特,4比特操作码的第5位用来指定使用哪个寄存器。
3比特的寻址模式说明符表示了怎样解析指令中的操作数部分。如果寻址模式是000,那么指令的操作数说明符中存储的就是操作数。这种寻址模式称为立即寻址(i)。如果寻址模式是001,那么操作数说明符中存储的是操作数所在的内存地址名称。这种寻址模式称为直接寻址(d)。
(3)一些示例指令
操作码 | 指令含义 |
---|---|
0000 | 停止执行 |
1100 | 将操作数载入寄存器A中 |
1101 | 将字节载入寄存器A |
1110 | 将寄存器A的内容存储到操作数中 |
1111 | 存储寄存器A中的字节 |
0110 | 从寄存器A减操作数 |
0111 | 将操作数加到寄存器A中 |
2、Pep/9的输入/输出 |
遵循的设计原则是内存映射输入/输出,这种方式将输入和输出设备与主存中特定的、固定的地址联系起来。
三、一个程序实例
对于每一个二进制指令,第一行展示了8位指令说明符,第二行展示了16位操作数说明符。
(1)Pep/9模拟器
Pep/9是虚拟机,这也意味着他在现实生活中并不是作为物理意义上的计算机存在,然而,我们可以使用pep9/模拟器来模拟程序。
(2)另一个机器语言实例
四、汇编语言
- 汇编语言(assembly language):一种低级语言,用助记码表示特定计算机的机器语言部分。
- 汇编器(assembler):把汇编语言程序翻译成机器代码的程序。
汇编语言程序——>汇编器——>机器码程序
1、Pep/9汇编语言
助记码 | 操作数、寻址模式说明符 | 指令的含义 |
---|---|---|
STOP | 停止执行 | |
LDA | 0x008B, i | 把008B载入寄存器A |
LDA | 0x008B, d | 把内存单元8B中的内容载入寄存器A |
STA | 0x008B, d | 把寄存器A中的内容存入内存单元8B |
ADDA | 0x008B, i | 把008B加到寄存器A中 |
ADDA | 0x008B, d | 把内存单元8B中的内容加到寄存器A中 |
SUBA | 0x008B, i | 从寄存器A中减去008B |
SUBA | 0x008B, d | 从寄存器A中减去内存单元8B中的内容 |
- 汇编器指令(assembler directive):翻译程序使用的指令。
伪操作 | 参数 | 含义 |
---|---|---|
.END | 表示汇编语言的终点 | |
.ASCII | "bananax00" | 表示一个ASCII字节的字符串 |
.BLOCK | 字节数 | 在内存中保留一些特定的字节 |
.WORD | 0x008B | 在内存保留一个字,并存值进去 |
- 注释:为程序读者提供的解释性文字。
2、数字数据、分支、标签
- 分支:指出执行下一条指令的指令。
- *标签**:对内存位置起的名字,可以将这个名字当作操作数。
3、汇编语言中的循环
可以重写AddNums程序来使用循环
程序首先从用户那里读取一个数字,这个数字用来说明将会有多少数被加到SUM中。同时程序还使用一个计数器,初始值为零。接着进入循环,每次循环都会读入一个数字,并且加到SUM中。每一次循环,计数器都会加1并使用BREQ指令检查它是否会达到极限值。如果达到极限值,那么循环终止,程序停止。如果没有,那么回到循环顶部继续处理下一个值。
五、表达算法
- 算法:解决方案的计划或概要。
- 伪代码:一种表达算法的语言。
1、伪代码的功能
(1)变量
出现在伪代码算法中的名字,应用的是内存中存储值的位置。这些名字要能反映出它存放的值在算法中的角色。
(2)赋值
把值放入变量
例:
1)Set sum to 0
2)sum <—— 1
访问变量中的值
例:
1)Set sum to sum + num
2)sum <—— sum + num
(3)输入/输出
我们可以使用Write语句进行输出,使用Read语句进行输入。
例:
Write“Enter the number of values to read and sum”
Read num
双引号之间的字符叫作字符串,它们告诉用户要输入什么或者要输出什么。究竟采用Display还是Print是无关紧要的,它们都等价于Write,Get和Input都与Read同义。
(4)选择*
用选择结构可以执行或跳过某项操作。另外,用选择结构还可以在亮相操作之间进行选择。选择结构使用括号中的条件决定执行哪项操作。
例:
//Read and sum three numbers
IF (sum < 0)
Print error message
ELSE
Print sum
//Stop or whatever comes next
(5)重复
使用重复结构可以重复执行指令。比如在求和问题中,计数器被初始化、检验并增加。伪代码允许我们概述算法,所以这部分就变得易于理解。和选择结构一样,在WHILE旁边的圆括号中的表达式是一个判断,如果判断成立,缩进中的语句将被执行,如果不成立,就会跳过缩进中的语句,直接执行下一个非缩进语句。
例:
Set limit to number of values to sum
WHILE(counter < limit)
Read num
Set sum to sum + num
Set counter to counter + 1
//Rest of program
WHILE和IF旁边的括号里的表达式是布尔表达式,其结果可为真或假。在IF中如果表达式为真,则执行接下来的缩进代码块,若表达式为假,则跳过缩进代码块。在WHILE中,若果表达式为真,则执行缩进代码块。如果表达式为假则跳到下一个不缩进的执行语句。将WHILE、IF和ELSE大写是因为这些语句通常直接使用在很多编程语言中,在计算领域中它们有特殊的含义。
布尔表达式(boolean expression):评价为真或假的表达式。
2、执行伪代码算法
3、写伪代码算法
1)读入一些正数数对,然后按序输出这些数对。如果数对多于一对,就必须使用循环。下面是该算法的初稿:
WHILE(not done)
Write“Enter two values separated by a blank;press return”
Read number1
Read number2
Print them in order
如何知道何时停止呢?也就是说,如何终止程序中所说的not done呢?可以要求用户告诉程序要输入多少个数对。下面是算法的第二稿:
Write “How many pairs of values are to be entered?”
Read numberOfPairs
Set pairsRead to 0
WHILE (pairsRead < numberOfPairs)
Write “Enter two values separated by a blank;press return”
Read number1
Read number2
Print them in order
如何判断数对的顺序呢?可以用条件结构比较它们的值。如果number1小于number2,则先输出number1,再输出number2.否则,就先输出number2,再输出number1.在完成算法前,必须增加number1的值。下面是算法的终稿:
Write “How many pairs of values are to be entered?”
Read numberOfPairs
Set pairsRead to 0
WHILE (pairsRead < numberOfPairs)
Write “Enter two values separated by a blank;press return”
Read number1
Read number2
IF (number1 < number2)
Print number1,“ ”,number2
ELSE
Print number2,“ ”,number1
Set numberRead to numberRead + 1
- 桌面检查: 我们坐在桌子前,用纸和笔走查整个设计。在推理设计时,采用真实的数据来跟踪发生的情况非常有用。这种方法虽然简单,但却极其有效。
4、翻译伪代码算法
六、测试
- 测试计划(test plan):说明如何测试程序的文档。
- 代码覆盖(明箱)测试法(code-coverage(clear-box)testing):通过执行代码中的所有语句测试程序或子程序的测试方法。
- 数据覆盖(暗箱)测试法(data-coverage(black-box)testing):把代码作为一个暗箱,基于所有可能的输入数据测试程序或子程序的测试方法。
- 测试计划实现(test-plan implementation):用测试计划中规定的测试用例验证程序是否输出了预期的结果。
测试计划实现要运行测试计划中列出的所有测试用例,并记录运行结果。如果结果与预期不符,则必须重新审查设计,找出并纠正其中的错误。当每种测试用例都给出了预期的结果时,这个过程将结束。
小结
- 计算机能够存储、检索和处理数据。
- 计算机的机器语言是一套机器的硬件能够识别并执行的指令。
- Pep/8汇编语言是一种使用助记忆代码而不是二进制数表示的指令。
- 伪代码是人们为了表示算法而使用的一种便捷形式的语言。
- 与算法一样,程序也需要测试。
个人问题
1)程序的开始和终止结构如何书写
2)必要的程序书写语言有哪些
3)为什么书上没有提到FOR循环
4)如何定义输入值的类型
第七章 问题求解与算法设计
本章重点:算法在解决问题、开发策略、采用和测试的技术重点作用(经典搜索和排序算法)
一、如何解决问题
Polya的“如何解决它”列表:
第一步 必须理解问题。
第二步 找到信息和解决方案之间的联系。如果找不到直接的联系,则可能需要考虑辅助问题。最终,应该得到解决方案。
第三步 执行方案。
第四步 分析得到的解决方案。
1、提出问题
2、寻找熟悉的情况
3、分治法
把大问题划分为几个能解决的小问题——>把一项任务分成若干个子任务,而子任务还可以继续划分为子任务,如此下去。可以反复利用分治法,直到每个子任务都是可以实现的为止。
4、算法
- 算法(algorithm):在有限的时间内用有限的数据解决问题或者子问题的明确指令集合。
5、计算机问题求解过程 - 分析和说明阶段
分析: 理解(定义)问题
说明: 说明程序要解决的问题 - 算法开发阶段
开发算法: 开发用于解决问题的逻辑步骤序列
测试算法:
执行列出的步骤,看它们能否真正地解决问题 - 实现阶段
编码: 用程序语言翻译算法(通用解决方案)
测试: 让计算机执行指令序列。检查结果,修改程序,知道的得到正确答案 - 维护阶段
使用: 使用程序
维护: 修改程序,使它满足改变了的要求,或者纠正其中的错误。
6、方法总结
(1)分析问题
(2)列出主要任务
(3)编写其余的模块
(4)根据需要进行重组和改写
7、测试算法
算法的测试通常都是在编码算法的各种条件下运营程序,然后分析结果以发现问题。不过,这种测试只能在程序完成或至少部分完成时进行,这种测试太迟了,所以不能依赖。越早发现和修正问题,解决问题就越容易,代价也越小。
显然,需要在开发过程的更早阶段执行测试。特别是算法必须在实现之前进行测试。
二、有简单参数的算法
1、带有选择的算法
表达在给定的室外温度情况下穿什么衣服合适的算法:
Write“Enter the temperature”
Read temperature
Determine dress
IF(temperature>90)
Write “Texas weather:wear shorts”
ELSE IF(temperature>70)
Write “Ideal weather:shorts sleeves are fine”
ELSE IF(temperature>50)
Write “A little chilly:wear a light jacket”
ELSE IF(temperature>32)
Write “Philadelphia weather:wear a heavy coat”
ELSE
Write“Stay inside”
2、带有循环的算法
(1)控制循环
这类循环有三个不同的部分,使用一个特殊的变量叫做循环控制变量。第一部分是初始化:循环控制变量初始化为某个初始值。第二部分是测试:循环控制变量是否已经达到特定值?第三部分是增量:循环控制变量以1递增。以下算法重复过程limit次:
Set count to 0
WHILE(count < limit)
...
Set count to count + 1
...
循环控制变量count在循环外已被设置为0.测试表达式count<limit,如果表达式为真则执行循环。循环中的最后一句使得控制循环变量count递增。循环会执行多少次呢?循环执行时count为0,1,2...limit-1。因此,循环执行了limit次。循环控制变量的初始值和布尔表达式中的关系运算符共同决定了循环执行的次数。
while循环被称为前测试循环,因为在循环开始前就测试了。如果最初条件为假,将不进入循环。如果省略增量语句时会发生什么?布尔表达式从不改变。如果表达式开始时为假,那就什么也不会发生,循环也就不执行;如果表达式开始时为真,表达式将从不改变,所以循环将一直执行。实际上,大多数计算机系统都有一个计时器,所以程序不会真的一直运行下去。相反,程序将停止于一条错误信息。永远不会停止的循环称为一个无线循环。
例:
Write “How many pairs of values are to be entered?”
Read numberOfPairs
Set numberRead to 0
WHILE(numberRead < numberOfpairs)
//Body of loop
...
Set numberRead to numberRead + 1
Pep/9使用分号来表明之后的部分是注释,而不是程序的一部分。在我们的伪代码中,使用两个斜杠来开始注释。
(2)事件控制循环
循环中重复的次数是由循环体自身内发生的事件控制的循环被称为事件控制循环。当使用while语句来实现事件控制循环时,这一过程仍分为三个部分:事件必须初始化,事件必须被测试,事件必须更新。
例:
Write “Enter the new base”
Read newBase
Write “Enter the number to be converted”
Read decimalNumber
Set answer to 0
Set quotient to 1
WHILE(quotient is not zero)
Set quotient to decimalNumber DIV newBase
//Rest of loop body
Write “The answer is”,answer
技术控制循环是非常直接的,它指定了循环的次数,而在事件控制循环中则不太清楚,并不显而易见。
嵌套结构(nested structure):控制结构嵌入另一个控制结构的结构,又称为嵌套逻辑(nested logic)。
(3)平方根
给出一个你想要计算平方根的数,猜测一个可能的答案,然后把这个答案乘方。如果你猜测的正确,这个平方值就等于原始值,如果不正确,则调整你的猜测,重新开始。
算法:
Read in square
Set guess to square/4
Set epsilon to 1
WHILE (epsilon > 0.001)
Calculate new guess
Set epsilon to abs(square-guess*guess)
Write out square and the guess
- 抽象步骤(abstract step):细节仍未明确的算法步骤。
- 具体步骤(concrete step):细节完全正确的算法步骤。
三、复杂变量
1、数组
数组是同构项目的有名集合,可以通过单个项目在集合中的位置访问它们。
例:
integer number[10]
//Declares numbers to hold 10 integer values
Write “Enter 10 integer numbers,one per line”
Set position to 0//Set variable position to 0
WHILE(position < 10)
Read in numbers[position]
Set position to position + 1
//Continue with processing
与数组相关的算法分为三类:搜索、排序和处理。搜索就像它的字面意思一样,搜索数组中的项,一次寻找一个特定的值。排序是按顺序将元素放入数组中。如果项是字符或字符串,将以字母顺序排序。一个已排序的数组中的项已经排好顺序。处理是一种捕捉短语,包含了对数组中的项所做的所有其他计算。
2、记录
记录是异构项目的有名集合,可以通过名字单独访问其中的项目。所谓异构,就是指集合中的元素可以不必相同。集合可以包含整数、实数、字符串或其他类型的数据。记录可以把与一个对象相关的各种项目绑定在一起。
四、搜索算法
1、顺序搜索
我们依次查找每一个元素并将其与我们需要搜索的元素进行比较。如果匹配,则找到了这个元素,如果不匹配,则继续找下一个元素。什么时候停止?当我们发现了元素或者查找所有元素后都没有找到匹配项就停止。
例:
Set position to 0
WHILE(position < 10 AND found is FALSE)
IF(number[position]equals searchltem)
Set found to TRUE
ELSE
Set position to position + 1
布尔操作符包括特殊操作符AND、OR和NOT。AND操作符只有在表达式都为真时返回值才是TRUE,否则返回FALSE。OR操作符只有在表达式都为假时返回FALSE,其余返回TRUE。NOT操作符改变表达式的值。
2、有序数组中的顺序搜索
3、二分检索
二分检索算法假设要检索的数组是有序的,其中每次比较操作可以找到要找的项目或把数组减少一半。二分检索不是从数组开头开始顺序前移,而是从数组中间开始。如果要检索的项目小于数组的中间项,那么可以知道这个项目一定不会出现在数组的后半部分,因此只需要搜索数组的前半部分即可。
然后再检测数组的“中间”项(即整个数组1/4处的项目)。如果要检索的项目大于中间项,搜索将在数组的后半部分继续。如果中间项等于正在搜索的项目,搜索将终止。每次比较操作都会将搜索范围缩小一半。当要找的项目找到了,或可能出现在这个项目的数组为空的情况,整个过程将终止。
例:
Boolean Binary Search
Set first to 0
Set last to length-1
Set found to FALSE
WHILE(first<=last AND NOT found)
Set middle to (first + last)/2
IF(item equals data[middle])
Set found to TRUE
ELSE
IF(item < data[middle])
Set last to middle - 1
ELSE
Set first to middle + 1
Return found
五、排序
1、选择排序
对具有5个元素的数组排序,可以把这个数组看作由两个部分构成,即无序部分和有序部分。每当把一个项目放到正确的位置,无序部分就缩小了,而有序部分则扩展了。排序开始时,所有的项目都位于无序部分;排序结束时,所有项目都位于有序部分。
2、冒泡排序
冒泡排序也是一种选择排序法,只是在查找最小值时采用了不同的方法。它从数组的最后一个元素开始,比较相邻的元素对,如果下面的元素 小于上面的元素,就交换这两个元素的位置。通过这种方法,最小的元素就会“冒”到数组的顶部。每次都会把未排序的最小元素放到它的正确位置,不过这同时会改变数组中其他元素的位置。
3、插入排序
如果数组中只有一个元素,那么它就是有序的。如果有两个元素,需要的话可以进行比较和交换。现在,这两个元素是有序的,根据这两个元素把第三个元素放在合适的位置。现在相当于彼此前三个元素就是有序的。将元素加入有序部分类似于冒泡排序中冒泡的过程。如果找到一个位置,要插入的元素比数组中这个位置的元素小,那么就将新元素插入这个位置。
六、递归算法
- 递归(recursion):算法调用它本身的能力。
递归算法的两种情况: - 基本情况
基本情况是答案已知的情况。 - 一般情况
一般情况则是调用自身来解决问题的更小版本的解决方案。
1、子程序语句
子程序有两种形式,一种是只执行特定任务的命名代码,一种是不仅执行任务,还返回给调用单元一个值(值返回子程序)。第一种形式的子程序在调用单元中用作语句,第二种则用作表达式,返回的值被用来评估表达式。
2、递归阶乘
数的阶乘的定义是这个数与0和它自身之间的所有数的乘积,即:
N!=N(N - 1)!
0的阶乘是1.尺寸系数就是要计算阶乘的数。
基本情况是:
Factorial(0)=1
一般情况是:
Factorial(N)=NFactorial(N - 1)
3、递归二分检索
4、快速排序
七、几个重要思想
1、信息隐蔽
信息隐蔽(information hiding):隐蔽模块的细节以控制对这些细节的访问的做法。
2、抽象
- 抽象(abstraction):复杂系统的一种模型,只包括对观察者来说的必须章节。
- 数据抽象(data abstraction):把数据的逻辑视图和它的实现分离开。
- 过程抽象(procedural abstraction):把动作的逻辑视图和它的现实分离开。
- 控制抽象(control abstraction):把控制结构的逻辑视图和它的现实分离开。
- 控制结构(control structure):用于改变正常的顺序控制流的语句。
3、事物命名
4、测试