《程序是怎样跑起来的》chap1~chap4笔记
Posted 临风而眠
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《程序是怎样跑起来的》chap1~chap4笔记相关的知识,希望对你有一定的参考价值。
《程序是怎样跑起来的》chap1~chap4 笔记
这本科普书感觉很棒
文章目录
- 《程序是怎样跑起来的》chap1~chap4 笔记
- chap1 对程序员来说CPU是什么
- chap2 数据是用二进制数表示的
- chap3 计算机进行小数运算出错的原因
- chap4 熟练使用有棱有角的内存
chap1 对程序员来说CPU是什么
1.1 CPU的内部结构解析
1.2 CPU是寄存器的集合体
-
普通的程序员主要关注寄存器极客
-
程序是把寄存器作为对象来描述的
-
汇编
-
汇编语言采用**助记符(memonic)**来编写程序,每 一个原本是电气信号的机器语言指令都会有一个与其相应的助记符, 助记符通常为指令功能的英语单词的简写。例如,mov 和 add 分别是 数据的存储(move)和相加(addition)的简写
-
通常我们将汇编语言编写的程序转化成机器语言的过程称为汇编;反之,机器语言程序转化成汇编语言程序的过程则称为反汇编
-
把汇编语言转化成机器语言的程序称为汇编器(assembler)。有时汇编语言 也称为汇编
-
编译是指将使用高级编程语言编写的程序转换为机器语言的过程,其中, 用于转换的程序被称为编译器(compiler)。
-
-
-
寄存器主要种类
- 对程序员来说,CPU是什么呢?如图1-3所示,CPU是具有各种功能的寄存器的集合体。其中,程序计数器、累加寄存器、标志寄存器、指令寄存器和栈寄存器都只有一个,其他的寄存器一般有多个。程序计数器和标志寄存器比较特殊,这一点在后面的章节中会详细说明。另外,存储指令的指令寄存器等寄存器,由于不需要程序员做多关注,因此图1-3中没有提到。
- 对程序员来说,CPU是什么呢?如图1-3所示,CPU是具有各种功能的寄存器的集合体。其中,程序计数器、累加寄存器、标志寄存器、指令寄存器和栈寄存器都只有一个,其他的寄存器一般有多个。程序计数器和标志寄存器比较特殊,这一点在后面的章节中会详细说明。另外,存储指令的指令寄存器等寄存器,由于不需要程序员做多关注,因此图1-3中没有提到。
1.3 决定程序流程的程序计数器
- 实际上,一个命令和数据通常被存储在多个地址上,但为了便于说明,下图中把指令、数据分配到了一个地址中
- 地址 0100 是程序运行的开始位置。Windows 等操作系统把程序从硬盘复制到内存后,会将程序计数器(CPU 寄存器的一种)设定为0100,然后程序便开始运行。CPU 每执行一个指令,程序计数器的值就会自动加 1。例如,CPU 执行 0100 地址的指令后,程序计数器的值 就变成了 0101(当执行的指令占据多个内存地址时,增加与指令长度相应的数值)。然后,CPU 的控制器就会参照程序计数器的数值,从内 存中读取命令并执行。也就是说,程序计数器决定着程序的流程
- 地址 0100 是程序运行的开始位置。Windows 等操作系统把程序从硬盘复制到内存后,会将程序计数器(CPU 寄存器的一种)设定为0100,然后程序便开始运行。CPU 每执行一个指令,程序计数器的值就会自动加 1。例如,CPU 执行 0100 地址的指令后,程序计数器的值 就变成了 0101(当执行的指令占据多个内存地址时,增加与指令长度相应的数值)。然后,CPU 的控制器就会参照程序计数器的数值,从内 存中读取命令并执行。也就是说,程序计数器决定着程序的流程
1.4 条件分支和循环机制
启发挺大的
-
程序的流程分为顺序执行、条件分支和循环三种。
-
顺序执行是指按照地址内容的顺序执行指令。
-
条件分支是指根据条件执行任意地址的指令。
-
循环是指重复执行同一地址的指令。
-
顺序执行的情况比较简单,每执行一个指令程序计数器的值就自动加 1。但若程序中存在条件 、分支和循环,机器语言的指令就可以将程序计数器的值设定为任意地址(不是 +1)。这样一来,程序便可以返回到上一个地址来重复执行同 一个指令,或者跳转到任意地址。
-
接下来,以条件分支为例, 来具体说明循环时程序计数器的数值设定机制也是一样的。
-
-
条件分支和循环中使用的跳转指令,会参照当前执行的运算结果 来判断是否跳转。
-
标志寄存器
-
无论当前累加寄存器的运算结果是负数、零还是正数,标志寄存器都会将其保存(也负责存放溢出 和奇偶校验 的结果)
-
-CPU在进行运算时,标志寄存器的数值会根据运算结果自动设定。条件分支在跳转指令前会进行比较运算。 至于是否执行跳转指令,则由CPU在参考标志寄存器的数值后进行判断。运算结果的正、零、负三种状态由标志寄存器的三个位“表示。图1-6是32位CPU(寄存器的长度是32位)的标志寄存器的示例。标志寄存器的第一个字节位、第二个字节位和第三个字节位的值为1时,表示运算结果分别为正数、零和负数。
-
-
CPU执行比较的机制
1.5 函数的调用机制
-
哪怕是高级语言编写的程序, 函数 调用处理也是通过把程序计数器的值设定成函数的存储地址来实现的。不过,这和条件分支、循环的机制有所不同,因为单纯的跳转指令无法实现函数的调用。函数的调用需要在完成函数内部的处理后,处理流程再返回到函数调用点(函数调用指令的下一个地址)。因此,如果只是跳转到函数的入口地址,处理流程就不知道应该返回至哪里了
-
例
-
上图是给变量 a 和 b 分别代入 123 和 456 后,将其赋值给参数 (parameter)来调用 MyFunc 函数的 C 语言程序。
-
图中的地址是将 C语言编译成机器语言后运行时的地址。由于 1 行 C 语言程序在编译后通常会变成多行的机器语言,所以图中的地址是离散的
-
-
call和return指令
-
解决上面这个问题,函数调用使用的是 call 指令,而不是跳转指令。 在将函数的入口地址设定到程序计数器之前,call 指令会把调用函数后要执行的指令地址存储在名为栈 A的主存内。
-
函数处理完毕后,再通过 函数的出口来执行 return 命令。
-
return 命令的功能是把保存在栈中的地址设定到程序计数器中。如图 1-7 所示,MyFunc 函数被调用之前, 0154 地址保存在栈中。MyFunc 函数的处理完毕后,栈中的 0154 地址 就会被读取出来,然后再被设定到程序计数器中(图 1-8)
-
在编译高级编程语言的程序后,函数调用的处理会转换成 call 指 令,函数结束的处理则会转换成 return 指令。这样一来,程序的运行也就变得非常流畅
-
1.6 通过地址和索引实现数组
-
基址寄存器和变址寄存器
-
居然和数组联系起来了好耶
-
1.7 CPU的处理其实很简单
- 按照功能对 CPU 能执行的机器语言指令进行大体分类
chap2 数据是用二进制数表示的
2.1 用二进制数表示计算机信息的原因
-
CPU和内存是 IC 的一种
-
IC 的所有引脚,只有直流电压 低电平 或 高电平 两个状态。也就是说,IC 的一个引脚,只能表示两个状态。
-
IC 的这个特性,决定了计算机的信息数据只能用二进制数来处理。 由于 1 位(一个引脚)只能表示两个状态,所以二进制的计数方式就变 成了 0、1、10、11、100…这种形式。
-
计算机处理信息的最小单位—— 位,就相当于二进制中的一位。位的英文 bit 是二进制数位 (binary digit)的缩写
-
8 位二进制数被称为一个字节 。
-
字节是最基本的信息计量单位。
-
位是最小单位, 字节是基本单位。
-
内存和磁盘都使用字节单位来存储和读写数据
-
2.2 什么是二进制数
-
基数:数值的表现方法,进位计数制中各数位上可能有的数值的个数。十进制数 的基数是 10,二进制数的基数是 2
-
位权:“○○的 ×× 次幂” ,
- ○○:基数
- ××,在任何进制数中都是“数的位数-1”
- 即第 1 位是 1- 1 = 0 次幂,第 2 位是 2- 1 = 1 次幂, 第 3 位是 3- 1 = 2 次幂
2.3 移位运算和乘除运算的关系
-
移位运算:将二进制数值的各数位进行左右移位(shift = 移位)的运算。
- 移位有左移(向高位方向) 和右移(向低位方向)两种
- << 这个运算符表示 左移,右移时使用 >> 运算符。
- << 运算符和 >> 运算符的左侧是被移位的值,右侧表示要移位的位数
-
例
-
不过,移位运算也可以通过数位移动来代替乘法运算和除法运算。 例如,将 00100111 左移两位的结果是 10011100,左移两位后数值变成 了原来的 4 倍。用十进制数表示的话,数值从 39(00100111)变成了 156(10011100),也正好是 4 倍(39×4 = 156)
2.4 便于计算机处理的“补数”
2.3中之所以没有介绍有关右移的内容,是因为用来填充右移后空出来的高位的数值,有 0 和 1 两种形式。要想区分什么时候补 0 什么 时候补 1,只要掌握了用二进制数表示负数的方法即可
-
二进制数中表示负数值时,一般会把最高位作为符号来使用,因 此我们把这个最高位称为符号位。符号位是 0 时表示正数 ,符号位是 1 时表示负数。
-
计算机在做减法运算时,实际上内部是在做加法运算。用加法 运算来实现减法运算
- 为此,在表示负数时就需 要使用“二进制的补数”。
这本书感觉这节讲的并不是很清楚…
2.5 逻辑右移和算术右移的区别
-
逻辑右移
-
算术右移
将二进制数作为带符号的数值进行运算时,移位后要在最高位填充移位前符号位的值(0或1)。这就称为算术右移。
- 如果数值是用补数表示的负数值,那么右移后在空出来的最高位补1,就可以正确地实现1/2、1/4、1/8等的数值运算。
- 如果是正数,只需在最高位补0即可
-
只有在右移时才必须区分逻辑位移和算术位移。左移时,无论是 图形模式(逻辑左移)还是相乘运算(算术左移),都只需在空出来的 低位补 0 即可
- 例
- 例
-
符号扩充
- 以 8 位二进制数为例,符号扩充就是指在保持值不变的前提下将其转换成 16 位和 32 位的二进制数
- 不管是正数还是用补数表示的负数,都只需用符号位的值(0 或者 1)填充高位即可
2.6 掌握逻辑运算的窍门
我们不妨这样考虑,将二进制数表示的信息作为四则运算 的数值来处理就是 算术。而像图形模式那样,将数值处理为单纯的 0 和 1 的罗列就是 逻辑
- 真值表
-
例:白色为1,黑色为0
这里挺有意思
- 异或:相同取0,不同取1
这个网格让我想到了计算机图形学的实验
chap3 计算机进行小数运算出错的原因
3.1 将0.1累加100次也得不到10
-
例
程序没错,计算机也没有发生故障,当然,C语言也没有什么问题。可为什么会出现这样的结果呢?这时,如果考虑一下计算机处理小数的机制,就讲得通了。那么,计算机内部是如何处理小数的呢?
3.2 用二进制数表示小数
-
二进制数小数点前面部分的位权,第 1 位是 2 的 0 次幂、第 2 位 是 2 的 1 次幂……以此类推。小数点后面部分的位权,第 1 位是 2 的-1 次幂、第 2 位是 2 的-2 次幂,以此类推
-
例
-
3.3 计算机运算出错的原因
-
原因:有一些十进制数的小数无法转换成二进制数
-
例如,十进制数 0.1,就无法用二进制数正确表示,小数 点后面即使有几百位也无法表示
-
👇
图3-2中,小数点后4位用二进制数表示时的数值范围为0.0000一0.1111。因此,这里只能表示0.5、0.25、0.125、0.0625这四个二进制数小数点后面的位权组合而成(相加总和)的小数。将这些数值组合后能够表示的数值,即为表3-1中所示的无序的十进制数。
-
表3-1中,十进制数0的下一位是0.0625。因此,这中间的小数,就无法用小数点后4位数的二进制数来表示。同样,0.0625的下一位数一下子变成了0.125。这时,如果增加二进制数小数点后面的位数,与其相对应的十进制数的个数也会增加,但不管增加多少位,2的-○○次幂怎么相加都无法得到0.1这个结果。实际上,十进制数0.1转换成二进制后,会变成0.00011001100… ( 1100循环)这样的循环小数。这和无法用十进制数来表示1/3是一样的道理。1/3就是0.3333…,同样是循环小数。
-
至此,大家应该明白了为什么用代码清单3-1的程序无法得到正确结果了吧。因为无法正确表示的数值,最后都变成了近似值。计算机这个功能有限的机器设备,是无法处理无限循环的小数的。因此,在遇到循环小数时,计算机就会根据变量数据类型所对应的长度将数值从中间截断或者四舍五入。我们知道,将0.3333…这样的循环小数从中间截断会变成0.333333,这时它的3倍是无法得出1的(结果是0.999999 )、计算机运算出错的原因也是同样的道理。
3.4 什么是浮点数
https://akaedu.github.io/book/ch14s04.html
-
双精度浮点数类型用 64 位、单精度浮点数类型用 32 位来表示全体小数
-
在 C 语言中,双精度浮点数类型和单精 度浮点数类型分别用 double 和 float 来表示。不过,这些数据类型都采 用浮点数 来表示小数。
-
浮点数是指用符号、尾数、基数和指数这四部分来表示的小数
-
浮点数的内部构造
-
尾数部分和指数部分并不只 是单单存储着用整数表示的二进制数。尾数部分用的是“将小数点前面 的值固定为 1 的正则表达式”,而指数部分用的则是“EXCESS 系统表现”
3.5 正则表达式和EXCESS系统
这部分没完全看懂,以后回头再来看
按照特定的规则来表示数据的形式即为正则表达式。除小数之外,字符串 以及数据库等,也都有各自的正则表达式
-
尾数部分使用正则表达式
- 可以将表现形式多样的浮点数统一为 一种表现形式。
- 例如,十进制数 0.75 就有很多种表现形式,如图 3-5 所示。虽然它们表示的都是同一个数值,但因为表现方法太多,计算机在处理时会比较麻烦。因此,为了方便计算机处理,需要制定一个统一的规则。
- 例如,十进制数的浮点数应该遵循“小数点前面是 0,小数点后面第 1 位不能是 0”这样的规则。根据这个规则,0.75 就是 “0.75×10 的 0 次幂”,也就是说,只能用尾数部分是 0.75、指数部分是 0 这个方法来表示。根据这个规则来表示小数的方式,就是正则表达式
- 我们使用的是“将小数点前面的值固定为 1 的正则表达式”。具体来讲,就是将二进制数表示的小数左移或右移(这里是逻辑移位。因为符号位是独立的 )数次后,整数部分的第 1 位变为 1,第 2位之后都变为 0(这样是为了消除第 2 位以上的数位)。**而且,第 1 位的 1 在实际的数据中不保存。**由于第 1 位必须是 1,因此,省略该部分后就 节省了一个数据位,从而也就可以表示更多的数据范围(虽不算太多)
-
指数部分使用的EXCESS系统
-
举例说明
- 假设有这样一 个游戏,用1~13(A~K)的扑克牌来表示负数。这时,我们可以把中间的 7 这张牌当成 0。如果扑克牌 7 是 0,10 就表示+3,3 就表 示-4。事实上,这个规则说的就是 EXCESS 系统
3.6 在实际的程序中进行确认
读到这里,有人额角冒汗吗?上述内容不是仅仅读一遍就能马上理解的,最好能够在实际的程序中加以确认。因此,我们准备了一个试验用的程序,如代码清单3-2所示。接下来,就让我们一起看一下如何用单精度浮点数来表示十进制数0.75吧。
该程序执行后,十进制数 0.75 用单精度浮点数来表示就变成了 0-01111110-10000000000000000000000(图 3-7)。加入破折号(-)是为 了区分符号部分、指数部分、尾数部分。这里,符号部分为 0,指数部 分为 01111110,尾数部分为 10000000000000000000000。因为 0.75 是 正数,所以符号位是 0。指数部分的 01111110 是十进制数 126,用 EXCESS 系统表现就是- 1(126- 127 =- 1)。根据正则表达式的规则, 小数点前面的第 1 位是 1,因此尾数部分 10000000000000000000000 实 际上表示的是 1.10000000000000000000000 这个二进制数。将尾数部分 的二进制数转换成十进制数,结果就是(1 × 2 的 0 次幂)+(1 × 2 的-1次幂)= 1.5。因此,0-01111110-10000000000000000000000 这个单精度浮点数,表示的就是“+ 1.5 × 2 的-1 次幂”。2 的-1 次幂是 0.5,+ 1.5 × 0.5 = + 0.75。正好吻合,结果正确
那个指数应该就是直观感觉上的移动位数再使用EXCESS系统处理一下
3.7 如何避免计算机计算出错
-
计算机计算出错的原因之一是,采用浮点数来处理小数(另外,也有因“位溢出”而造成计算错误的情况)。作为程序的数据类型,不管是使用单精度浮点数还是双精度浮点数,都存在计算出错的可能性。接下来将介绍两种避免该问题的方法。
-
首先是回避策略,即无视这些错误。根据程序目的的不同,有时一些微小的偏差并不会造成什么问题。例如,假设使用计算机设计工业制品。将100个长0.1毫米的零件连接起来后,其长度并非一定要是10毫米,10.000002毫米也没有任何问题。一般来讲,在科学技术计算领域,计算机的计算结果只要能得到近似值就足够了。那些微小的误差完全可以忽略掉。
-
另一个策略是把小数转换成整数来计算。计算机在进行小数计算时可能会出错,但进行整数计算(只要不超过可处理的数值范围)时一定不会出现问题。因此,进行小数的计算时可以暂时使用整数,然后再把计算结果用小数表示出来即可。例如,本章一开头讲过的将0.1相加100次这一计算,就可以转换为将0.1扩大10倍后再将1相加100次的计算,最后把结果除以10就可以了(代码清单3-3)。
- 除此之外,BCD ( Binary Coded Decimal )也是一种使用二进制表示十进制的方法。
3.8 二进制数和十六进制数
-
在以位为单位表示数据时,使用二进制数很方便,但如果位数太多,看起来就比较麻烦。因此,在实际程序中,也经常会用十六进制数来代替二进制数。在C语言程序中,只需在数值的开头加上0x (0和x )就可以表示十六进制数,
-
二进制数的4位,正好相当于十六进制数的1位。例如,32位二进制数00111101110011001100110011001101用十六进制数来表示的话,就是3DCCCCCD这个8位数。由此可见,通过使用十六进制数,二进制数的位数能够缩短至原来的1/4。位数变少之后,看起来也就更清晰了(图3-9)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oN05HKJD-1669650779520)(https://cdn.jsdelivr.net/gh/xin007-kong/picture_new/img/20221128233550.png)]
-
用十六进制数来表示二进制小数时,小数点后的二进制数的4位也同样相当于十六进制数的1位。不够4位时用О填补二进制数的低位即可。例如,1011.011的低位补0后为1011.0110,这时就可以表示为十六进制数B.6(图 3-10)。十六进制数的小数点后第1位的位权是即 1/16=0.0625
chap4 熟练使用有棱有角的内存
4.1 内存的物理机制很简单
-
内存实际上是一种名为内存 IC 的电子元件。虽然内存 IC 包 括 DRAM、SRAM、ROMA 等多种形式,但从外部来看,其基本机制都 是一样的。内存 IC 中有电源、地址信号、数据信号、控制信号等用于 输入输出的大量引脚(IC 的引脚),通过为其指定地址(address),来 进行数据的读写
-
读写数据图示
-
C语言程序案例
下面我们来看一个具体的示例。如代码清单4-1所示,这是一个往a、b、c这3个变量中写入数据123的C语言程序。这3个变量表示的是内存的特定区域。通过使用变量,即便不指定物理地址,也可以在程序中对内存进行读写。这是因为,在程序运行时,Windows等操作系统会自动决定变量的物理地址。
- 这3个变量的数据类型分别是,表示1字节长度的char,表示2字节长度的 short,以及表示4字节长度的 long’。因此,虽然同样是数据123,存储时其所占用的内存大小是不一样的。这里,我们假定采用的是将数据低位存储在内存低位地址的低字节序( little endian)“方式(图4-4 )。
4.3 简单的指针
-
指针也是一种变量,它所表示的不是数据的值,而是存储着数据的内存的地址。
-
通过使用指针,就可以对任意指定地址的数据进行读写。
-
虽然前面所提到的假想内存 IC 中仅有 10 位地址信号,但在 Windows 计算机上使用的程序通常都是 32 位(4 字节)的内存地址。 这种情况下,指针变量的长度也是 32 位。
-
woc,突然知道为啥指针是4字节了!!!
-
举例
-
从内存的角度读懂C指针 - 开发者共读的文章 - 知乎 https://zhuanlan.zhihu.com/p/249330470 这篇知乎文章写的有些错误,不过总体还行
4.4 数组是高效使用内存的基础
-
数组是指多个同样数据类型的数据在内存中连续排列的形式。
-
作为数组元素的各个数据会通过连续的编号被区分开来,这个编号称 为索引(index)。指定索引后,就可以对该索引所对应地址的内存进行 读写操作 。而索引和内存地址的变换工作则是由编译器自动实现的。
-
例
-
数组的定义中所指定的数据类型,也表示一次能够读写的内存大小。char 类型的数组以 1 个字节为单位对内存进行读写,而 short 类型 和 long 类型的数组则分别以 2 个字节、4 个字节为单位对内存进行读写。
-
之所以说数组是内存的使用方法的基础,是因为数组和内存的物理构造是一样的。特别是 1 字节类型的数组,它和内存的物理构造完全一致。不过,如果只能逐个字节地来读写,程序就会变得比较麻烦, 因而可以指定任意数据类型来定义数组。
4.5 栈、队列以及环形缓冲区
-
栈 和队列,都可以不通过指定地址和索引来对数组的元素进行读写。需要临时保存计算过程中的数据、连接在计算机上的设备或者输入 输出的数据时,都可以通过这些方法来使用内存。如果每次保存临时数据都需指定地址和索引,程序就会变得比较麻烦,因此要加以改进。
-
栈和队列的区别在于数据出入的顺序是不同的。在对内存数据进 行读写时,栈用的是 LIFO(Last Input First Out,后入先出)方式,而 队列用的则是 FIFO(First Input First Out,先入先出)方式。
-
如果我们 在内存中预留出栈和队列所需要的空间,并确定好写入和读出的顺序, 就不用再指定地址和索引了。
-
如果要在程序中实现栈和队列,就需要以适当的元素数来定义一个用来存储数据的数组,以及对该数组进行读写的函数对。当然,在 这些函数的内部,对数组的读写会涉及索引的管理,但从使用函数的 角度来说,就没有必要考虑数组及索引了
-
暂且把往栈中写入数据的函数命名为 Push,把从栈中 读出数据的函数命名为 Pop,
- 把往队列中写入数据的函数命名为 EnQueue,把从队列中读出数据的函数命名为 DeQueue 。Push 和 Pop 以及 EnQueue 和 DeQueue 分别组成一对函数。Push 和 EnQueue 用于为 函数的参数传递要写入的数据。Pop 和 DeQueue 用于将读出的数据作 为函数返回值返回。通过使用这些函数,可以将数据临时保存(写入), 然后再在需要时候把这些数据读出来
-
队列一般以环状缓冲区实现
4.6 链表使元素的追加和删除更容易
-
链表和二叉查找树,都是不用考虑索引的顺序就可 以对数组元素进行读写的方式。通过使用链表,可以更加高效地对数 组数据(元素)进行追加和删除处理。而通过使用二叉查找树,则可以 更加高效地对数组数据进行检索
-
在数组的各个元素中,除了数据的值之外,通过为其附带上下一 个元素的索引,即可实现链表。数据的值和下一个元素的索引组合在一起,就构成了数组的一个元素。这样,数组元素相连就构成了念珠似的链表。由于链表末尾的元素没有后续的数据,因此就需要用别的值(在这里是-1)来填充
- 删除
在需要追加或删除数据的情况下,使用链表是很高效的。首先,让我们来看一下删除的情况。在图4-10表示的链表中,假设要删除从起始位置开始的第3个元素。此时,我们只需要把第2个元素的“下一个元素:2”变成“下一个元素:3”即可。由于数组的元素通常是按照索引顺序来引用的,因此当我们需要引用构成链表的数组的某一个元素时,通过该元素的索引信息就可以找到下一个元素。当第2个元素的下一个元素变成第4个元素后,那么第3个元素就被删除了。虽然第3个元素在物理内存上还残留着,但在逻辑上则确实被删除了(图4-11 )。
- 增加数据
接下来就让我们来看一下如何往链表中追加数据。假设要在图4-10的链表的第5位前追加一个新数据。此时,我们只需要在刚才消除的第3个元素的位置中保存新的数据,并将第4个元素的“下一个元素:5”变更成“下一个元素:2”,以使新追加的元素的索引信息变成“下一个元素:5”即可。虽然新追加的元素在物理上是第3个,但从逻辑上看来则是第5个(图4-12)。
- 如果不使用链表数组,那么中途删除或追加元素时,其后的元素 就必须要全部移动。示例中数组的元素只有 6 个,处理起来不会花费 较多时间。而在实际的程序中,有时需要对包含数千至数万个元素的数组进行频繁的数据追加或删除操作。如果每次都需要移动数千至数 万个元素,那么哪怕是高速计算机也会花费很长时间(图 4-13、图 4-14)。反之,使用代码清单来追加或删除数据则毫不费事。
4.7 二叉查找树使数据搜索更有效
- 二叉查找树是指在链表的基础上往数组中追加元素时,考虑到数据的大小关系,将其分成左右两个方向的表现形式。例如,假设我们事先把50这个值保存到了数组中。那么,如果接下来的值比先前保存的数值大的话,就要将其放到右边,反之如果小的话就放在左边。但实际的内存并不会分成两个方向,这是在程序逻辑上实现的(4-15)
-
为了实现二叉查找树,怎么处理比较好呢?其实数组的每个元素中只要有数据的值和两个索引信息就可以了。图4-16向我们展示了如何用数组来实现图4-14中的二叉查找树。二叉查找树是由链表构造发展而来的表现形式,因此在追加或删除元素方面也同样是有效的。
-
使用二叉查找树的便利之处在于可以使数据的搜索等更有效率。在使用一般的数组时,必须从数组的开头按照索引顺序来查找目标数据。而使用二叉查找树时,当目标数据比现在读出来的数据小时就可以转到左侧,反之目标数据较大时即可转到链表的右侧,这样就加快了找到目标数据的速度。
之前学数据结构的时候没有思考到这一层…
数组中。那么,如果接下来的值比先前保存的数值大的话,就要将其放到右边,反之如果小的话就放在左边。但实际的内存并不会分成两个方向,这是在程序逻辑上实现的(4-15)
以上是关于《程序是怎样跑起来的》chap1~chap4笔记的主要内容,如果未能解决你的问题,请参考以下文章