第六章 低级程序设计语言与伪代码
6.1 计算机操作
计算机是能够存储、检索和处理数据的可编程电子设备。存储、检索和处理是计算机能够对数据执行的动作。
6.2 机器语言
计算机真正执行的程序设计指令是用机器语言编写的指令。
- 机器语言:由计算机直接使用的二进制编码指令构成的语言。
Pep/8:一台虚拟机
-虚拟机:为了模拟真实机器的重要特征而设计的假想机器。
Pep/8反应的重要特性
Pep/8的内存单元由65536字节的存储空间构成。Pep/8的字长是2字节,或者16比特。
Pep/8有七个寄存器,重点研究其中三个:
- 程序计数器(PC),其中包含下一条即将被执行的指令的地址。
- 指令寄存器(IR),其中包含正在被执行的指令的一个副本。
- 累加器(是一个寄存器)。
指令格式
一条指令由两个部分组成,即8位的指令说明符和(可选的)16位的操作数说明符。指令说明符(指令的第一个字节)说明了要执行什么操作和如何解释操作数的位置。操作数说明符(指令的第二个和第三个字节)存放的是操作数本身或者操作数的地址。有些指令没有操作数说明符。
操作代码(称为操作码)的长度从4比特到8比特不等。4比特操作码的第5位用来指定使用哪个寄存器。
寻址模式说明符表示了怎样解析指令中的操作数部分。如果寻址模式是000,那么指令的操作数说明符中存储的是操作数所在的内存地址名称。这种寻址模式称为直接寻址。
没有操作数的指令称为一元指令,这些指令没有操作数说明符。也就是说,一元指令的长度是1字节,而不是3字节。
一些示例指令
例子:
指令说明符:1100000
寻址模式是立即寻址,即要被载入寄存器A中的值在操作数说明符中。
指令说明符:11000001
寻址模式是直接寻址,意味着操作数本身并不在操作数说明符中,而是操作数说明符存储了操作数驻留在内存中的地址。
在存储操作码中使用立即寻址模式是非法的,也就是说,我们不能尝试将寄存器的内容存储到操作数说明符中。
01001把字符输入操作数:这一指令只能使用直接寻址,所以输入的字符被存储在操作数说明符中显示的地址中。
6.3 一个程序实例
在屏幕上显示"Hello":
在屏幕上显示字符的指令是0101,即“从操作符输出字符”的操作。
6.3.1 手工模拟
读取-执行周期的四个步骤:
- 从程序计数器指定的位置)读取下一条指令
- 译解指令(并且更新程序计数器)
- 如果需要,获取数据(操作数)
- 执行指令
注意,一旦指令已被访问,程序计数器就会递增。
6.3.2 Pep/8模拟程序
虽然Pep/8是一台(假想的)虚拟机,但是它有对应的模拟程序。要运行一个程序,需要逐字节地输入十六进制的代码,每个字节之间用空格隔开,以zz结束程序。
- 装入程序:软件用于读取机器语言并把它载入内存的部分
6.4 汇编语言
- 汇编语言:一种低级语言,用助记码表示特定计算机的机器语言指令。
- 汇编器:把汇编语言程序翻译成机器代码的程序。
汇编语言给每条机器语言指令分配了一个助记指令码,程序员可以用这些指令码代替二进制数字。
6.4.1 Pep/8汇编语言
6.4.2 汇编器指令
大多数程序设计语言都有两种类型的指令,即要翻译的指令和翻译程序使用的指令。
- 汇编器指令:翻译程序使用的指令,也叫伪操作。
6.4.3 Hello程序的汇编语言版本
编译器会忽略从分号开始到一行结束的所有字符,这就是一个注释。
- 注释:为程序读者提供的解释性文字。
汇编器的输入是一个用汇编语言编写的程序,输出是用机器代码编写的程序。
6.4.6 具有循环的程序
如何知道已经读取了多少个值呢?可以在每次重复循环时建立一个散列标记,这个散列标记就是内存中一个为0的存储单元,我们称之为计数器。每次循环重复时,我们在内存中该存储单元中加1,即计数器加1.当计数器等于我们想输入的数量,就完成读取和计数。
6.5 表达算法
- 算法:解决方案的计划或概要,或解决问题的逻辑步骤顺序。
- 伪代码:一种表达算法的语言。
6.5.1 伪代码的功能
变量
出现在伪代码算法中的名字,引用的是内存中的存储值的位置。这些名字要能反映出它存放的值在算法中的角色。
赋值
Set sum to 0
这个语句把一个值存放到变量sum中。另一种表示同一概念的方法是使用反向箭头(←):sum<——1
存放在sum中的值加上存放在mum的值,结果存放在sum中。
输入/输出
我们可以使用Write语句进行输出,使用Read语句进行输入。双引号之间的字符叫作字符串,告诉用户要输入什么或者要输出什么。
选择
用选择结构可以选择执行或跳过某项操作。此外,用选择结构还可以在两项操作之间进行选择。选择结构使用括号中的条件决定执行哪项操作。符号“//”用于加注释,它并不是算法的一部分。
重复
使用重复结构可以重复执行指令。和选择结构一样,在WHILE旁边的圆括号中的表达式是一个判断,如果判断成立,缩进中的语句将被执行,如果不成立,就会跳过缩进中的语句,直接执行下一个非缩进语句。
WHILE和IF旁边的括号里的表达式是布尔表达式,在IF中,如果表达式为真,则执行接下来的缩进代码块,若表达式为假,则跳过缩进代码块。在WHILE中,如果表达式为真,则执行缩进代码块。如果表达式为假则跳到下一个执行语句。
- 布尔表达式:评价为真为假的表达式。
6.5.3 写伪代码算法
- 推迟细节:首先给任务一个名称,然后再补充细节来完成这个任务。
- 分布解决:首先编写算法代码,然后补充细节完成这个任务的策略。
- 桌面检查:在纸上走查整个设计。
6.6 测试
- 测试计划:说明如何测试程序的文档。
- 代码覆盖(明箱)测试法:通过执行代码中的所有语句测试程序或子程序的测试方法,会确保程序中的每条语句都能被执行到。
- 测试计划实现:用测试计划中规定的测试用例验证程序是否输出了预期结果。
第七章 问题求解与算法设计
7.1.1 提出问题
如何解决它:
第一步必须理解问题。
第二步找到信息和解决方案之间的联系。如果找不到直接的联系,则可能需要考虑辅助问题。最终,应该得到解决方案。
第三步执行方案。
第四步分析得到的解决方案。
7.1.3 分治法
通常,我们会把一个大问题划分为几个能解决的小单元。这项原则尤其适用于计算领域:把大的问题分割成能够单独解决的小问题。
7.1.4 算法
- 算法:在有限的时间内用有限的数据解决问题或子问题的明确指令合集。
7.1.5 计算机问题求解过程
计算机问题求解过程包括四个阶段,即分析和说明阶段、算法开发阶段、实现阶段和维护阶段。
7.1.6 方法总结
- 分析问题
首先要理解问题,列出必须处理的信息。明确采用什么样的解决方式。思考如何手动地解决这个问题。 - 列出主要任务
用自然语言或伪代码在主模块中重述问题。用任务名把问题分解成功能区块。
在主模块中所要做的只是给下一层中每个解决任务的模块一个名字,要采用含义明确的标识符。 - 编写其余的模块
每一层中的模块可以指定多个下层模块。上层模块必须完整。不断细化每个模块,直到模块中的每条语句都是具体的步骤为止。 - 根据需要进行重组和改写
要维持透明性,简单直接地表达你的想法。自顶向下设计,将任务分层从而解决。
7.1.7 测试算法
数学问题求解的目标是生成问题的特定答案,因此,检查结果等价于测试推出答案的过程。
7.2 有简单参数的算法
简单(原子)变量是那些不能被分开的变量,例如,数字就是简单变量。
7.2.1 带有选择的算法
我们定义热天为90以上,好天气70以上,有点寒冰为50以上,寒冷为32以上。
IF(temperature>90)
Write"Texas weather:wear shots"
ELSE IF(temperature>70)
Write"Ideal weather:short sleeves are fine"
ELSE IF(temperature>32)
Write"Philadelphia weather:wear a heavy coat"
ELSE
Write"Stay insides"
到达第二个if语句的唯一方式是第一个if表达式是不真实的。
7.2.2 带有循环的算法
有两种基本的循环,分别为计数控制和事件控制。
计数控制循环
计数控制循环可以指定过程重复的次数,这个循环的机制是简单记录过程重复的次数且在重复再次开始前检测循环是否已经结束。
这类循环有三个不同的部分,使用一个特殊的变量叫作循环控制变量。第一部分是初始化:循环控制变量初始化为某个初始值。第二部分是测试:循环控制变量是否已经达到特定值?第三部分是增量:循环控制变量以1递增。
while循环被称为前测试循环,因为在循环开始前就开始测试了。
永远不会终止的循环称为一个无限循环。
计数循环如:
Write"How many pairs of values are to be entered?"
Read numberRead to 0
WHILE(numberRead<numberOfPairs)
//Body of loop
...
Set numberRead to numberRead+1
Pep/8使用分号来表示之后的部分是注释,在伪代码中,使用两个斜杠来开始注释。
事件控制循环
循环中重复的次数是由循环体自身内发生的事件控制的循环被称为事件控制循环。这一过程仍分为三个部分:事件必须初始化,事件必须被测试,事件必须更新。
事件控制循环如:
Write"Enter the new base"
Read newBase
Write"Enter the number to be converted"
Read decimaiNumber
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
计数控制循环是非常直接简单的,它指定了循环的次数,而在事件控制循环中则不太清楚,并不显而易见。
在控制结构中执行或跳过的语句中可以是简单的语句或者是复杂的语句,因此跳过或重复的语句中可以包含一个控制结构。选择语句可以嵌套在循环结构中,循环结构可以嵌套在选择语句中。控制结构嵌入另一个控制结构被称为嵌套结构。(又称嵌套逻辑)
平方根
给出一个想要计算平方根的数,猜测答案,如果猜测的平方和原始值的差距在±0.001之间,则我们把这个差距叫作epsilon差异。如何测量差距的正负?运用绝对值计算,这个表达式为abs(epsilon),即绝对值。
- 抽象步骤:细节仍未明确的算法步骤。
- 具体步骤:细节完全明确的算法步骤。
7.3 复杂变量
- 字符串:引用的字母。
7.3.1 数组
数组是同构项目的有名集合,可以通过单个项目在集合中的位置访问它们。项目在集合中的位置叫作索引。
与数组有关的算法分为三类:搜索、排序和处理。搜索就像它的字面意思一样,搜索数组中的项,一次寻找一个特定的值。排序是按顺序将元素放入数组中。处理是一种捕捉短语,包含了对数组中的项所做的所有其他计算。
7.3.2 记录
记录是异构项目的有名集合,可以通过名字单独访问其中的项目。所谓异构,就是指集合中的元素可以不必相同。集合可以包含整数、实数、字符串或其他类型的数据。记录可以把与一个对象相关的各种项目绑定在一起。
7.4 搜索算法
7.4.1 顺序搜索
第一个搜索算法遵循了搜索定义。我们依次查找每一个元素并将其与我们需要搜索的元素进行比较。发现了元素或者是查找所有的元素后都没有找到匹配项就停止。
AND是一种布尔操作符,布尔操作符包括特殊操作符AND、OR和NOT。AND操作符只有在表达式都为真时返回值才是TRUE,否则返回FALSE。OR操作符只有在表达式都为假时返回FALSE,其余返回TRUE。NOT操作符改变表达式的值。
7.4.2 有序数组中的顺序搜索
我们在算法中是用变量index而不是position。程序员在处理数组时经常使用数学标识符index而不是直观的标识符position或place。
7.4.3 二分检索
数组的顺序搜索在数组开头开始,直到找到匹配项或者整个数组中都没有匹配项。
二分检索查找数组中的项目的方法则完全不同,它采用的是分治法。
二分检索算法假设要检索的数组是有序的,其中每次比较操作可以找到的项目或把数组减少一半。二分检索不是从数组开头开始顺序前移,而是从数组中间开始。然后再检测数组的“中间”项(即整个数组1/4处的项目)。
为什么我们不总是用二分检索呢?因为为了计算中间项的索引,每个比较操作都需要更多的计算。此外,数组必须是有序的。如果数组是有序的已经排好,且其中的项目不超过20个,那么使用二分检索算法比较好。
7.5 排序
所谓排序,就是按顺序排放东西。在计算领域,把无序数组转化成有序数组是很常见的有用操作。
7.5.1 选择排序
选择排序算法也许是最简单的,但却有缺陷,它需要两个完整列表(数组)的空间。即使不考虑内存空间,复制操作显然也很费空间。不过对这种手动方法稍作修改,就可以免除所需的复制空间。当把最小项移动到新的数组中时,就空出一个位置,因此不必把最小值写入第二个列表,而是把它与它应该所在的位置处的当前值交换即可。
这个算法只有三个抽象步骤,即确定数组是否已经排好序、找到最小元素的索引和互换两者位置。
7.5.2 冒泡排序
冒泡排序也是一种选择排序法,只是在查找最小值时采用了不同方法。它从数组的最后一个元素开始,比较相邻的元素对,如果下面的元素小于上面的元素,就交换这两个元素的位置。
冒泡排序是非常慢的排序算法。比较排序算法的方法通常是看它们对数组排序的迭代次数,而冒泡排序要对数组中除最后一个元素之外的所有元素进行一次迭代。
7.5.3 插入排序
现在,这两个元素是有序的,根据这两个元素把第三个元素放在合适的位置。现在,相对于彼此前三个元素就是有序的。将元素加入有序部分类似于冒泡排序中的冒泡的过程。
选择排序的每次迭代后,一个元素被放置到它的永久位置。而插入排序的每次迭代后,一个元素将被放在相对于其他元素来说适当的位置上。
7.6 递归算法
递归就是算法调用它本身的能力,是另一种重复(循环)的循环结构。这种算法使用一个选择语句来确定是否重复算法来调用一遍或停止这一过程,而不是使用一个循环语句执行一个算法。
每个递归算法至少有两种情况:基本情况和一般情况。 基本情况是答案已知的情况;一般情况则是调用自身来解决问题的更小版本的解决方案。因为一般情况下解决的是原始问题越来越小的版本,所以程序最终达到基本情况,即答案是已知的,所以递归停止。递归解决方案的第一步都是确定尺寸系数。尺寸系数可能就是数值本身。
7.6.1 子程序语句
我们可以给一段代码一个名称,然后程序另一部分的一个语句使用这个名称。遇到这个名称时,这个进程的其他部分将会终止,等待这个命名代码被执行。当命名代码执行完毕将会继续处理下面的语句。命名代码出现的地方被称为调用单元。
子程序有两种形式,一种只执行特定任务的命名代码,一种是不仅执行任务,还返回给调用单元一个值。第一种形式的子程序在调用单元中用作语句,第二种则用表达式,返回的值被用来评估表达式。
7.6.2 递归阶乘
数的阶乘的定义是这个数于0和它自身之间的所有数的乘积,即 N!=N*(N-1)!
7.6.3 递归二分检索
递归算法必须从非递归算法中调用。
7.6.4 快速排序
快速排序算法的基本思想是对两个小列表排序比对一个大列表排序更快更容易。其基本策略是“分治法”。这种策略的基础是递归。
7.7 几个重要思想
7.7.1 信息隐蔽
对于设计的每个特定分层,设计者只考虑与之相关的细节。这种做法叫作信息隐蔽,即在进行高层设计时不能见到低层的细节。
7.7.2 抽象
抽象和信息隐蔽就像一个硬币的两面。信息隐蔽是隐藏细节的做法,抽象则是隐藏细节后的结果。
- 抽象:复杂系统的一种模型,只包括对观察者来说必需的细节。
- 数据抽象:把数据的逻辑视图和它的实现分离开。
- 过程抽象:把动作的逻辑视图和它的实现分离开。
- 控制抽象:把控制结构的逻辑视图和它的实现分离开。
- 控制结构:用于改变正常的顺序控制流的语句,
7.7.3 测试
测试在编程的每个阶段都十分重要,有两种基本的测试分类:白盒测试,基于代码本身;黑盒测试,基于测试所有可能的输入值。