第六章 低级程序设计语言与伪代码
6.1计算机操作
1.计算机:能够存储、检索和处理数据的可编程电子设备。
- 存储、检索和处理是计算机能够对数据执行的操作。也就是说,控制单元执行的指令能够把数据存储到机器的内存中,在机器内存中检索数据,在算术逻辑单元中以某种方式处理数据。
6.2机器语言
1.机器语言(machine language):由计算机直接使用的二进制编码指令构成的语言。
每种处理器都有自己专用的机器指令集合。这些指令是机器唯一真正能够执行的指令。
2.Pep/8:一台虚拟机
- 虚拟机(virtual computer (machine)):为了模拟真实机器的重要特征而设计的假象机器。
Pep/8有39个机器语言指令。
- Pep/8反映的重要特性
- Pep/8的字长是2字节,或者16比特。这样向算术/逻辑单元(ALU)流入的数据或从算术/逻辑单元流出的数据在长度上就是16比特。
- Pep/8有七个寄存器,我们重点研究:程序计数器(PC)、指令寄存器(IR)、累加器.
- 累加器是用来保存操作的数据和结果。
- 可用的比特数决定了我们可以使用的内存大小。
- 指令格式
- 在Pep/8中,一条指令由两部分组成,即8位的指令说明符和(可选的)16位的操作数说明符。
指令说明符(指令的第一个字节):说明了要执行什么操作和如何解释操作数的位置。
操作数说明符(指令的第二和第三个字节):存放操作数本身或者操作数的地址。
有些指令没有操作数说明符,即一元指令(没有操作数的指令)。
指令说明符的格式根据表示一个具体操作所用的比特数的不同而不同。在Pep/8中,操作代码(称为操作码)的长度从4比特到8比特不等。我们在这里所用的操作码长度是4比特或5比特,4比特操作码的第5位用来指定使用哪个寄存器。
3比特的寻址模式说明符表示了怎样解析指令中的操作数部分。如果寻址模式是000,那么指令的操作数说明符中存储的就是操作数。这种寻址模式称为立即寻址(i)。如果寻址模式是001,那么操作数说明符中存储的是操作数所在的内存地址名称。这种寻址模式称为直接寻址(d)。
- 一些示例指令
操作码 | 指令含义 |
---|---|
0000 | 停止执行 |
1100 | 将操作数载入寄存器A中 |
1110 | 将寄存器A的内容存储到操作数中 |
0111 | 将操作数加到寄存器A中 |
1000 | 在寄存器A的值中减去操作数的值 |
01001 | 把字符输入操作数 |
01010 | 从操作数输出字符 |
6.3 一个程序实例
6.3.1 手工模拟
读取-执行周期的四个步骤:
1.从程序计数器指定的位置读取下一条指令
2.译解指令(并且更新程序计数器)
3.如果需要,获取数据(操作数)
4.执行指令
6.3.2 Pep/8模拟程序
- 装入程序(loader):软件用于读取机器语言并把它载入内存的部分。
6.4 汇编语言
1.汇编语言(assembly language):一种低级语言,用助记码表示特定计算机的机器语言部分。
2.汇编器(assembler):把汇编语言程序翻译成机器代码的程序。
6.4.1 Pep/8汇编语言
助记忆码 | 操作数、寻址模式说明符 | 指令的含义 |
---|---|---|
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中的内容 |
BR | 分支到操作数说明符中指定的位置 | |
CHARI | 0x008B, d | 读取一个字符,把它存入内存单元8B中 |
CHARO | 0x008B, i | 输出字符B |
CHARO | 0x008B, d | 输出存储在内存单元8B中的字符 |
DECI | 0x008B, d | 读取一个十进制数,把它存储在内存单元8B中 |
DECO | 0x008B, i | 输出十进制数139(即十六进制的8B) |
DECO | 0x008B, d | 输出存储在内存单元8B中的十进制数 |
6.4.2 汇编器指令
- 汇编器指令(assembler directive):翻译程序使用的指令,也叫伪操作。
汇编器指令(assembler directive):翻译程序使用的指令。
伪操作 | 参数 | 含义 |
---|---|---|
.ASCII | "Strx00" | 表示一个ASCII字节的字符串 |
.BLOCK | 字节数 | 创建一个字节块 |
.WORD | 值 | 创建一个字,并存值进去 |
.END | 表示汇编语言的终点 |
6.4.3 Hello程序的汇编语言版本
注释(comment):为程序读者提供的解释性文字。
- 汇编语言的重要性:
它通过将指令抽象为单词,从而删除了机器语言编程的很多细节。虽然在执行一个程序时增加了一个步骤(汇编语言翻译成机器代码),但是这个额外的步骤是非常值得的,这大大简化了程序员的工作。
6.4.4 一个新程序
6.4.5 具有分支的程序
6.4.6 具有循环的程序
6.5 表达算法
1.算法(algorithm):解决方案的计划概要,或解决问题的逻辑步骤顺序。
2.伪代码(pseudocode):一种表达算法的语言。
6.5.1 伪代码的功能:
变量
出现在伪代码算法中的名字,应用的是内存中存储值的位置。这些名字要能反映出它存放的值在算法中的角色。
赋值
把值放入变量
例:
1)Set sum to 0
2)sum <—— 1
访问变量中的值
例:
1)Set sum to sum + num
2)sum <—— sum + num
输入/输出
我们可以使用Write语句进行输出,使用Read语句进行输入。
例:
Write“Enter the number of values to read and sum”
Read num
双引号之间的字符叫作字符串,它们告诉用户要输入什么或者要输出什么。究竟采用Display还是Print是无关紧要的,它们都等价于Write,Get和Input都与Read同义。
选择
用选择结构可以执行或跳过某项操作。另外,用选择结构还可以在亮相操作之间进行选择。选择结构使用括号中的条件决定执行哪项操作。
例:
//Read and sum three numbers
IF (sum < 0)
Print error message
ELSE
Print sum
//Stop or whatever comes next
重复
使用重复结构可以重复执行指令。比如在求和问题中,计数器被初始化、检验并增加。伪代码允许我们概述算法,所以这部分就变得易于理解。和选择结构一样,在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):评价为真或假的表达式。
6.5.2 执行伪代码算法
6.5.3 写伪代码算法
1.读入一些正数数对,然后按序输出这些数对。如果数对多于一对,就必须使用循环。下面是该算法的初稿:
WHILE(not done)
Write“Enter two values separated by a blank;press return”
Read number1
Read number2
Print them in order
2.如何知道何时停止呢?也就是说,如何终止程序中所说的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
3.如何判断数对的顺序呢?可以用条件结构比较它们的值。如果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
- 桌面检查(desk checking):
我们坐在桌子前,用纸和笔走查整个设计。在推理设计时,采用真实的数据来跟踪发生的情况非常有用。这种方法虽然简单,但却极其有效。
6.5.4 翻译伪代码算法
6.6 测试
1.测试计划(test plan):说明如何测试程序的文档。
- 代码覆盖(明箱)测试法(code-coverage(clear-box)testing):通过执行代码中的所有语句测试程序或子程序的测试方法。
- 数据覆盖(暗箱)测试法(data-coverage(black-box)testing):把代码作为一个暗箱,基于所有可能的输入数据测试程序或子程序的测试方法。
- 测试计划实现(test-plan implementation):用测试计划中规定的测试用例验证程序是否输出了预期的结果。
测试计划实现要运行测试计划中列出的所有测试用例,并记录运行结果。如果结果与预期不符,则必须重新审查设计,找出并纠正其中的错误。当每种测试用例都给出了预期的结果时,这个过程将结束。
小结
- 计算机能够存储、检索和处理数据。
- 计算机的机器语言是一套机器的硬件能够识别并执行的指令。
- Pep/8汇编语言是一种使用助记忆代码而不是二进制数表示的指令。
- 伪代码是人们为了表示算法而使用的一种便捷形式的语言。
- 与算法一样,程序也需要测试。
第七章 问题求解与算法设计
7.1如何解决问题
7.1.1 提出问题
典型问题:
- 对这个问题我了解多少?
- 解决方案是什么样的?
- 存在什么特例?
- 我如何知道已经找到解决方案了 ?
Polya的“如何解决它”列表:
1.必须理解问题。
2.找到信息和解决方案之间的联系。如果找不到直接的联系,则可能需要考虑辅助问题。最终,应该得到解决方案。
3.执行方案。
4.分析得到的解决方案。
7.1.2 寻找熟悉的情况
7.1.3 分治法
通常,我们会把一个大问题划分为几个能解决的小单元。通过反复利用分治法,直到每个子任务都是可以实现的为止。
7.1.4 算法
算法(algorithm):在有限的时间内用有限的数据解决问题或者子问题的明确指令集合。
7.1.5 计算机问题求解过程
- 分析和说明阶段
分析:
理解(定义)问题
说明:
说明程序要解决的问题 - 算法开发阶段
开发算法:
开发用于解决问题的逻辑步骤序列
测试算法:
执行列出的步骤,看它们能否真正地解决问题 - 实现阶段
编码:
用程序语言翻译算法(通用解决方案)
测试:
让计算机执行指令序列。检查结果,修改程序,知道的得到正确答案 - 维护阶段
使用:
使用程序
维护:
修改程序,使它满足改变了的要求,或者纠正其中的错误
7.1.6 方法总结
1.分析问题
2.列出主要任务
3.编写其余的模块
4.根据需要进行重组和改写
7.1.7 测试算法
算法的测试通常都是在编码算法的各种条件下运营程序,然后分析结果以发现问题。不过,这种测试只能在程序完成或至少部分完成时进行,这种测试太迟了,所以不能依赖。越早发现和修正问题,解决问题就越容易,代价也越小。
显然,需要在开发过程的更早阶段执行测试。特别是算法必须在实现之前进行测试。
7.2 有简单参数的算法
- 简单(原子)变量是那些不能被分开的变量,是存储在一个地方的一个值。
7.2.1 带有选择的算法
- 顶级(主要)模块只是表达任务
7.2.2 带有循环的算法
有两种基本的循环,分别为计数控制和事件控制
1.计数控制循环
- 计数控制循环可以指定过程重复的次数,这种循环的机制是简单记录过程重复的次数并且在重复再次开始前检测循环是否已经结束。
- while循环被称为前测试循环,因为在循环开始前就测试了。
- 无限循环:永远不会终止的循环
2.事件控制循环
- 事件控制循环:循环中重复的次数是由循环体自身内发生的事件控制的循环。
- 嵌套结构(nested structure):控制结构嵌入另一个控制结构的结构,又称为嵌套逻辑(nested logic)。
3.平方根
- 如果猜测的平方与原始值的差距在±0.001之间,则这个值就是足够好的。我们把这个差距叫作epsilon差异
- 抽象步骤(abstract step):细节仍未明确的算法步骤。
- 具体步骤(concrete step):细节完全明确的算法步骤。
7.3 复杂变量
7.3.1 数组
- 数组是同构项目的有名集合,可以通过单个项目在集合中的位置访问它们。
- 索引:项目在集合和中的位置。
- 与数组有关的算法分为三类:搜索、排序和处理。
- 搜索:搜索数组中的项,一次寻找一个特定的值。
- 排序:按顺序将元素放入数组中。
- 处理:一种捕捉短语,,包含了对数组中的项所做的所有其他计算。
7.3.2 记录
记录是异构项目的有名集合,可以通过名字单独访问其中的项目。
- 异构:就是指集合中的元素可以不必相同。集合可以包含整数、实数、字符串或其他类型的数据。记录可以把与一个对象相关的各种项目绑定在一起。
7.4 搜索算法
7.4.1 顺序搜索
我们依次查找每一个元素并将其与我们需要搜索的元素进行比较。如果匹配,则找到了这个元素,如果不匹配,则继续找下一个元素。什么时候停止?当我们发现了元素或者查找所有元素后都没有找到匹配项就停止。
7.4.2 有序数组中的顺序搜索
7.4.3 二分检索
二分检索算法假设要检索的数组是有序的,其中每次比较操作可以找到要找的项目或把数组减少一半。二分检索不是从数组开头开始顺序前移,而是从数组中间开始。如果要检索的项目小于数组的中间项,那么可以知道这个项目一定不会出现在数组的后半部分,因此只需要搜索数组的前半部分即可。 然后再检测数组的“中间”项(即整个数组1/4处的项目)。如果要检索的项目大于中间项,搜索将在数组的后半部分继续。如果中间项等于正在搜索的项目,搜索将终止。每次比较操作都会将搜索范围缩小一半。当要找的项目找到了,或可能出现在这个项目的数组为空的情况,整个过程将终止。
- 二分检索(binary search):在有序列表中查找项目的操作,通过比较操作排除大部分检索范围。
7.5 排序
7.5.1 选择排序
对具有5个元素的数组排序,可以把这个数组看作由两个部分构成,即无序部分和有序部分。每当把一个项目放到正确的位置,无序部分就缩小了,而有序部分则扩展了。排序开始时,所有的项目都位于无序部分;排序结束时,所有项目都位于有序部分。
7.5.2冒泡排序
冒泡排序也是一种选择排序法,只是在查找最小值时采用了不同的方法。它从数组的最后一个元素开始,比较相邻的元素对,如果下面的元素 小于上面的元素,就交换这两个元素的位置。通过这种方法,最小的元素就会“冒”到数组的顶部。每次都会把未排序的最小元素放到它的正确位置,不过这同时会改变数组中其他元素的位置。
7.5.3插入排序
如果数组中只有一个元素,那么它就是有序的。如果有两个元素,需要的话可以进行比较和交换。现在,这两个元素是有序的,根据这两个元素把第三个元素放在合适的位置。现在相当于彼此前三个元素就是有序的。将元素加入有序部分类似于冒泡排序中冒泡的过程。如果找到一个位置,要插入的元素比数组中这个位置的元素小,那么就将新元素插入这个位置。
7.6 递归算法
递归(recursion):算法调用它本身的能力。
递归算法的两种情况:
- 基本情况
基本情况是答案已知的情况。 - 一般情况
一般情况则是调用自身来解决问题的更小版本的解决方案。
7.6.1 子程序语句
子程序有两种形式,一种是只执行特定任务的命名代码,一种是不仅执行任务,还返回给调用单元一个值(值返回子程序)。第一种形式的子程序在调用单元中用作语句,第二种则用作表达式,返回的值被用来评估表达式。
7.6.2递归阶乘
数的阶乘的定义是这个数与0和它自身之间的所有数的乘积,即:
N!=N(N - 1)!
0的阶乘是1.尺寸系数就是要计算阶乘的数。
基本情况是:
Factorial(0)=1
一般情况是:
Factorial(N)=NFactorial(N - 1)
7.6.3递归二分检索
7.6.4快速排序
7.7几个重要思想
7.7.1信息隐蔽
信息隐蔽(information hiding):隐蔽模块的细节以控制对这些细节的访问的做法。
7.7.2 抽象
抽象(abstraction):复杂系统的一种模型,只包括对观察者来说的必须章节。
数据抽象(data abstraction):把数据的逻辑视图和它的实现分离开。
过程抽象(procedural abstraction):把动作的逻辑视图和它的现实分离开。
控制抽象(control abstraction):把控制结构的逻辑视图和它的现实分离开。
控制结构(control structure):用于改变正常的顺序控制流的语句。
7.7.3事物命名
在编写算法是,我们使用速记短语表示要处理的任务和信息,也就是说,给数据和过程一个名字,这些名字叫做标识符
7.7.4测试
两种基本的测试分类:白盒测试,基于代码本身;黑盒测试,基于测试所有可能的输入值。
小结
- Polya在他的经典著作《如何解决它》中列出来数学问题的求解策略。应用这些策略时,将生成一个解决问题的方案。在计算领域,这种方案成为算法。
- 循环有两种,分为计数控制循环与事件控制循环。计数控制循环会执行预定次数的循环,而事件控制循环则是执行到循环中时间的改变。
- 数据有两种形式:不可分割的和复合的。数组是一种同构的结构,给出了一个有名称的元素的集合和允许用户访问个别元素在结构中的位置。
- 搜索是指在数组中寻找一个特定值的行为。排序是指将数组中的元素按一定的顺序排列。
- 递归算法是指可以在子程序自身中出现子程序名的算法。阶乘和二分检索实际上就是递归算法。