一、计算机组成原理
关系代数笛卡尔积+选择+投影题
一先学透所有核心知识点(零基础必看)
1. 关系代数三大核心运算符
| 运算符 | 名称 | 语法格式 | 核心作用 | 零基础大白话 |
|---|---|---|---|---|
× |
笛卡尔积(Cartesian Product) | R × S |
将两个关系的所有元组两两组合,生成新关系 | 把R的每一行,和S的每一行全部拼在一起,所有组合都列出来 |
σ |
选择(Selection) | σ_{条件}(R) |
按条件筛选行,只保留满足条件的元组 | 「挑行」,只留符合条件的行,列数不变 |
π |
投影(Projection) | π_{列号/列名}(R) |
按列号/列名筛选列,只保留指定的列 | 「挑列」,只留指定的列,行数不变 |
2. 运算符优先级与运算顺序
关系代数的运算顺序严格遵循从内到外先括号后括号外:
先算括号内的
R×S(笛卡尔积)→ 再算σ(选择,挑行)→ 最后算π(投影,挑列)
3. 列号与列名的对应规则(解题核心)
当两个关系做笛卡尔积 R×S 时,新关系的列顺序 = R的所有列在前,S的所有列在后,列号从1开始依次编号。
- 若两个表有重名列(比如本题R和S都有
CD列),必须用表名前缀区分:R.C代表R表的C列,S.C代表S表的C列,否则会产生歧义。
4. 本题关系RS的结构明细
关键结论:
R×S共7列,列号1-7对应:1:R.A2:R.B3:R.C4:R.D5:S.C6:S.D7:S.E
|
若关系 R、S 如下图所示,则关系代数表达式 $\pi_{1,3,7}(\sigma_{3<6}(R \times S))$ 与第( )题等价。 |
| A. |
| B.$\pi_{A,R.C,E}(\sigma_{R.C<S.D}(R \times S))$ |
| C.$\pi_{A,S.C,S.E}(\sigma_{R.C<S.D}(R \times S))$ |
| D.$\pi_{R.A,R.C,R.E}(\sigma_{R.C<S.D}(R \times S))$ |
| B |
|
知识点一:什么是"关系" 关系代数里的"关系"就是一张二维表,和你平时见到的 Excel 表格一模一样。这道题里 R 是一张 4 列 3 行的表,S 是一张 3 列 2 行的表。 --- 知识点二:什么是笛卡尔积 $R \times S$ 笛卡尔积就是把两张表的每一行两两组合,拼成一张大表。R 有 3 行,S 有 2 行,结果是 $3 \times 2 = 6$ 行。列数相加,R 有 4 列,S 有 3 列,结果有 7 列。 这 7 列从左到右依次编号,后面所有计算都依赖这个编号: $$\underbrace{R.A}_{1},\ \underbrace{R.B}_{2},\ \underbrace{R.C}_{3},\ \underbrace{R.D}_{4},\ \underbrace{S.C}_{5},\ \underbrace{S.D}_{6},\ \underbrace{S.E}_{7}$$ 注意 R 和 S 都有叫做 C 和 D 的列,拼在一起之后为了区分,要加上表名前缀写成 $R.C$、$S.C$、$R.D$、$S.D$。 拼完之后的 6 行数据是(前4个数来自R,后3个数来自S): $$\text{行①}\ (1,2,4,6,3,4,2) \quad R\text{第1行} + S\text{第1行}$$ --- 知识点三:什么是选择操作 $\sigma$ $\sigma$ 读作"sigma",是筛选行的操作,只有满足条件的行才保留。 这道题写的是 $\sigma_{3<6}$,意思是"第3列的值小于第6列的值"才保留。对照列编号,第3列是 $R.C$,第6列是 $S.D$,条件就是 $R.C < S.D$。 逐行检验: $$\text{行①}:\ R.C=4,\ S.D=4,\quad 4 < 4\ \text{不成立,删掉}$$ 筛选后剩 5 行: $$\sigma_{R.C<S.D}(R \times S) = \{(1,2,4,6,8,9,3),\ (2,3,3,1,3,4,2),\ (2,3,3,1,8,9,3),\ (3,4,1,3,3,4,2),\ (3,4,1,3,8,9,3)\}$$ --- 知识点四:什么是投影操作 $\pi$ $\pi$ 读作"pi",是选取列的操作,只保留你想要的列,其余列全部丢掉。 这道题写的是 $\pi_{1,3,7}$,意思是只保留第1列、第3列、第7列: $$\underbrace{\text{第1列}}_{R.A},\quad \underbrace{\text{第3列}}_{R.C},\quad \underbrace{\text{第7列}}_{S.E}$$ 从上面 5 行中每行只取这三列: $$\pi_{1,3,7}(\cdots) = \{(1,4,3),\ (2,3,2),\ (2,3,3),\ (3,1,2),\ (3,1,3)\}$$ 最终结果共 5 行,3 列,列名是 $A$、$R.C$、$E$。 --- 逐项分析四个选项 原式 $\pi_{1,3,7}(\sigma_{3<6}(R \times S))$ 的含义是:条件为 $R.C < S.D$,投影取 $A$、$R.C$、$E$。 选项 A:$\pi_{A,C,E}(\sigma_{C<D}(R \times S))$ $R \times S$ 之后有两个叫 $C$ 的列($R.C$ 和 $S.C$),也有两个叫 $D$ 的列($R.D$ 和 $S.D$),直接写 $C < D$ 完全有歧义,错误。 选项 B:$\pi_{A,R.C,E}(\sigma_{R.C<S.D}(R \times S))$ 条件 $R.C < S.D$ 对应第3列 $<$ 第6列,正确。投影取 $A$(第1列)、$R.C$(第3列)、$E$(第7列),完全吻合。**正确答案。** 选项 C:$\pi_{A,S.C,S.E}(\sigma_{R.C<S.D}(R \times S))$ 条件正确,但投影取的是 $S.C$(第5列),而原式第3列是 $R.C$,不是 $S.C$,取错了列。 选项 D:$\pi_{R.A,R.C,R.E}(\sigma_{R.C<S.D}(R \times S))$ 条件正确,但 $R$ 表的列名只有 $A、B、C、D$,根本不存在 $R.E$ 这一列,表达式非法。 --- 最终答案是 $\textbf{B}$ 解题核心步骤: $$R \times S \xrightarrow{\text{列编号}} \sigma_{3<6} \xrightarrow{\text{筛行}} \pi_{1,3,7} \xrightarrow{\text{取列}} \text{结果}$$ 把数字编号翻译成列名,再去选项里逐一比对即可。 现在回头逐个分析四个选项 原式用数字编号写的是 π₁₃₇(σ₃<₆(R×S)),我们已经知道它的含义是:条件为 R.C < S.D,投影取 AR.CE。等价的选项就是要把这个含义用列名而不是列号表达出来,两种写法说的必须是同一件事。 选项 A:π_{ACE}(σ_{C<D}(R×S)) 这个选项的问题在于 R×S 之后有两个叫 C 的列(R.C 和 S.C),也有两个叫 D 的列(R.D 和 S.D)。直接写 C<D 完全不知道是哪个 C 和哪个 D 在比较,表达式是有歧义的,因此错误。 选项 B:π_{AR.CE}(σ_{R.C<S.D}(R×S)) 条件写成 R.C < S.D,对应原式的第3列<第6列,正确。投影取 AR.CE,A 对应第1列,R.C 对应第3列,E 对应第7列,完全吻合。这就是正确答案。 选项 C:π_{AS.CS.E}(σ_{R.C<S.D}(R×S)) 条件 R.C<S.D 是对的,但投影取的是 S.C,而原式第3列是 R.C 不是 S.C。R.C 是 R 表自己的 C 列(第3列),S.C 是 S 表的 C 列(第5列),两者数值完全不同,取错了。 选项 D:π_{R.AR.CR.E}(σ_{R.C<S.D}(R×S)) 条件 R.C<S.D 是对的,但投影里写了 R.E。回头看 R 表的列名是 ABCD,根本没有 E 这一列,所以 R.E 根本不存在,这个表达式是非法的。 最终答案是 B 记住解这类题的核心步骤:第一,做笛卡尔积之前先把新表的列编号从左到右写清楚第二,把 σ 下标的数字翻译成对应的列名第三,把 π 下标的数字翻译成对应的列名第四,拿着翻译好的列名去选项里逐一对照。
|
码制(原码/反码/补码/移码)零基础完整知识点
一核心定义与转换规则(对应第一张图)
1. 正数的码制转换
- 正数的原码 = 反码 = 补码(三者完全一致,无任何变化)
- 正数转移码:数值位不变,仅符号位取反(0变1)
2. 负数的码制转换(核心流程)
- 原码 → 反码:符号位不变,数值位全部取反(0变1,1变0)
- 反码 → 补码:符号位参与运算,给反码的数值位末位加1
- 补码 → 移码:数值位不变,仅符号位取反(1变0)
- 补码 → 原码(反向转换):符号位不变,数值位从右往左数第1个1的前1位开始,全部取反(等价于补码再求补码)
二四种码制的基础定义(对应第二张图)
| 码制 | 核心定义 | 关键规则 |
|---|---|---|
| 原码 | 最高位为符号位,其余低位表示数值的绝对值,直接表示数据 | 符号位:0表示正数,1表示负数数值位为数值的二进制绝对值 |
| 反码 | 原码到补码的过渡编码,无独立存储价值 | 正数反码 = 原码负数反码 = 原码符号位不变,数值位取反 |
| 补码 | 计算机实际存储运算有符号整数的唯一编码 | 正数补码 = 原码负数补码 = 反码末位加1(符号位参与运算) |
| 移码 | 专门用于浮点数阶码的编码,用于简化大小比较 | 无论正负,移码 = 补码符号位取反,数值位不变 |
三8位码制运算示例表(对应第二张图,零基础直观理解)
以8位二进制(1位符号位+7位数值位)为标准,展示不同运算下四种码制的具体形式:
| 码制 | 数值+1(+1) | 数值-1(-1) | (+1) + (-1) = 0 |
|---|---|---|---|
| 原码 | 0000 0001 |
1000 0001 |
1000 0010(运算结果错误,原码无法直接做减法) |
| 反码 | 0000 0001(与原码相同) |
1111 1110(原码数值位取反) |
1111 1111(运算结果为-0,存在正负零问题) |
| 补码 | 0000 0001(与原码相同) |
1111 1111(反码末位加1) |
0000 0000(运算结果正确,无正负零) |
| 移码 | 1000 0001(补码符号位取反) |
0111 1111(补码符号位取反) |
1000 0000(0的移码,用于浮点数阶码) |
表格补充说明
- 原码运算缺陷:原码无法直接进行减法运算,
(+1)+(-1)得到1000 0010(对应-2),结果完全错误,因此计算机不使用原码存储。 - 反码运算缺陷:反码运算后得到
1111 1111,对应-0,存在“正负零”两个编码,浪费存储资源,仅作为补码的计算中间步骤。 - 补码核心优势:补码完美解决了原码反码的缺陷,
(+1)+(-1)直接得到正确的0,且只有一个0的编码,同时将减法转化为加法,大幅简化CPU硬件设计,是计算机的核心编码。 - 移码核心作用:移码的大小关系与真实数值完全一致,直接按二进制位比较就能判断大小,无需处理符号位,因此专门用于IEEE 754浮点数的阶码存储,简化浮点数的大小比较对阶运算。
- 关键备注:补码和移码中,±0的编码完全相同,不存在正负零的区分。
四完整转换流程总结(零基础速记)
1. 正数(符号位为0)
原码 = 反码 = 补码 → 符号位取反 → 移码
2. 负数(符号位为1)
原码 → 符号位不变数值位取反 → 反码 → 末位加1(符号位参与运算) → 补码 → 符号位取反 → 移码
补码 → 符号位不变数值位从右往左第一个1前取反 → 原码(反向转换)
五8位码制表示范围(必记考点)
| 码制 | 8位表示范围 | 说明 |
|---|---|---|
| 原码 | -127 ~ +127 | 存在±0两个编码,少表示一个数 |
| 反码 | -127 ~ +127 | 存在±0两个编码,少表示一个数 |
| 补码 | -128 ~ +127 | 仅一个0编码,多表示-128,计算机实际使用 |
| 移码 | -128 ~ +127 | 与补码范围一致,仅符号位取反 |
六补充:补码转真值的完整步骤(解题核心)
- 判断正负:补码最高位为0是正数,为1是负数
- 正数求真值:补码 = 原码,直接按权展开数值位,加正号
- 负数求真值:
- 补码 → 反码:符号位不变,数值位取反
- 反码 → 原码:符号位不变,数值位加1
- 原码 → 真值:符号位为负,按权展开数值位,加负号
|
题目:如果“2X”的补码是“90H”,那么X的真值是()。 |
| A.72 |
| B.-56 |
| C.56 |
| D.111 |
| B |
|
首先把补码90H转换成二进制,按照一位十六进制对应四位二进制的规则,9对应1001,0对应0000,所以90H对应的8位二进制补码是10010000。 |
|
某种机器的浮点数表示格式如下(允许非规格化表示)。若阶码以补码表示,尾数以原码表示,则 $1\ 0001\ 0\ 0000000001$ 表示的浮点数是( )。 |
| A.$2^{-16} \times 2^{-10}$ |
| B.$2^{-15} \times 2^{-10}$ |
| C.$2^{-16} \times (1 - 2^{-10})$ |
| D.$2^{-15} \times (1 - 2^{-10})$ |
| B |
|
知识点一:浮点数的结构
|
校验码知识点
什么是校验码
计算机在存储和传输数据的时候,可能因为硬件故障电磁干扰等原因导致某个二进制位从 0 变成 1,或者从 1 变成 0,这就叫做"出错"。校验码就是一种专门用来发现甚至纠正这种错误的编码方案。
常见的校验码有两种。一种叫奇偶校验码,只能发现奇数个位出错,但没办法确定是哪一位出错,所以只能检错不能纠错。另一种就是这道题考的海明码,它不仅能发现错误,还能精确定位出错的那一位,从而自动纠正。
海明码的基本原理
海明码的做法是:在原始数据位里插入若干个额外的校验位。每个校验位负责监控特定的几个数据位,它的值由被它监控的所有数据位共同决定。一旦某个数据位出错,就会有一部分校验位的值发生变化,通过观察哪些校验位出了问题,就能精确算出是第几位出错,然后把那一位翻转回来,完成纠错。
海明码需要几个校验位——核心公式
设原始数据有 $n$ 位,需要插入 $r$ 个校验位,则 $r$ 必须满足:
$$2^r \geq n + r + 1$$
这个公式怎么理解:$r$ 个校验位一共能表示 $2^r$ 种不同的状态组合。这些状态要能覆盖三种情况:第一,$n$ 个数据位各自出错,第二,$r$ 个校验位各自出错,第三,没有任何位出错。三种情况加起来一共需要 $n + r + 1$ 种状态,所以 $2^r$ 必须大于等于 $n + r + 1$。
求 $r$ 的方法就是从小到大逐个代入 $r = 1 2 3 \ldots$,找到第一个让不等式成立的 $r$,那就是最少需要的校验位数。
海明码的位置编号规则
把海明码的所有位(包括数据位和校验位)从右到左依次编号,从 $1$ 开始。
校验位只能放在编号为 $2$ 的幂次的位置上,也就是位置 $124816\ldots$,分别叫做 $P_1P_2P_3P_4\ldots$。其余位置放数据位。
某个数据位由哪些校验位负责——二进制分解
这是最核心的规则:把某个数据位的位置序号写成二进制,哪几个二进制位上是 $1$,就由对应位置上的校验位来监控这个数据位。
举个具体例子:位置序号 $14$,写成二进制是 $1110$,从右往左各位的权值是 $1248$,值为 $1$ 的位对应权值是 $248$,所以位置 $14$ 的数据由位置 $8$ 的 $P_4$位置 $4$ 的 $P_3$位置 $2$ 的 $P_2$ 共同负责校验。
再举一个:位置序号 $5$,写成二进制是 $0101$,值为 $1$ 的位对应权值是 $1$ 和 $4$,所以位置 $5$ 的数据由位置 $4$ 的 $P_3$位置 $1$ 的 $P_1$ 共同负责校验。
记住口诀:位置序号做二进制分解,分解出哪些 $2$ 的幂次,就由那些幂次位置上的校验位负责。
|
海明码是一种纠错码,对于 $32$ 位的数据,至少需要增加( )个校验位才能构成海明码。 |
| A.3 |
| B.4 |
| C.5 |
| D.6 |
| D |
|
解析: |
|
以10位数据为例,其海明码表示为 $D_9D_8D_7D_6D_5D_4P_4D_3D_2D_1P_3D_0P_2P_1$,其中 $D_i$($0 \leq i \leq 9$)表示数据位,$P_j$($1 \leq j \leq 4$)表示校验位,数据位 $D_5$ 由( )进行校验。 |
| A.$P_4P_1$ |
| B.$P_4P_2$ |
| C.$P_4P_3P_1$ |
| D.$P_3P_2P_1$ |
| B |
|
解析:
|
层次化存储体系
|
CPU访问存储器时,被访问数据一般聚集在一个较小的连续存储区域中。若一个存储单元已被访问,则其邻近的存储单元有可能还要被访问,该特性被称为( )。 |
| A.数据局部性 |
| B.指令局部性 |
| C.空间局部性 |
| D.时间局部性 |
| C |
|
这道题考查的是计算机科学中非常重要的**“局部性原理”(Locality of Reference)**。为了方便理解,我们可以用日常生活来打比方:
|
|
计算机系统的主存主要是由( )构成的。 |
| A.DRAM |
| B.SRAM |
| C.Cache |
| D.EEPROM |
| A |
|
这道题考查的是计算机内存硬件的组成。
简单总结记忆:
|
内存编址与存储芯片知识点
什么是十六进制
计算机里的地址通常用十六进制表示,十六进制用 $0\text{-}9$ 和 $A\text{-}F$ 共16个符号,其中 $A=10B=11C=12D=13E=14F=15$。地址末尾加 H 就表示这是一个十六进制数。
按字节编址是什么意思
按字节编址的意思是:内存里每一个字节都有一个独立的地址编号,相邻两个地址之间正好差1个字节。所以从地址 $X$ 到地址 $Y$ 一共包含的字节数就是:
$$\text{字节数} = Y - X + 1$$
加1是因为要把起始地址本身也算进去,就像从第1页到第5页一共是5页而不是4页一样。
怎么计算两个十六进制地址之间的字节数
直接用末地址减去首地址,再加1,结果就是字节数。十六进制做减法和十进制一样,只是逢16进1。
KB 和字节的换算
$1\text{KB} = 1024$ 字节 $= 2^{10}$ 字节。换算时把字节数除以 $1024$ 就得到 KB 数。
存储芯片的容量怎么看
一块芯片写成"$64\text{K} \times 8\text{bit}$",意思是这块芯片能存 $64\text{K}$ 个存储单元,每个存储单元是 $8$ 位(即1字节)。所以这块芯片的总容量就是 $64\text{K} \times 1$ 字节 $= 64\text{KB}$。
需要几片芯片
用需要覆盖的总内存容量除以每片芯片的容量,就得到需要的芯片数量:
$$\text{芯片数} = \frac{\text{总内存容量}}{\text{单片芯片容量}}$$
|
内存按字节编址,地址从 $\text{A0000H}$ 到 $\text{CFFFFH}$ 的内存,共有( )字节。 |
| A. $80\text{KB}$ |
| B.$96\text{KB}$ |
| C.$160\text{KB}$ |
| D.$192\text{KB}$ |
| D |
|
按公式,字节数 $=$ 末地址 $-$ 首地址 $+ 1$。 |
|
若用存储容量为 $64\text{K} \times 8\text{bit}$ 的存储器芯片构成该内存空间,至少需要( )片。 |
| A.2 |
| B.3 |
| C.5 |
| D.8 |
| B |
|
由第一题已知,需要覆盖的总内存空间是 $192\text{KB}$。 |
I/O 数据传输控制方式知识点
什么是 I/O 数据传输
I/O 是 Input/Output 的缩写,也就是输入输出。计算机不只有 CPU 和内存,还连接着键盘鼠标硬盘网卡打印机等各种外部设备。CPU 和这些外部设备之间交换数据,就叫做 I/O 数据传输。问题在于外部设备的速度远远比 CPU 慢,CPU 需要一种机制来协调和管理这些设备,这就是 I/O 数据传输控制方式。
程序查询方式
CPU 主动不断地去问外部设备"你好了没有,数据准备好了吗",设备没准备好就一直等,准备好了才继续。这就像你在店里等餐,每隔几秒就问服务员"好了吗好了吗",一直等到上菜为止。这种方式 CPU 会被完全占用,什么其他事情都干不了,效率极低。
中断方式
CPU 不去主动等待,而是继续干自己的事情。外部设备准备好了之后,主动发一个信号通知 CPU,这个信号就叫做中断请求。CPU 收到中断请求后,暂停当前正在执行的程序,保存好当前的工作状态(这叫保存现场),然后跳转去处理外部设备的事情。处理完之后,恢复刚才保存的工作状态(这叫恢复现场),回到原来暂停的地方继续执行。
这就像你在专心写作业,手机突然响了(中断请求),你记下写到哪一行了(保存现场),去接电话(处理中断),接完电话回来翻到刚才那一行继续写(恢复现场)。
中断方式让 CPU 不再需要傻等,效率大幅提升,是现代计算机最核心的机制之一。
保存现场的含义
CPU 在执行程序时,各种寄存器里存着当前程序运行到哪里了中间计算结果是多少等关键信息。一旦跳去处理中断,这些信息可能被覆盖掉,回来就不知道刚才做到哪了。所以在跳转之前,必须把这些寄存器的值全部压入栈(一块临时存储区域)保存起来,处理完中断再从栈里取回来,恢复成中断前的状态,这样原程序就能无缝衔接地继续跑下去。
DMA 方式
DMA 是 Direct Memory Access(直接内存访问)的缩写。它引入了一个专门的 DMA 控制器,让外部设备可以直接和内存之间传输大批量数据,整个过程完全不需要 CPU 参与。CPU 只在开始和结束时介入一下,中间全程解放出来干别的事。这就像公司请了一个专职快递员,老板(CPU)只需要说一声"帮我把这批货从仓库搬到车间",剩下的全交给快递员,老板可以去忙其他事。DMA 特别适合大批量数据传输,比如硬盘读写网络数据包收发。
通道方式
通道是比 DMA 更强大的方式,它相当于一个功能简化的小 CPU,能执行一套专门的通道指令,独立管理多个外部设备的数据传输,只在完成整批任务后才通知主 CPU。这种方式用在大型计算机服务器等需要同时管理大量 I/O 设备的场合。
实际应用举例
你按下键盘上的一个键,键盘控制器立刻向 CPU 发出中断请求,CPU 暂停当前任务,跳转到键盘中断处理程序,读取你按的是哪个键,然后回来继续之前的工作。整个过程发生在几微秒内,你完全感觉不到。同样,当你的程序在从硬盘加载一个大文件时,操作系统会启动 DMA 传输,CPU 在此期间可以继续响应鼠标点击播放音乐等其他任务,等 DMA 传完了再发一个中断通知 CPU。
|
计算机运行过程中,遇到突发事件,要求 CPU 暂时停止正在运行的程序,转去为突发事件服务,服务完毕,再自动返回原程序继续执行,这个过程称为( )。 |
| A.阻塞 |
| B.中断 |
| C.动态绑定 |
| D.静态绑定 |
| B |
|
题目描述的完整过程是:正在执行程序 → 遇到突发事件 → 暂停当前程序 → 转去处理事件 → 处理完返回原程序继续。这就是中断机制的标准定义,一字不差。 |
|
中断处理过程中保存现场的目的是( )。 |
| A. 防止丢失数据 |
| B.防止对其他部件造成影响 |
| C.返回去继续执行原程序 |
| D.为中断处理程序提供数据 |
| C |
|
前面知识点里讲过,保存现场就是把 CPU 当前各寄存器的状态(程序运行到哪里了、中间结果是多少等)全部保存起来。保存的目的只有一个:中断处理完之后,能把这些状态全部恢复回去,让原程序从被打断的地方无缝继续跑下去,就好像中断从来没发生过一样。 |
总线系统知识点
什么是总线
总线就是计算机内部各个部件之间传递信息的公共通道,可以把它想象成一条高速公路,CPU内存硬盘显卡等所有部件都连在这条公路上,信息就沿着这条路来回传送。如果没有总线,每两个部件之间都要单独连一根线,部件一多线路就会乱成一团,根本没法管理。总线的好处就是所有部件共享同一组线路,大大简化了连接方式。
三总线结构——数据总线地址总线控制总线
计算机里的总线按照传输信息的类型不同,分成三种,合在一起就叫三总线结构。
第一种是数据总线,负责传输真正的数据内容,比如从内存里读出的数值CPU 计算好的结果等。数据总线是双向的,数据可以从 CPU 送往内存,也可以从内存送往 CPU。数据总线的宽度(有几根线)决定了每次能传多少位数据,比如32位数据总线每次能传32个二进制位。
第二种是地址总线,负责传输地址,告诉其他部件"我要访问的是哪个位置"。比如 CPU 想读内存里某个地址的数据,就先通过地址总线把那个地址发出去。地址总线是单向的,只能从 CPU 发出去,不能反过来。地址总线的宽度决定了 CPU 最多能寻址多大的内存空间,比如32位地址总线最多能访问 $2^{32} = 4\text{GB}$ 的内存。
第三种是控制总线,负责传输控制命令和状态信号,比如"我现在要读数据""我现在要写数据""操作完成了""有中断请求"等。控制总线是双向的,CPU 可以发命令给设备,设备也可以发状态信号给 CPU。
三条总线的分工可以用一个比喻来记
你去图书馆借书:你告诉管理员书架编号(地址总线),管理员把书拿给你(数据总线),你说的是"借"还是"还"(控制总线)。三条线各司其职,缺一不可。
实际应用
你双击打开一个程序,CPU 通过地址总线告诉内存控制器"去读地址 X 的数据",控制总线发出"读"信号,内存控制器把数据通过数据总线送回 CPU。整个过程三条总线协同工作,每秒钟这样的操作要发生几十亿次。现代电脑主板上那些密密麻麻的电路线,很大一部分就是各种总线的物理实现。
|
三总线结构的计算机总线系统由( )组成。 |
| A.CPU总线、内存总线和IO总线 |
| B.数据总线、地址总线和控制总线 |
| C.系统总线、内部总线和外部总线 |
| D.串行总线、并行总线和PCI总线 |
| B |
|
"三总线结构"是计算机组成原理里的一个固定术语,它指的就是数据总线、地址总线和控制总线这三条总线,这是教材里的标准定义,需要直接记住。 |
指令寻址方式及其应用
知识点通俗讲解
在了解“寻址方式”之前,我们先打个通俗的比方。假设CPU(中央处理器)是一个极其忙碌的厨师,“指令”就是菜谱上的一个个步骤,而**“操作数”就是做菜需要的食材**。
所谓**“寻址方式”,其实就是厨师去哪里找食材的方法**。找食材的地方不同,花费的时间自然就不一样。
- 立即寻址(Immediate Addressing):
- 什么意思:指令本身就带着要处理的数据。
- 厨师比喻:菜谱上直接写着“把这包已经贴在菜谱上的盐倒进去”。食材就在步骤说明书上。
- 速度:最快。厨师看菜谱的同时就已经把盐拿在手里了,不需要去任何地方找。
- 寄存器寻址(Register Addressing):
- 什么意思:数据存放在CPU内部的“寄存器”中(寄存器是CPU里极其微小但速度极快的存储空间)。
- 厨师比喻:菜谱上写着“用你围裙口袋里的盐”。寄存器就像是厨师贴身的口袋。
- 速度:很快,排第二。虽然不如直接贴在菜谱上快(因为要伸手去口袋掏一下),但因为口袋就在厨师自己身上,拿取速度依然非常迅速。
- 直接寻址(Direct Addressing):
- 什么意思:指令里只给出了数据在内存(主存)中的地址,CPU必须根据这个地址去内存里把数据取出来。
- 厨师比喻:菜谱上写着“去厨房外面的大仓库(内存)里的第8号架子上拿盐”。
- 速度:最慢。厨师必须停下手头的活,走出厨房,走到仓库,找到架子,拿盐,再走回来。在计算机中,CPU访问外部内存的速度远远慢于它自身内部的处理速度。
实际中的应用
- 软件优化与底层编程:虽然我们平时用高级语言(如PythonJava)写代码时感觉不到这些,但把代码翻译给电脑听的“编译器”却在疯狂计算。为了让程序跑得更快,编译器会尽量把常用的变量放在“寄存器”里(寄存器寻址),或者直接用数字常量(立即寻址),尽量减少去内存里找数据(直接寻址)的次数。
- 选购电脑的参考:这能帮你理解为什么电脑的CPU缓存(相当于比寄存器大一点,比内存快很多的“小仓库”)那么重要。因为去内存(直接寻址)太慢了,CPU需要离自己近的地方来存放数据,减少等待时间。
|
计算机指令系统采用多种寻址方式。立即寻址是指操作数包含在指令中,寄存器寻址是指操作数在寄存器中,直接寻址是指操作数的地址在指令中。这三种寻址方式操作数的速度( )。 |
| A.立即寻址最快,寄存器寻址次之,直接寻址最慢 |
| B.寄存器寻址最快,立即寻址次之,直接寻址最慢 |
| C.直接寻址最快,寄存器寻址次之,立即寻址最慢 |
| D.寄存器寻址最快,直接寻址次之,立即寻址最慢 |
| A |
|
对于零基础的朋友,解这道题的核心在于理解计算机硬件的一个基本常识:距离CPU计算核心越近的地方,速度越快距离越远,速度越慢。 当CPU执行一条指令时,首先要把这条指令读取进来。 在寄存器寻址的情况下,数据存放在CPU内部的寄存器里。寄存器是用最昂贵最快速的材料做成的,且就在CPU内部。CPU读取指令后,指令告诉它“数据在X号寄存器里”,CPU只需要在自己内部“转身”拿一下即可。这虽然多了一个“去寄存器拿”的动作,但因为在CPU内部,所以速度依然极其迅速,位列次之(第二快)。 在直接寻址的情况下,数据存放在计算机的主内存(内存条)中。指令只告诉CPU一个内存地址。此时,CPU必须通过主板上的线路(总线)向内存发送请求,内存找到数据后,再通过线路传回给CPU。相比于CPU内部的闪电速度,去外部内存取数据就像是“跨省物流”,耗时非常长。因此,这种需要访问外部内存的方式是这三种里最慢的。 综上所述,速度的排序是:立即寻址(不用找) > 寄存器寻址(在CPU内部找) > 直接寻址(去外部内存找)。这完全符合选项A的描述。 |
CISC 与 RISC
在计算机处理器(CPU)的设计中,主要分为两大阵营:CISC(复杂指令集) 和 RISC(精简指令集)。我们可以把它们想象成两种不同的“工作流”哲学:
| 比较维度 | CISC (复杂指令集) | RISC (精简指令集) |
|---|---|---|
| 设计哲学 | 大而全,指令功能极强 | 小而精,指令简单高效 |
| 指令长度 | 长度不固定 | 长度通常固定(统一) |
| 指令数量 | 很多,包含复杂指令 | 很少,只有最基本的指令 |
| 寻址方式 | 种类多且复杂 | 种类少且简单 |
| 寄存器 | 数量较少 | 数量较多(为了减少访存) |
| 实现方式 | 主要靠“微程序控制” | 主要靠“硬布线逻辑” |
| 典型代表 | Intel x86 系列(电脑) | ARM 架构(手机平板) |
实际应用中的理解
- CISC(Intel/AMD):就像一把**“瑞士军刀”**,它自带开瓶器剪刀指甲锉等几十种工具。无论遇到什么复杂任务,可能只需要翻出一把特定的多功能工具就能完成。但缺点是工具太复杂,体积大,耗电高。
- RISC(ARM):就像一套**“专业组合工具”**,它只有最基础的锤子螺丝刀。如果你需要完成复杂的家具组装,虽然要多拿几个工具搭配使用,但因为每个工具都轻巧手感极好,整体操作效率反而更高,且极度省电。这也是为什么现在的手机芯片(ARM)比电脑芯片(x86)更省电的原因。
|
以下关于 RISC(精简指令系统计算机)技术的叙述中,错误的是( )。 |
| A.指令长度固定、指令种类尽量少 |
| B.指令功能强大、寻址方式复杂多样 |
| C.增加寄存器数目以减少访存次数 |
| D.用硬布线电路实现指令解码,快速完成指令译码 |
| B |
|
这道题考查的是对 RISC 核心设计思路的理解。我们需要找的是“错误”的描述。 |
流水线技术
知识点通俗讲解
流水线技术(Pipelining)的核心思想是:把一个复杂的任务拆分成几个小步骤,并行处理,以提高效率。
想象一个“汉堡制作流水线”:
- 第 1 步:取指(取材料) —— 耗时 4 分钟
- 第 2 步:分析(处理材料) —— 耗时 2 分钟
- 第 3 步:执行(组装汉堡) —— 耗时 3 分钟
如果不使用流水线,做一个汉堡要 9 分钟,100 个汉堡就要 900 分钟。
但在流水线中,只要第一个汉堡处理完第一步,就可以马上处理第二个汉堡的第一步,不需要等第一个汉堡全部做完。
关键规则:
流水线中,每一个步骤的周期(即速度)必须由这几个步骤里“最慢的那个步骤”来决定。 就像工厂传送带的速度不能超过最慢的工位,否则材料会堆积。在本题中,最慢的步骤是“取指”(4个单位时间)。
实际应用
现代电脑手机的 CPU 都在使用流水线技术。它们之所以能每秒执行几十亿次指令,就是因为这种并行处理机制。如果 CPU 没有流水线,处理器的性能会下降到现在的几十分之一甚至更低。
|
将一条指令的执行过程分解为取指、分析和执行三步,按照流水方式执行,若取指时间为 4Δt,分析时间为 2Δt,执行时间为 3Δt,则执行完 100 条指令,需要的时间为( )Δt。 |
| A.200 |
| B.300 |
| C.400 |
| D.405 |
| D |
|
我们用流水线的计算公式来解决这个问题。 1. 确定流水线的速度(步长): 2. 理解计算公式:
3. 总时间计算: 总结口诀: 因此,正确答案是 D。 |
可靠性:串联与并联的艺术
在计算机系统设计中,“可靠性”指的是系统在规定时间内不出故障的能力。为了让系统更稳固,工程师们主要使用两种结构:
1. 串联模型(一荣俱荣,一损俱损)
- 结构:几个部件像“糖葫芦”一样连在一起。
- 逻辑:只有所有部件都正常工作,系统才正常。如果其中任何一个坏了,整个系统就瘫痪了。
- 计算:总可靠性 = 所有部件可靠性的乘积(因为可靠性R通常小于1,乘得越多,结果越小)。
2. 并联模型(备用机制,互为备份)
- 结构:几个部件像“立交桥”一样并行排列。
- 逻辑:只要有一个部件工作,系统就能跑。只有所有部件同时坏掉,系统才会崩溃。
- 计算:
- 先算“坏”的概率:单个部件坏的概率是 $(1-R)$。
- 并联时所有部件同时坏的概率:$(1-R)^n$ (n是部件数量)。
- 总可靠性 = $1 - (\text{所有部件同时坏掉的概率}) = 1 - (1-R)^n$。
3. 实际应用
- 服务器集群:为了防止网站挂掉,公司会买多台服务器(并联)。只要有一台活着,用户就能访问。
- 航天飞行器:为了防止重要零件坏掉,通常会设计 3 个备份(三模冗余)。因为串联系统极其脆弱,所以关键系统内部往往充斥着各种并联设计。
|
某计算机系统由下图所示的部件构成,假定每个部件的千小时可靠度都为R,则该系统的千小时可靠度为( )。 |
| A.R+2R/4 |
| B.R+R²/4 |
| C.R(1-(1-R)²) |
| D.R(1-(1-R)²)² |
| D |
|
我们看图说话,这个系统由三部分连在一起(串联):
第一步:计算并联结构的可靠性
第二步:计算整个系统的可靠性
将它们乘起来: 对照选项,D 选项写的是 $R(1-(1-R)^2)^2$,这与我们的计算结果完全一致。 因此,正确答案是 D。 |
二、操作系统
操作系统概述
嵌入式系统(比如你微波炉里的芯片汽车的行车电脑智能手表)与普通电脑不同,它通常资源有限,且针对特定任务设计。为了满足不同的需求,它有几个核心特性:
| 特性 | 通俗含义 | 核心逻辑 |
|---|---|---|
| 可定制性 | 像“拼积木”一样 | 根据硬件需求,删减掉不需要的功能,只保留最核心的部分。 |
| 实时性 | 像“急救车”一样 | 系统必须能在规定时间内完成任务,不能有丝毫延迟。 |
| 可靠性 | 像“金刚钻”一样 | 系统必须非常稳定,不能动不动就死机,尤其是在工业或医疗领域。 |
| 易移植性 | 像“变形金刚”一样 | 系统代码能轻松从一种硬件平台搬到另一种硬件平台运行。 |
实际应用中的理解
- 可定制性:比如开发一个简单的电子体温计,它的操作系统不需要视频播放功能,那就在定制时直接去掉。这能极大地节省内存,降低硬件成本。
- 易移植性:比如工程师开发了一款智能灯泡的控制程序,如果公司换了一个更便宜的芯片供应商,他们不需要重新写一遍代码,只需要稍微修改,就能把系统“搬”到新芯片上跑,这大大缩短了产品上市的时间。
|
从减少成本和缩短研发周期考虑,要求嵌入式操作系统能运行在不同的微处理器平台上,能针对硬件变化进行结构与功能上的配置。该要求体现了嵌入式操作系统的( )。 |
| A.可定制性 |
| B.实时性 |
| C.可靠性 |
| D.易移植性 |
| A |
|
这道题考察的是你对嵌入式系统各项能力的辨析。 题目里有两处关键词:
为什么选 A (可定制性) 而不是 D (易移植性)?
在计算机软考等相关标准中,“针对硬件的变化进行结构与功能上的配置”这一句,正是描述嵌入式系统“可定制性”(也叫可裁剪性)的标准术语。因此,A 是更精准的答案。 |
进程与线程:任务的“大家庭”
在操作系统中,“进程”是资源分配的单位,而“线程”是 CPU 调度的单位。为了零基础理解,我们可以把“进程”看作是一个公司,而“线程”就是公司里的员工。
| 资源类别 | 共享情况 | 解释 |
|---|---|---|
| 进程的代码段 | 共享 | 公司的规章制度(代码)是所有员工共同遵守的。 |
| 全局变量 | 共享 | 公司公共区域的办公用品(数据),大家都可以使用。 |
| 栈指针/寄存器 | 私有 | 员工个人的记事本(栈),记录自己当前做到了哪一步,不能让别人看。 |
实际应用中的理解
- 多线程提升效率:比如你打开浏览器(进程),一个线程负责下载图片,一个线程负责解析网页,一个线程负责播放背景音乐。因为它们共享同一个内存空间(全局变量),所以数据交换非常快,这就是为什么现代软件反应灵敏的原因。
- 私有栈的必要性:如果大家共享同一个“记事本”(栈),A 线程写了一半,B 线程跑过来擦掉写上自己的,A 线程回来后就会立刻崩溃(程序出错)。所以,每个线程必须有自己的“专属记事本”。
|
在支持多线程的操作系统中,假设进程 P 创建了 t1、t2、t3 线程,那么( )。 |
| A.该进程的代码段不能被 t1、t2、t3 共享 |
| B.该进程的全局变量只能被共享 |
| C.该进程中 t1、t2、t3 的栈指针不能被共享 |
| D.该进程中 t1 的栈指针可以被 t2、t3 共享 |
| C |
|
我们来逐一分析选项: 选项 A 错误:进程内的代码段是所有线程共同执行的逻辑,它们必须共享,否则就无法协作了。 选项 B 错误:这是一个迷惑选项。虽然全局变量确实是被所有线程共享,但题目说“只能被共享”表达不完整,且 B 选项本身并不是为了描述“线程间特有资源”这一核心考点。 选项 C 正确:这是核心考点。 栈指针(Stack Pointer)是记录线程执行到哪一行代码函数调用轨迹的个人记录。因为每个线程执行的顺序和逻辑不同,所以每个线程必须拥有自己独立的栈空间和栈指针。t1 的栈指针代表 t1 的工作进度,绝对不能被 t2 或 t3 共享,否则会发生严重的运行错误。 选项 D 错误:这与 C 选项直接冲突。t1 的栈指针是它自己的隐私,不能被其他线程共享。 综上所述,因为线程需要各自独立运行,所以线程的“栈”和“栈指针”必须是各自私有的,不能共享。因此选 C。 |
进程管理
在操作系统中,一个进程就像一个人在处理任务,它随时处于以下三种状态之一:
| 状态 | 形象比喻 | 核心逻辑 |
|---|---|---|
| 运行态 | 正在工作 | 进程正在使用 CPU 执行指令。 |
| 就绪态 | 排队等候 | 进程已具备所有运行条件,只需等待 CPU 分配时间即可运行。 |
| 等待态 | 阻塞/休息 | 进程需要等待某个资源(如等打印机扫描仪),即便此时 CPU 有空也无法运行。 |
|
在单处理器系统中,采用先来先服务调度算法。系统中有4个进程 P1、P2、P3、P4(假设进程按此顺序到达),其中 P1 为运行状态,P2 为就绪状态,P3 和 P4 为等待状态,且 P3 等待打印机,P4 等待扫描仪。若 P1( ),则 P1、P2、P3 和 P4 的状态应分别为什么? |
| A.时间片到 |
| B.释放了扫描仪 |
| C.释放了打印机 |
| D.已完成 |
| A |
|
这道题需要通过 P1 的动作来推断系统的状态变化。
|
|
接上题,若 P1(时间片到),则 P1、P2、P3 和 P4 的状态应分别为什么? |
| A.等待、就绪、等待和等待 |
| B.运行、就绪、运行和等待 |
| C.就绪、运行、等待和等待 |
| D.就绪、就绪、等待和运行 |
| C |
|
我们根据进程调度规则来推演:
结论:P1(就绪)P2(运行)P3(等待)P4(等待)。 |
信号量与PV操作
在操作系统中,多个进程常常需要抢夺有限的资源(比如 3 台扫描仪)。为了不发生混乱,系统引入了“信号量(S)”的概念,我们可以把它想象成一个**“资源计数器”**。
| 操作名称 | 符号 | 核心动作 | 形象理解 |
|---|---|---|---|
| P 操作 | P(S) | S = S - 1 | 申请资源:每申请一次,资源数就减 1。 |
| V 操作 | V(S) | S = S + 1 | 释放资源:每归还一次,资源数就加 1。 |
- 如果 S > 0:代表有空闲资源,进程可以直接拿走使用。
- 如果 S < 0:代表资源被抢光了,绝对值 |S| 代表当前正在排队等待的进程个数。
实际应用:想象一个有 3 个座位的自习室。S 的初始值是 3。
- 每进一个人(执行 P 操作),S 变成 210。
- 当 S 为 0 时,第 4 个人再想进来,S 就会变成 -1。这表示现在有 1 个人正在排队等着有人离开。
|
假设系统中有 n 个进程共享 3 台扫描仪,并采用 PV 操作实现进程同步与互斥。若系统信号量 S 的当前值为 -1,进程 P1、P2 又分别执行了 1 次 P(S) 操作,那么信号量 S 的值应为( )。 |
| A.3 |
| B.-3 |
| C.1 |
| D.-1 |
| B |
因此,正确答案是 B。 |
前趋图与PV操作
在计算机系统中,为了让多个进程和谐地“排队”或“并行”执行,我们需要一套规则来管理。前趋图就是描述任务先后顺序的流程图,而 PV 操作则是实现这些顺序的“控制开关”。
| 概念 | 形象理解 | 核心逻辑 |
|---|---|---|
| 前趋图 | 比赛流程图 | 方框代表任务,箭头代表“谁必须在谁之前”完成。 |
| P 操作 | 申请/等待 | 就像“检查接力棒”,没有棒子就停下等待。 |
| V 操作 | 释放/信号 | 就像“交接接力棒”,告诉下一棒“我干完了,你可以开始了”。 |
- 实际应用:例如在手机安装 App 的过程。系统必须先“下载安装包”,再“解压文件”,最后“安装到系统”。这就是一个典型的前趋关系。通过 PV 操作,系统可以确保解压动作一定在下载完成之后才开始,绝不会出错。
|
根据前趋图,若用 PV 操作控制进程 P1~P4 并发执行的过程,需设置 5 个信号量 S1~S5,初值皆为 0。图中 a、b、c 处应分别填写( ); (注:a 在 P1 执行后,b 在 P2 执行前,c 在 P2 执行后,d 在 P3 执行前,e 在 P3 执行后,f 在 P4 执行前)
|
| A.V(S1)V(S2)、P(S1)V(S3)和V(S4) |
| B.P(S1)V(S2)、P(S1)P(S2)和V(S1) |
| C.V(S1)V(S2)、P(S1)P(S3)和V(S4) |
| D.P(S2)、V(S3)V(S5)和P(S4)P(S5) |
| C |
|
a处(P1执行完之后) b处(P2执行之前)
所以需要等两个条件:P(S1)P(S3) c处(P2执行完之后) ✅ 对应选项 C:V(S1)V(S2)P(S1)P(S3)和V(S4) |
|
(上题)d、e 和 f 处应分别填写( )。 |
| A.P(S2)、V(S3)V(S5)和P(S4)P(S5) |
| B.V(S2)、P(S3)V(S5)和V(S4)P(S5) |
| C.P(S2)、V(S3)P(S5)和P(S4)V(S5) |
| D.V(S2)、V(S3)P(S5)和P(S4)V(S5) |
| A |
|
d处(P3执行之前) e处(P3执行完之后)
所以:V(S3)V(S5) f处(P4执行之前)
所以:P(S4)P(S5) ✅ 对应选项 A:P(S2)V(S3)V(S5)和P(S4)P(S5) |
死锁问题
在多进程操作系统中,“死锁”是一种极端的崩溃状态。你可以把它想象成一个**“谁也不让谁”**的交通堵塞。
| 概念 | 形象比喻 | 核心逻辑 |
|---|---|---|
| 互斥资源 | 只有一把钥匙的厕所 | 同一时间只能被一个进程占用。 |
| 死锁 | 你等我手里的钥匙,我等你手里的钥匙 | 几个进程都在无限期地等待对方释放资源,谁也无法继续。 |
| 死锁临界点 | 所有人刚好差一点点就完成 | 当“总资源数”小于“每个进程所需资源之和,且每个人都拿到了一部分资源”时,就会死锁。 |
- 实际应用:在数据库操作中,如果事务 A 锁住了表 1 想访问表 2,同时事务 B 锁住了表 2 想访问表 1,如果不做死锁检测和预防,程序就会彻底“卡死”。为了避免这种情况,操作系统通常会利用“银行家算法”来提前预测:如果你现在把资源分配给进程,会不会导致后续大家都没法完成?如果会,就拒绝分配。
|
某计算机系统中互斥资源 R 的可用数为 8,系统中有 3 个进程 P1、P2 和 P3 竞争 R,且每个进程都需要 i 个 R,该系统可能会发生死锁的最小 i 值为( )。 |
| A.1 |
| B.2 |
| C.3 |
| D.4 |
| D |
|
要发生死锁,必须满足一个前提:大家每个人都占有了一部分资源,且剩下的资源不够满足任何一个人完成任务。
结论:
|
进程资源图:死锁的“诊断书”
进程资源图(Resource Allocation Graph)是用来分析系统是否发生“死锁”的有力工具。
| 概念 | 形象理解 | 核心含义 |
|---|---|---|
| 资源节点 (R) | 像仓库里的设备 | 每个方框里的小圆点代表“可用资源数量”。 |
| 进程节点 (P) | 像正在工作的工人 | 圆圈代表正在请求或占用资源的进程。 |
| 阻塞节点 | 此时干不了活的工人 | 进程当前申请的资源数 > 系统能给的剩余数。 |
| 非阻塞节点 | 此时能干活的工人 | 进程当前申请的资源数 <= 系统能给的剩余数。 |
核心判断规则(化简法):
- 找“非阻塞节点”:如果一个进程申请的资源正好有空余,它就能运行。
- “运行完毕释放资源”:进程运行完会归还资源,原本阻塞的进程可能因此获得资源而变成“非阻塞”。
- 判断死锁:如果在最后,所有进程都变成了“阻塞”状态(化简不完了),那说明系统发生了死锁。如果最后所有进程都能被“化简掉”(运行完毕),则无死锁。
|
在如图所示的进程资源图中,( )。 |
| A.P1、P2、P3 都是非阻塞节点,该图可以化简,所以是非死锁的 |
| B.P1、P2、P3 都是阻塞节点,该图不可以化简,所以是死锁的 |
| C.P1、P2 是非阻塞节点,P3 是阻塞节点,该图不可以化简,所以是死锁的 |
| D.P2 是阻塞节点,P1、P3 是非阻塞节点,该图可以化简,所以是非死锁的 |
| D |
(注:实际考试中,根据化简法,P1P2 可运行释放资源给 P3,所以该图是可以化简的,属于非死锁状态。选项 D 准确描述了化简过程和非死锁结论。) |
页式存储
页式存储管理是操作系统中将内存“化整为零”的核心技术,通过页表将逻辑地址映射到物理内存。页表中的关键标志位定义如下:
| 标志位 | 含义 (0 / 1) | 作用 |
|---|---|---|
| 状态位 | 0: 不在内存 / 1: 在内存 | 指示该页面当前是否已加载至物理内存中。 |
| 访问位 | 0: 未访问过 / 1: 访问过 | 记录页面近期使用情况,辅助页面置换算法。 |
| 修改位 | 0: 未修改过 / 1: 修改过 | 决定置换时是否需要将数据写回硬盘。 |
|
某计算机系统页面大小为 4K,进程的页面变换表如下(已旋转)。若进程的逻辑地址为 2D16H,该地址经过变换后,其物理地址应为( )。 |
| A.2048H |
| B.4096H |
| C.4D16H |
| D.6D16H |
| A |
|
|
|
进程 P 有 5 个页面,系统分配了 3 个存储块。若访问页面 3 不在内存,此时状态表如下,应优先淘汰的页号为( )。
|
| A.0 |
| B.1 |
| C.2 |
| D.4 |
| A |
|
段式存储
在计算机内存管理中,如果说“页式存储”是把程序无情地切成大小相等的“豆腐块”,那么“段式存储”就是按照程序的自然逻辑(比如主程序子函数数据区)切分成大小不一的“段落”。
| 存储方式 | 形象比喻 | 核心逻辑 | 越界检查规则 |
|---|---|---|---|
| 页式存储 | 把书强制每100页切一刀 | 每一块大小固定 | 只要页号存在,页内偏移量绝对不会超标。 |
| 段式存储 | 把书按“章节”撕开(大小不一) | 每一段大小不固定 | 寻找内容时,给定的“偏移量”绝对不能超过该段的“段长”。 |
- 实际应用:段式存储非常重要,它能确保“数据段”不会越界跑到“代码段”里去执行,防止程序因为非法访问内存而崩溃(即经典的“段错误”)。
|
设某进程的段表如下,逻辑地址( )可以转换为对应的物理地址。
|
| A.(0, 1597)、(1, 30) 和 (3, 1390) |
| B.(0, 128)、(1, 30) 和 (3, 1390) |
| C.(0, 1597)、(2, 98) 和 (3, 1390) |
| D.(0, 128)、(2, 98) 和 (4, 1066) |
| B |
因为选项 B 中的三个地址全部符合偏移量小于段长的规则,所以它们均可转换为对应的物理地址。故选 B。 |
段页式存储
段页式存储是操作系统中一种“折中”的内存管理方案。它综合了“段式存储”逻辑结构清晰和“页式存储”内存利用率高的优点。
| 存储技术 | 逻辑结构 | 物理结构 | 核心特点 |
|---|---|---|---|
| 段式 | 按功能分段(大小不等) | 连续存储 | 逻辑清晰,利于共享和保护 |
| 页式 | 固定大小划分(大小相等) | 离散存储 | 内存利用率高,无外部碎片 |
| 段页式 | 先分段,再分页 | 页式存储 | 结合二者优点,但管理成本最高 |
- 实际应用:现代计算机操作系统(如 Linux 在 x86 架构下的实现)大多采用段页式管理。它既满足了程序员从逻辑上划分程序(代码段数据段)的需求,又解决了物理内存中大段连续空间难以申请的问题,是性能与灵活性平衡的结果。
|
假设段页式存储管理系统中的地址结构如下图所示,则系统( )。 |
| A.最多可有 256 个段,每个段的大小均为 2048 个页,页的大小为 8K |
| B.最多可有 256 个段,每个段最大允许有 2048 个页,页的大小为 8K |
| C.最多可有 512 个段,每个段的大小均为 1024 个页,页的大小为 4K |
| D.最多可有 512 个段,每个段最大允许有 1024 个页,页的大小为 4K |
| B |
|
要解决这道题,我们只需要从地址结构的“位数”出发进行简单的 2 次幂计算:
结论对比:
故正确答案为 B。 |
磁盘管理与调度算法
磁盘调度算法旨在通过合理的磁头移动路径,尽可能降低寻道时间,提高系统磁盘读写性能。
| 算法名称 | 核心逻辑 | 特点 |
|---|---|---|
| 先来先服务 (FCFS) | 按照请求到达的先后顺序调度 | 公平,但磁头可能来回“大幅度摆动”,效率低。 |
| 最短寻道优先 (SSTF) | 优先处理离当前位置最近的柱面 | 寻道效率高,但可能导致远处的请求产生“饥饿”。 |
| 扫描算法 (SCAN) | 像电梯一样,一端走到另一端 | 避免了极端饥饿,性能较均衡。 |
| 循环扫描 (CSCAN) | 磁头单向移动,到头后瞬间回到起点 | 响应时间更加均匀。 |
|
某磁盘磁头从一个磁道移至另一个磁道需 10ms。逻辑上相邻数据块平均移动距离为 10 个磁道,每块的旋转延迟与传输时间分别为 100ms 和 2ms,读取一个 100 块的文件需要( )ms。 |
| A.10200 |
| B.11000 |
| C.11200 |
| D.20200 |
| D |
|
|
假设磁盘臂位于 15 号柱面,进程请求序列如下表所示,采用“最短寻道优先 (SSTF)”算法,系统响应序列为( )。 |
| A.①②③④⑤⑥ |
| B.⑤①②④③⑥ |
| C.②③④⑤①⑥ |
| D.④②③⑤①⑥ |
| B |
|
设备管理
在计算机系统中,设备管理负责协调操作系统与各种硬件(鼠标硬盘打印机等)之间的通信。为了简化操作,管理软件被分成了四个层次,每一层都像是一个专门处理不同任务的“翻译官”。
| 层次 | 名称 | 核心功能 |
|---|---|---|
| 第1层 | 与设备无关的系统软件 | 屏蔽底层硬件细节,提供统一的 IO 调用接口。 |
| 第2层 | 设备驱动程序 | 将通用的 IO 指令翻译成特定硬件能执行的控制信号。 |
| 第3层 | 中断处理程序 | 硬件完成任务后发出“中断”信号,通知 CPU 任务已结束。 |
| 第4层 | 硬件 | 物理设备本身(如键盘硬盘)。 |
- 实际应用:当你编写一个程序读取文件时,你不需要知道硬盘是哪个厂家生产的(这归功于第1层)操作系统调用硬盘驱动程序来发送读扇区的指令(第2层)当数据读完,硬件会触发中断,告诉系统可以读取了(第3层),最后硬件完成动作。
|
I/O 设备管理软件一般分为 4 个层次,图中 ①、②、③ 分别对应( )。 |
| A.设备驱动程序、虚设备管理、与设备无关的系统软件 |
| B.设备驱动程序、与设备无关的系统软件、虚设备管理 |
| C.与设备无关的系统软件、中断处理程序、设备驱动程序 |
| D.与设备无关的系统软件、设备驱动程序、中断处理程序 |
| D |
|
文件相关概念
在操作系统中,文件管理系统负责在磁盘上组织和查找数据。为了高效管理这些数据,系统不仅存“用户内容”,还要存“管理信息”。
| 概念 | 形象理解 | 核心作用 | 对系统的重要性 |
|---|---|---|---|
| 目录 (Directory) | 图书馆的索引卡片 | 记录文件名在哪多大存放在磁盘的哪些块中 | 极高:它是系统的“地图” |
| 空闲块表 | 仓库管理员的库存单 | 记录磁盘哪些地方是空的,可以放新文件 | 中等:丢失会导致空间浪费 |
| 用户数据 | 书本里的正文内容 | 真正被用户读写的信息 | 低:仅影响该文件本身 |
- 实际应用:当你删除一个文件时,操作系统通常只是把该文件在“目录”里的索引删掉了,并把对应的“空闲块”标记为可用。如果由于断电导致“目录”文件损坏,整个磁盘的布局就会错乱,这就是为什么系统奔溃时目录结构受损会导致系统无法启动的原因。
|
若系统在将( )文件修改的结果写回磁盘时发生崩溃,则对系统的影响相对较大。 |
| A.目录 |
| B.空闲块 |
| C.用户程序 |
| D.用户数据 |
| A |
|
完整解析:
|
树形目录结构
一知识点
| 知识点 | 说明 | 示例 |
|---|---|---|
| 根目录 | 文件系统的起点,用 / 表示 |
/ |
| 目录(文件夹) | 用于存放文件或子目录,形成层级关系 | /swtools |
| 文件 | 存储实际数据的实体 | rw.dll |
| 绝对路径 | 从根目录 / 开始描述文件位置 |
/swtools/flash/rw.dll |
| 相对路径 | 从当前工作目录出发描述文件位置 | flash/rw.dll |
| 当前工作目录 | 用户当前所在的目录 | swtools |
| 父目录符号 | .. 表示返回上一级目录 |
../rempl/file.txt |
二实际应用
| 应用场景 | 说明 |
|---|---|
| 操作系统文件管理 | 通过路径快速定位文件 |
| 软件开发 | 引用配置文件资源文件等 |
| 命令行操作 | 使用 cdcprm 等命令 |
| Web开发 | 资源文件(如图片脚本)的路径引用 |
| 系统维护 | 日志文件和库文件的管理 |
|
如图所示目录结构,若要访问文件
|
A.rw.dll |
B.flash/rw.dll |
C./swtools/rempl/rw.dll |
D./Programe file/swtools/rw.dll |
| C |
|
|
若当前工作目录为 |
A./flash/,/swtools/flash/ |
B.flash/ ,/swtools/flash/ |
C.swtools/flash/,/flash/ |
D./swtools/flash,/flash/ |
| B |
|
索引文件结构
什么是文件的存储问题
文件存在磁盘上,磁盘被切成一块一块固定大小的"数据块"来管理,这道题里每块是 1KB。一个文件的内容可能很大,需要占用很多块,这些块不一定是连续存放的,可能分散在磁盘各处。那么操作系统怎么知道一个文件的数据分别存在哪些块上呢?答案就是索引节点。
什么是索引节点(inode)
每个文件都有一个对应的索引节点,里面存的不是文件内容,而是这个文件的"地图"——记录着文件数据分别存在磁盘的哪些块上。这个地图是通过若干个地址项来实现的,每个地址项存一个磁盘块的地址(编号)。这道题里每个地址项占 4 字节,一共有 8 个地址项 iaddr[0]~iaddr[7]。
逻辑块号和地址项的对应关系
文件的数据块从用户角度按顺序编号,叫逻辑块号,从 0 开始。逻辑块号和 iaddr 地址项一一对应:逻辑块号 0 对应 iaddr[0],逻辑块号 1 对应 iaddr[1],以此类推。
三种寻址方式
| 方式 | 寻址过程 | 每个地址项能覆盖的块数 |
|---|---|---|
| 直接地址索引 | 地址项 → 数据块,一步到位 | 1块 |
| 一级间接地址索引 | 地址项 → 索引块 → 数据块,两步 | 256块(一个索引块能存 $1024\div4=256$ 个地址) |
| 二级间接地址索引 | 地址项 → 一级索引块 → 二级索引块 → 数据块,三步 | $256\times256=65536$ 块 |
一个 1KB 的索引块能存多少个地址:$1024 \div 4 = 256$ 个,这个数字非常关键,后面计算都要用到。
实际应用
Linux 的 ext2/ext3/ext4 文件系统就采用这种 inode 结构。小文件(比如几KB的配置文件)只用直接索引,访问速度极快。大文件(比如视频)会用到间接索引,多花一两次磁盘读取来查索引块,但能支持几十GB的超大文件。你在 Linux 终端输入 stat 文件名 就能看到文件的 inode 信息。
|
某文件系统采用索引节点管理,其磁盘索引块和磁盘数据块大小均为 1KB 字节且每个文件索引节点有 8 个地址项 iaddr[0]~iaddr[7],每个地址项大小为 4 字节,其中 iaddr[0]~iaddr[4] 采用直接地址索引,iaddr[5] 和 iaddr[6] 采用一级间接地址索引,iaddr[7] 采用二级间接地址索引。若用户要访问文件 userA 中逻辑块号为 4 和 5 的信息,则系统应分别采用( )。 |
| A.直接地址访问和直接地址访问 |
| B.直接地址访问和一级间接地址访问 |
| C.一级间接地址访问和一级间接地址访问 |
| D.一级间接地址访问和二级间接地址访问 |
| B |
|
这道题只需要搞清楚一件事:逻辑块号 4 和逻辑块号 5 分别对应哪个 iaddr 地址项,然后看那个地址项用的是什么索引方式。 逻辑块号从 0 开始,iaddr 也从 0 开始,两者直接对应。逻辑块号 4 就对应 iaddr[4],逻辑块号 5 就对应 iaddr[5]。 题目告诉我们 iaddr[0]~iaddr[4] 用的是直接地址索引。iaddr[4] 在这个范围里,所以访问逻辑块 4 用的是直接地址访问,iaddr[4] 里存的就是数据块的地址,直接一步拿到数据。 题目告诉我们 iaddr[5] 和 iaddr[6] 用的是一级间接地址索引。iaddr[5] 用的就是一级间接,所以访问逻辑块 5 要先通过 iaddr[5] 找到一个索引块,再从索引块里找到真正存数据的那个块,需要两步。 一个用直接,一个用一级间接,对应选项 B。 答案是 B,直接地址访问和一级间接地址访问。 |
|
该文件系统可表示的单个文件最大长度是( )KB。 |
| A.517 |
| B.1029 |
| C.65797 |
| D.66053 |
| D |
|
把所有 8 个地址项全部用满,能指向的数据块总数就是文件的最大大小,因为每块是 1KB,块数等于 KB 数。分三部分来算。 先算好一个基础数字:一个 1KB 的索引块能存多少个地址项,$1024 \div 4 = 256$ 个,记住这个 256,后面要反复用到。 第一部分是 iaddr[0]~iaddr[4],共 5 个直接地址项,每个直接对应 1 个数据块: $$5 \times 1 = 5 \text{ 块}$$ 第二部分是 iaddr[5] 和 iaddr[6],共 2 个一级间接地址项,每个先指向一个索引块,索引块里有 256 个地址,每个地址对应 1 个数据块: $$2 \times 256 = 512 \text{ 块}$$ 第三部分是 iaddr[7],共 1 个二级间接地址项,先指向一个一级索引块(有 256 个地址),一级索引块里每个地址再指向一个二级索引块(又有 256 个地址),二级索引块里每个地址才对应 1 个数据块: $$1 \times 256 \times 256 = 65536 \text{ 块}$$ 三部分加总: $$5 + 512 + 65536 = 66053 \text{ KB}$$ 对照选项,66053 就是选项 D。 答案是 D,66053KB。 |
位示图法
什么是位示图
磁盘上有成千上万个物理块,操作系统需要随时知道哪些块是空闲的哪些块已经被占用。位示图就是专门用来记录这件事的一张"地图"。
它的原理非常简单:用一个二进制位(0或1)对应一个物理块,0 表示该块空闲,1 表示该块已占用。所有物理块的状态连在一起,就形成了一张位示图,存在磁盘的固定位置上。
字和位的概念
位示图里的二进制位不是一个个单独存的,而是按"字"来组织的。一个字有固定的位数,这道题里每个字是 32 位。字从 0 开始编号,每个字里的位也从 0 开始编号。
| 概念 | 说明 |
|---|---|
| 字长 | 每个字有多少位,这题是 32 位 |
| 字号 | 第几个字,从 0 开始编 |
| 位号 | 在这个字里是第几位,从 0 开始编 |
| 物理块号 | 磁盘上第几个块,从 0 开始编 |
物理块号和字号位号的换算
已知物理块号,求它在位示图里第几个字的第几位:
$$\text{字号} = \left\lfloor \frac{\text{物理块号}}{\text{字长}} \right\rfloor$$
$$\text{位号} = \text{物理块号} \mod \text{字长}$$
反过来,已知字号和位号,求物理块号:
$$\text{物理块号} = \text{字号} \times \text{字长} + \text{位号}$$
实际应用
Linux 的 ext 文件系统用块位图(block bitmap)来管理磁盘空闲块,Windows 的 NTFS 也有类似的位图结构。每次新建文件需要分配磁盘空间时,操作系统就扫描位图找到值为 0 的位,把对应的物理块分配出去,并把该位改为 1。删除文件时则把对应位改回 0,表示空闲。位示图结构紧凑查找效率高,是最常用的磁盘空间管理方法之一。
|
某文件管理系统在磁盘上建立了位示图(bitmap),记录磁盘的使用情况。若磁盘上物理块的编号依次为 0、1、2、……;系统中的字长为 32 位,位示图中字的编号依次为 0、1、2、……,每个字的一个二进制位对应文件存储器上的一个物理块,取值 0 和 1 分别表示物理块是空闲或占用。假设操作系统将 2053 号物理块分配给某文件,那么该物理块的使用情况在位示图中编号为( )的字中描述。 |
| A.32 |
| B.33 |
| C.64 |
| D.65 |
| C |
|
这道题只需要一步计算:把物理块号 2053 除以字长 32,商就是字号。 $$\text{字号} = \left\lfloor \frac{2053}{32} \right\rfloor$$ 做除法:$32 \times 64 = 2048$,$2053 - 2048 = 5$,所以 $2053 \div 32 = 64$ 余 $5$。 商是 64,余数是 5,说明 2053 号物理块在位示图的第 64 号字里,是这个字的第 5 位(位号从0开始)。 对照选项,答案是 64,即选项 C。 顺便验证一下余数的含义:第 64 号字负责第 $64 \times 32 = 2048$ 号到第 $64 \times 32 + 31 = 2079$ 号这 32 个物理块,2053 号恰好在这个范围里($2053 - 2048 = 5$,是这个字里的第 5 位),完全正确。 答案是 C,第 64 号字。 |
三、程序设计语言基础
编译程序与解释程序
什么是高级语言的翻译问题
我们写的 CPython 这类代码,CPU 完全看不懂,CPU 只认识二进制的机器指令。所以必须有一个"翻译官"把人写的代码翻译成机器能执行的指令。这个翻译官有两种工作模式,一种叫编译器,一种叫解释器。
编译器的工作方式
编译器的做法是:先把整个源程序完整地翻译一遍,生成一个独立的目标程序文件,然后每次运行直接执行那个目标程序,编译器本身不再参与。就像把一本中文书完整翻译成英文书,翻译完之后英文读者直接读英文书,不需要翻译官再在场。
解释器的工作方式
解释器的做法是:不提前翻译整个程序,而是每次运行时一行一行地读源代码,读一句翻译一句立刻执行,解释器全程参与。就像现场口译,翻译官必须一直在场,说一句翻一句。
两者的核心区别
| 对比项 | 编译型语言(如C) | 解释型语言(如Python) |
|---|---|---|
| 翻译程序 | 编译器 | 解释器 |
| 是否生成目标代码 | 生成目标代码 | 不生成目标代码 |
| 执行方式 | 先编译后执行 | 边解释边执行 |
| 翻译程序是否参与执行 | 编译器不参与执行 | 解释器全程参与 |
| 执行效率 | 高 | 低 |
| 可移植性 | 差(目标代码依赖平台) | 强(源代码跨平台) |
共同点
两者都是高级程序语言的翻译工具,翻译过程都包含词法分析语法分析语义分析三个阶段。
实际应用
C/C++ 是典型的编译型语言,代码编译后生成 .exe 或 .out 文件,运行速度极快,常用于操作系统游戏引擎等对性能要求高的场合。PythonJavaScript 是典型的解释型语言,代码可以直接在任何装了解释器的机器上跑,不需要为每个平台单独编译,灵活性和可移植性强,常用于脚本数据分析网页开发。
|
编译器与解释器是程序语言翻译的两种基本形态,以下关于编译器工作方式及特点的叙述中,正确的是( )。 |
| A.边翻译边执行,用户程序运行效率低且可移植性差 |
| B.先翻译后执行,用户程序运行效率高且可移植性好 |
| C.边翻译边执行,用户程序运行效率低但可移植性好 |
| D.先翻译后执行,用户程序运行效率高但可移植性差 |
| D |
|
题目问的是编译器,不是解释器,要特别注意这一点。 先确定编译器的执行方式。编译器是先把整个源程序完整翻译成目标代码,再执行那个目标代码,整个过程是"先翻译,后执行",绝对不是边翻译边执行。这一条就排除了选项 A 和选项 C,因为它们都说"边翻译边执行",那是解释器的特征,不是编译器的。 再看剩下的 B 和 D,执行方式都是"先翻译后执行",正确。区别在于可移植性。 编译器生成的目标代码是针对特定平台(特定 CPU特定操作系统)的机器指令,换一个平台就跑不了,必须重新编译。所以编译型语言可移植性差。 选项 B 说"可移植性好",这是错的,排除。 选项 D 说"运行效率高但可移植性差",运行效率高是对的(因为目标代码直接是机器指令,不需要再翻译),可移植性差也是对的,完全符合编译器的特征。 答案是 D,先翻译后执行,用户程序运行效率高但可移植性差。 |
|
若 C 程序的表达式中引用了未赋初值的变量,则( )。 |
| A.编译时一定会报告错误信息,该程序不能运行 |
| B.可以通过编译并运行,但运行时一定会报告异常 |
| C.可以通过编译,但链接时一定会报告错误信息而不能运行 |
| D.可以通过编译并运行,但运行结果不一定是期望的结果 |
| D |
|
这道题考查的是 C 语言编译器对未赋初值变量的处理机制。 在 C 语言里,使用未赋初值的局部变量是一个合法的语法行为,编译器不会把它当成语法错误,顶多给一个警告(warning),而不是错误(error),所以程序可以正常通过编译,排除选项 A 和选项 C。 程序通过编译后也可以正常链接和运行,不会在运行时抛出异常(C 语言不像 Java 那样有严格的运行时检查机制),排除选项 B。 真正的问题是:未赋初值的局部变量在内存里存的是一个随机的垃圾值,是之前那块内存里残留的任意数据。程序拿这个垃圾值去参与计算,逻辑上就出错了,结果自然不是你期望的。但程序本身不会崩溃也不会报异常,就这么悄悄算出一个错误的结果。 答案是 D,可以通过编译并运行,但运行结果不一定是期望的结果。 |
常见程序设计语言的特点
程序设计语言的分类
程序设计语言按照发展历史和抽象程度,从低到高分为机器语言汇编语言和高级语言三大类。高级语言里又细分出很多种类,脚本语言是其中一种。
| 类别 | 代表 | 特点 |
|---|---|---|
| 机器语言 | 二进制指令 | CPU直接执行,速度最快,难以编写 |
| 汇编语言 | x86汇编 | 用助记符代替二进制,仍然依赖硬件平台 |
| 高级语言 | CJavaPython | 接近人类语言,需要编译或解释后才能执行 |
脚本语言是什么
脚本语言是高级语言的一个子类,专门用来编写自动化任务快速处理数据控制其他程序等轻量级工作的语言。它的核心特征有以下几点。
第一,脚本语言主要采用解释方式实现,也就是边读代码边执行,不需要提前编译成目标程序,写完直接运行,非常方便。
第二,脚本语言不是通用程序设计语言。通用语言(比如 CJava)适合开发各类软件,脚本语言更擅长特定场合,比如网页交互系统自动化数据处理等。
第三,脚本语言不适合系统级程序开发。系统级程序(比如操作系统驱动程序)需要直接操作硬件追求极致性能,这是 C/C++ 等编译型语言的领域,脚本语言执行效率低,不适合。
第四,脚本语言完全可以定义和调用函数。PythonJavaScript 里定义函数是最基本的功能,这一点不存在限制。
常见脚本语言及应用场景
| 脚本语言 | 典型应用 |
|---|---|
| Python | 数据分析人工智能自动化脚本 |
| JavaScript | 网页前端交互浏览器动态效果 |
| Shell/Bash | Linux 系统自动化运维 |
| PHP | 网页后端开发 |
| Lua | 游戏逻辑脚本(如《魔兽世界》插件) |
实际应用举例
你在网页上点击一个按钮弹出提示框,背后执行的就是 JavaScript 脚本。运维工程师每天凌晨自动备份服务器数据,靠的是 Shell 脚本。数据科学家用几十行 Python 脚本就能完成几千行 Excel 数据的清洗和分析。这些场合都不需要编译,写完就能跑,这正是脚本语言的优势所在。
|
以下关于脚本语言的叙述中,正确的是( )。 |
| A.脚本语言是通用的程序设计语言 |
| B.脚本语言更适合应用在系统级程序开发中 |
| C.脚本语言主要采用解释方式实现 |
| D.脚本语言中不能定义函数和调用函数 |
| C |
|
逐个分析每个选项。 选项 A 说脚本语言是通用的程序设计语言,这是错的。通用程序设计语言指的是像 CJava 这类什么类型的软件都能开发的语言。脚本语言是专门面向特定场景的,比如自动化任务网页交互等,不是通用语言,因此 A 错误。 选项 B 说脚本语言更适合系统级程序开发,这也是错的。系统级程序(比如操作系统内核硬件驱动)需要直接控制硬件追求极高的运行速度,必须用 C/C++ 这类能生成高效目标代码的编译型语言。脚本语言用解释方式运行,效率低,完全不适合这种场合,因此 B 错误。 选项 C 说脚本语言主要采用解释方式实现,这是正确的。脚本语言的最大特征就是不需要预先编译,由解释器直接读取源代码逐行执行,PythonJavaScriptShell 都是这样工作的,因此 C 正确。 选项 D 说脚本语言中不能定义函数和调用函数,这是明显错误的。Python 用 答案是 C,脚本语言主要采用解释方式实现。 |
|
下图所示为一个不确定有限自动机(NFA)的状态转换图。该 NFA 可识别字符串( )。
|
| A.0110 |
| B.0101 |
| C.100 |
| D.1010 |
| A |
|
先把 NFA 的转换关系理清楚。从图中可以读出所有转换: 状态 0 读入字符 0 → 到状态 1 状态 1 通过 ε(不消耗字符)→ 到状态 2 状态 2 读入字符 1 → 到状态 3 状态 3 通过 ε → 到状态 4(向前走) 状态 3 通过 ε → 回到状态 2(向后回,图中底部的 ε 箭头) 状态 4 读入字符 0 → 到状态 5(接受状态) ε 转换不消耗任何字符,可以自由跳转。关键在于状态 3 可以通过 ε 回到状态 2,这意味着可以重复读入多个 1。 现在逐个验证四个选项: 验证选项 C(1100): 初始在状态 0,第一个字符是 1。状态 0 只有读入 0 才能转移,读入 1 无路可走,直接失败。排除 C。 验证选项 D(1010): 同样第一个字符是 1,状态 0 无法处理,失败。排除 D。 验证选项 B(0101): 状态 0 读 0 → 状态 1,ε → 状态 2,读 1 → 状态 3。 现在需要读 0,状态 3 只有 ε 转换(去状态 4 或回状态 2)。 走 ε 到状态 4,再读 0 → 状态 5(接受状态)。 但此时字符串还剩最后一个 1 没读,而状态 5 是接受状态,没有任何出边,无法继续读 1,失败。排除 B。 验证选项 A(0110): 状态 0 读 0 → 状态 1 ε → 状态 2 读第一个 1 → 状态 3 此时利用状态 3 的 ε 回边,回到状态 2 读第二个 1 → 状态 3 ε → 状态 4 读 0 → 状态 5(接受状态)✓ 整个字符串 0110 恰好读完,停在接受状态 5,识别成功。 答案是 A,0110。 |
程序设计语言的基本成分
高级程序设计语言的四大成分
通用的高级程序设计语言一般都提供四类语言成分,分别是数据运算控制和数据传输。这四类成分缺一不可,共同构成一门完整的编程语言。
| 成分 | 作用 | 举例 |
|---|---|---|
| 数据 | 描述程序要处理的信息,包括数据类型和数据结构 | 整数浮点数字符串数组结构体 |
| 运算 | 对数据进行操作和计算 | 加减乘除比较大小逻辑与或非 |
| 控制 | 决定程序执行的流程和顺序 | 顺序选择循环三种结构 |
| 数据传输 | 在程序内部或程序与外界之间传递数据 | 赋值语句输入输出函数参数传递 |
控制成分的三种结构
控制成分是最核心的考点,任何程序的执行流程都由且仅由以下三种基本控制结构组成,这是结构化程序设计的理论基础。
|
通用的高级程序设计语言一般都会提供描述数据、运算、控制和数据传输的语言成分,其中,控制包括顺序、( )和循环结构。 |
| A.选择 |
| B.递归 |
| C.递推 |
| D.函数 |
| A |
|
这道题考的是结构化程序设计里控制结构的标准分类,属于需要直接记住的知识点。 结构化程序设计理论证明,任何程序的控制流程都可以用三种基本结构来表达,分别是顺序结构选择结构和循环结构,这三个是固定搭配,缺一不可,也不需要其他结构来补充。 题目里已经给出了顺序和循环,空出来的那一个就是选择结构,直接选 A。 再排除其他三个选项。递归是函数调用自身的一种编程技巧,比如用函数计算阶乘时自己调自己,它本质上是函数调用的一种形式,不属于基本控制结构,因此 B 错误。递推是一种解题思路,比如从已知的前几项推出后面的项,它是算法设计的方法,不是控制结构,因此 C 错误。函数是数据传输和代码复用的机制,属于语言成分里的另一类,不属于控制结构,因此 D 错误。 答案是 A,选择。 |
函数调用方式
什么是参数传递
调用函数时,需要把数据从调用方传给被调函数,传递方式不同,函数内部对参数的修改是否影响外部变量的结果也不同。最常见的两种方式是传值和传引用。
传值(Call by Value)
把实参的值复制一份给函数的形参。函数内部操作的是那份拷贝,不管函数里怎么修改形参,原来的实参变量完全不受影响。就像把你的文件复印一份给别人,别人在复印件上涂改,你的原件不变。
传引用(Call by Reference)
把实参变量的地址传给函数,函数内部直接操作原变量本身。函数里对形参的任何修改,都会直接反映到原来的实参变量上。就像直接把你的原件给别人,别人在上面改,你的原件就真的变了。
两种方式的对比
| 对比项 | 传值 | 传引用 |
|---|---|---|
| 传递的内容 | 实参的值的拷贝 | 实参变量的地址 |
| 函数内修改是否影响原变量 | 不影响 | 影响 |
| C语言写法 | int x |
int &a(C++)或指针 |
| 适用场景 | 只需要读取数据 | 需要修改调用方的变量 |
实际应用
swap 交换函数是最经典的例子。如果用传值,函数内部交换了两个局部变量,函数结束后调用方的两个变量根本没变,交换失败。必须用传引用,才能真正交换调用方的变量。
|
函数 f()、h() 定义如下,调用函数 h() 时,第一个参数采用传值(call by value)方式,第二个参数采用传引用(call by reference)方式。设有函数调用 f(5),那么 `printf(x)` 执行后输出的值为( )。
|
| A.24 |
| B.25 |
| C.30 |
| D.36 |
| A |
|
这道题要一步一步跟着程序执行,分清楚哪个变量是谁的,哪个会被修改。 第一步,调用 f(5),进入 f 函数体。args 接收到值 5,然后执行 第二步,执行 进入 h 函数体,h 有自己的局部变量 x(形参),还有一个引用参数 a。 传值的结果是:h 自己的局部变量 x 得到了 args 的值的拷贝,即 h 的 x = 5。注意这个 x 是 h 的局部变量,和 f 里的 x 是两个完全不同的变量,改它不影响 f 的 x。 传引用的结果是:h 的参数 a 直接绑定到 f 里的变量 x 上,a 就是 f 里 x 的别名,对 a 的任何修改都直接修改 f 里的 x。 第三步,在 h 函数体内执行 第四步,执行 第五步,h 函数 return,回到 f 函数体。此时 f 里的 x 已经被 h 通过引用改成了 24。 第六步,执行 答案是 A,24。 |
编译过程概述
编译器的六个阶段
编译器把源程序翻译成目标代码,整个过程按顺序经历六个阶段:
每个阶段做什么
词法分析是第一步,把源程序的字符流切分成一个个有意义的基本单词,比如关键字 int变量名 x运算符 + 等,叫做 Token。
语法分析是第二步,检查这些 Token 的排列组合是否符合语法规则,比如 if 后面必须跟条件表达式,缺了括号就报语法错误。
语义分析是第三步,在语法正确的基础上进一步检查含义是否合理,比如把一个整数赋值给字符串类型变量,语法上可能没问题,但语义上是错的。
中间代码生成是第四步,把源程序翻译成一种介于高级语言和机器语言之间的中间表示,方便后续优化和跨平台处理。
代码优化是第五步,对中间代码进行改进,让生成的目标代码运行更快占用内存更少。这一步是可选的,不是每个编译器都必须做。
目标代码生成是第六步,把中间代码(或优化后的中间代码)翻译成特定平台的机器指令,生成最终的目标程序。
哪些阶段是可选的
中间代码生成和代码优化这两个阶段不是每个编译器都必须有的。简单的编译器可以直接从语义分析跳到目标代码生成,跳过中间代码和优化步骤。而词法分析语法分析语义分析目标代码生成这四步是每个编译器都必须有的。
解释器和编译器的区别
解释器不生成目标代码,而是直接读源程序边解释边执行,整个运行过程中解释器都参与其中。编译器生成目标代码之后就退出了,后续运行不需要编译器再参与。
| 对比项 | 编译器 | 解释器 |
|---|---|---|
| 是否生成目标代码 | 是 | 否 |
| 是否参与运行控制 | 不参与 | 参与 |
| 程序执行速度 | 快 | 慢 |
| 可移植性 | 差 | 好 |
实际应用
GCC 是最常见的 C 语言编译器,你写的 .c 文件经过它的六个阶段最终变成可执行文件。编译时加 -O2 参数就是开启代码优化阶段,生成的程序会更快。Python 解释器运行 .py 文件时全程参与,所以 Python 程序一般比 C 程序慢很多。
|
编译器对高级语言源程序的处理过程可以划分为词法分析、语法分析、语义分析、中间代码生成、代码优化、目标代码生成等阶段,其中( )并不是每个编译器都必需的。 |
| A.词法分析和语法分析 |
| B.语义分析和中间代码生成 |
| C.中间代码生成和代码优化 |
| D.代码优化和目标代码生成 |
| C |
|
这道题问的是哪两个阶段不是每个编译器都必须有的。 先明确哪些阶段是必须有的:词法分析语法分析语义分析目标代码生成,这四步是编译器的核心流程,缺少任何一步就无法完成从源程序到目标程序的翻译,所以每个编译器都必须有。 再看哪些阶段是可选的:中间代码生成不是必须的,有些简单的编译器可以直接从语义分析跳到目标代码生成,不经过中间代码这一步。代码优化同样不是必须的,优化只是让生成的代码跑得更快更省内存,不做优化程序照样能正确运行。 选项 A 包含词法分析和语法分析,两者都是必须的,排除。选项 B 包含语义分析(必须)和中间代码生成(可选),两者不都是可选的,排除。选项 C 包含中间代码生成和代码优化,两者都是可选的,符合题意。选项 D 包含代码优化(可选)和目标代码生成(必须),两者不都是可选的,排除。 答案是 C,中间代码生成和代码优化。 |
|
与编译器相比,解释器( )。 |
| A.不参与运行控制,程序执行的速度慢 |
| B.参与运行控制,程序执行的速度慢 |
| C.参与运行控制,程序执行的速度快 |
| D.不参与运行控制,程序执行的速度快 |
| B |
|
这道题同时考查两个特征:解释器是否参与运行控制,以及程序执行速度。 先看是否参与运行控制。编译器的工作在生成目标代码之后就结束了,程序运行时 CPU 直接执行目标代码,编译器完全不参与。解释器不同,它没有生成目标代码,每次运行程序时都要一边读源代码一边翻译一边执行,整个运行过程中解释器始终在场,一直参与控制程序的执行。所以解释器是参与运行控制的,排除 A 和 D。 再看执行速度。编译器提前把整个程序翻译成了机器指令,运行时 CPU 直接执行,速度极快。解释器每次运行都要实时翻译,多了一道翻译的开销,执行速度比编译器慢得多。所以解释器执行速度慢,排除 C。 答案是 B,参与运行控制,程序执行的速度慢。 |
文法
知识点
上下文无关文法(CFG)是描述程序设计语言语法的重要工具,它由四元组组成:终结符集非终结符集开始符号和产生式规则。在本题的算术表达式文法中,通过递归规则定义了运算优先级和结合性:
- 优先级:括号 > 一元负号 > 乘法 > 加法
- 结合性:加法乘法为左结合
- 各非终结符含义:
- $E$(表达式):处理加法运算,可推导为单个项或表达式+项
- $T$(项):处理乘法运算,可推导为单个因子或项*因子
- $F$(因子):处理一元负号和数字,可推导为负因子或数字
- $N$(数字):终结符,代表0-9的单个数字
|
简单算术表达式的结构可以用下面的上下文无关文法进行描述($E$为开始符号),()是符合该文法的句子。 |
| A.2--3*4 |
| B.2+-3*4 |
| C.(2+3)*4 |
| D.2*4-3 |
| B |
|
我们逐一分析选项是否能从开始符号$E$推导出来:
综上,正确答案为 B。 💡 补充说明: |
正规式与正规集
知识点
正规式(也叫正则表达式)是描述正规集(字符串集合)的工具,核心是用简洁的符号表示一类有规律的字符串。
核心符号含义
| 符号 | 名称 | 含义 | 例子 |
|---|---|---|---|
| `r | s` | 或 | 匹配r或s的字符串 |
rs |
连接 | 先匹配r再匹配s的字符串 |
ab 匹配ab |
r* |
闭包 | 匹配r重复0次或多次的字符串 |
a* 匹配空串aaaaaa… |
实际应用
- 编程中字符串校验(如邮箱手机号格式验证)
- 文本编辑器的查找替换功能
- 编译原理中词法分析器的设计(定义标识符关键字的规则)
|
由字符a、b构成的字符串中,若每个a后至少跟一个b,则该字符串集合可用正规式表示为()。 |
| A.(b|ab)* |
| B.(ab*)* |
| C.(a*b*)* |
| D.(a|b)* |
| A |
|
题目要求“每个
综上,只有选项A能保证“每个 💡 零基础小技巧:做这类题时,可以用“举反例”的方法,先想一个不符合题目要求的字符串(比如 |
有限自动机
知识点
有限自动机(Finite Automaton FA)是编译原理中词法分析的核心工具,和正规式等价,用于描述字符串的匹配规则。它分为**确定有限自动机(DFA)和不确定有限自动机(NFA)**两种,二者都能描述正规集,且NFA可以转化为等价的DFA。
核心概念与分类
| 类型 | 全称 | 核心特点 | 状态转移 | 空转移 | 例子 |
|---|---|---|---|---|---|
| DFA | 确定有限自动机 | 每个状态对同一个输入字符,只有一种转移选择 | 唯一确定 | 不允许 | 本题的自动机 |
| NFA | 不确定有限自动机 | 状态对同一个输入字符,可能有多个转移选择,或允许空转移 | 不唯一 | 允许(用ε表示) |
带分支或空边的自动机 |
关键补充
- 等价性:所有NFA都可以通过子集法,转化为功能完全相同的DFA,因此二者描述的语言集合是一样的。
- 实际应用:
- DFA:词法分析器的实现(如编译器识别关键字标识符),因为它匹配效率高无回溯。
- NFA:正则表达式引擎的底层实现,先把正则式转化为NFA,再转为DFA执行匹配。
|
**题目**:下图所示为一个有限自动机(其中,A是初态、C是终态),该自动机识别的语言可用正规式()表示。 |
| A.(0|1)*01 |
| B.1*0*10*1 |
| C.1*0*01 |
| D.1*(0|10)*1 |
| A |
|
这是一个确定有限自动机(DFA),我们按步骤分析:
综上,正确答案是 A。 💡 零基础补充:区分DFA和NFA的小技巧
|
表达式
知识点
表达式是程序设计语言中描述计算的核心单元,常见的表示形式有中缀表达式前缀表达式(波兰式)和后缀表达式(逆波兰式)。
三种表达式对比
| 表达式类型 | 别称 | 形式(以a + b * c为例) |
求值结构 | 特点 |
|---|---|---|---|---|
| 中缀表达式 | 常规表达式 | a + b * c |
需考虑优先级和括号 | 人类易读,计算机处理麻烦 |
| 前缀表达式 | 波兰式 | + a * b c |
递归/栈 | 运算符在前面,无优先级问题 |
| 后缀表达式 | 逆波兰式 | a b c * + |
栈 | 运算符在后面,最适合计算机求值 |
逆波兰式求值原理(栈的应用)
- 从左到右扫描表达式
- 遇到数字:压入栈中
- 遇到运算符:弹出栈顶两个数(先弹出的是右操作数,后弹出的是左操作数),计算结果后压回栈中
- 扫描结束后,栈中仅剩的数就是表达式的结果。
实际应用
- 编译器的表达式求值:将中缀表达式转为逆波兰式,再用栈求值,避免处理优先级和括号
- 计算器程序的底层实现:大部分计算器都基于逆波兰式和栈来完成计算。
|
表达式采用逆波兰式表示时,利用()进行求值。 |
| A.栈 |
| B.队列 |
| C.符号表 |
| D.散列表 |
| A |
综上,正确答案是 A。 💡 零基础小例子:用栈计算逆波兰式
|
四、数据结构
线性表
知识点
线性表是最基础的数据结构,是元素按线性顺序排列的有限序列,主要有两种存储方式:顺序存储(数组)和链式存储(链表)。
两种存储方式对比
| 维度 | 顺序存储(数组) | 链式存储(链表) |
|---|---|---|
| 存储结构 | 连续内存块,元素依次存放 | 分散内存节点,每个节点存数据+指向下一节点的指针 |
| 读/访问 | 直接通过下标访问,时间复杂度 $O(1)$ | 必须从头节点遍历,时间复杂度 $O(n)$ |
| 插入/删除 | 需移动后续元素,平均时间复杂度 $O(n)$ | 仅修改指针,时间复杂度 $O(1)$ |
| 空间性能 | 无额外开销,存储密度=1 | 每个节点额外存指针,存储密度<1 |
| 适用场景 | 频繁访问元素个数固定 | 频繁增删元素个数动态变化 |
实际应用
- 顺序存储:程序中的数组栈队列的顺序实现,适合频繁查询的场景。
- 链式存储:链表栈/队列的链式实现,以及操作系统的内存管理文件系统的目录结构,适合频繁增删的场景。
|
设有一个包含n个元素的有序线性表。在等概率情况下删除其中的一个元素,若采用顺序存储结构,则平均需要移动( )个元素 |
| A.1 |
| B.$(n-1)/2$ |
| C.$\log n$ |
| D.n |
| B |
|
|
设有一个包含n个元素的有序线性表。在等概率情况下删除其中的一个元素,若采用单链表存储,则平均需要移动( )个元素。 |
| A.0 |
| B.1 |
| C.$(n-1)/2$ |
| D.$n/2$ |
| A |
|
单链表存储结构分析 |
队列和栈
知识点
核心概念辨析
| 数据结构 | 核心特性 | 操作规则 | 底层实现 | 关键特点 |
|---|---|---|---|---|
| 栈 (Stack) | 后进先出 (LIFO) | 压栈(Push)弹栈(Pop)取栈顶(Top) | 数组链表 | 只能在一端操作 |
| 队列 (Queue) | 先进先出 (FIFO) | 入队(Enqueue)出队(Dequeue) | 数组链表 | 只能在队尾入队头出 |
| 双端队列 (Deque) | 两端均可操作 | 队头/队尾均可入队出队 | 循环数组双向链表 | 灵活性最高 |
特殊规则:受限双端队列(输出受限/输入受限)
题目中描述的规则属于**“输入受限的双端队列”**:
- 规则:元素从哪个端口进入,就只能从哪个端口取出(A进A出B进B出)。
- 转化:这种结构实际上等价于两个独立的栈。
- A端口形成一个栈(ab依次入栈,只能b先出,a后出)。
- B端口形成一个栈(cd依次入栈,只能d先出,c后出)。
- 最终结论:所有从A端进入的元素,出队顺序只能是
...b a...所有从B端进入的元素,出队顺序只能是...d c...。
实际应用
- 栈:函数调用栈编辑器撤销操作表达式求值(后缀式)深度优先搜索(DFS)。
- 队列:消息队列任务调度广度优先搜索(BFS)银行叫号系统。
- 双端队列:缓存淘汰机制(如LRU)滑动窗口算法实现双端队列数据结构。
💡 零基础解题口诀:
遇到“双端队列同一端进出”的题,直接把它看作两个栈。
A口进的元素:永远是 b 在前,a 在后。
B口进的元素:永远是 d 在前,c 在后。
谁违反了这个顺序,谁就是不可能的序列。
|
双端队列是指在队列的两个端口都可以加入和删除元素。现在要求元素进队列和出队列必须在同一端口,即从A端进队的元素必须从A端出、从B端进队的元素必须从B端出,则对于4个元素的序列a、b、c、d,若要求前2个元素(a、b)从A端口按次序全部进入队列,后两个元素(c、d)从B端口按次序全部进入队列,则不可能得到的出队序列是( )。 |
| A.d、a、b、c |
| B.d、c、b、a |
| C.b、a、d、c |
| D.b、d、c、a |
| A |
|
🧠 一题目本质在说什么? 这是一个**双端队列(deque)**问题,但有个关键限制:
这句话非常重要!意味着:
所以其实可以理解为:
📥 二入队过程 题目规定:
我们来画一下: A端(左边): 进:a → b
👉 出队顺序:b → a B端(右边): 进:c → d
👉 出队顺序:d → c 🔑 三核心约束(最关键!) 最终输出序列必须满足:
👉 这是“铁规则”,不能违反! 🔍 四逐个看选项 ❌ A:d a b c 看 a 和 b:
👉 违反 A端规则 → 不可能 ✅ B:d c b a 检查:
👉 完全符合 → 可以 ✅ C:b a d c 检查:
👉 可以 ✅ D:b d c a 检查:
👉 也可以 🎯 五最终答案
🧩 六记忆技巧(考试用) 看到这种题,直接记:
然后只要检查:
谁违反,谁就是答案。
|
串
知识点
串(字符串)是由零个或多个字符组成的有限序列,是数据结构中一种特殊的线性表(元素为字符)。核心概念与操作如下:
核心概念对比
| 概念 | 定义 | 关键点 | 示例(串 S="abc") |
|---|---|---|---|
| 空串 | 长度为0的串,不包含任何字符 | 不含字符,长度=0 | ""(长度0) |
| 空白串 | 由一个或多个空白字符(空格制表符等)组成的串 | 含字符,长度>0 | " "(长度1) |
| 子串 | 串中任意连续的字符序列 | 连续的子序列 | "ab""bc" 是子串"ac" 不是 |
| 子序列 | 从串中删除0个或多个字符,不改变相对顺序的字符序列 | 不要求连续 | "ac" 是 S 的子序列 |
| 串的比较 | 按字典序(ASCII码值)逐字符对比,直到出现不同字符或结束 | 不是只比长度 | "a" < "b""app" < "apple" |
关键结论
- 空串 $\neq$ 空白串:空串无字符,空白串有空格字符。
- 子串 $\subset$ 子序列:子串是子序列的一种特殊情况(连续)。
- 串的比较:先比第一个不同字符的ASCII码,长度不同仅当前面字符都相同时才生效。
实际应用
- 文本处理:编辑器的搜索替换匹配(如正则表达式)。
- 编译原理:识别标识符关键字字符串常量。
- 网络通信:HTTP请求头URL的解析与处理。
- 生物信息学:DNA序列的比对与模式匹配。
|
下列有关于字符串的描述中,说法正确的是( )。 |
| A.空串就是指含有空白字符构成的串 |
| B.子串是指任意个连续的字符组成的子序列 |
| C.子序列是特殊的子串 |
| D.字符串的比较实质就是两个串的长度对比 |
| B |
|
逐一分析选项:
答案:B 💡 零基础避坑指南: |
二叉树的存储和特性
知识点
1. 树的核心特性(通用树)
- 树的度:树中所有节点的度的最大值。
- 节点的度:节点拥有的子节点个数。
- 边数与节点数关系:
边数 = 节点总数 - 1(树是连通无环图)。 - 总度数定理:所有节点的度之和 = 边数。
2. 二叉树的核心特性
- 二叉链表存储:每个节点有两个指针(左孩子右孩子),共
2k个指针(k为节点数)。 - 空指针数:
k个节点的二叉树,边数为k-1,即有k-1个非空指针,因此空指针数 =2k - (k-1) = k+1。 - 满二叉树/完全二叉树:特殊结构,常考性质如叶子节点数节点编号规则。
3. 实际应用
- 二叉链表:实现二叉树的链式存储,是所有二叉树算法(遍历查找平衡树)的基础。
- 树的度计算:用于解决节点数边数叶子数相关的计算题,是数据结构考试的高频考点。
|
已知树T的度为4,且度为4的结点数为7个、度为3的结点数为5个、度为2的结点数为8个、度为1的结点数为10个,那么T的叶子结点个数为( )。(注:树中结点的度,结点的度中的最大值称为树的度。) |
| A.30 |
| B.35 |
| C.40 |
| D.49 |
| C |
|
设叶子结点数(度为0的节点)为
|
|
设某二叉树采用二叉链表表示(即结点的两个指针分别指示左、右孩子)。当该二叉树包含k个结点时,其二叉链表结点中必有( )个空的孩子指针。 |
| A.k-1 |
| B.k |
| C.k+1 |
| D.2k |
| C |
💡 零基础小技巧:
|
二叉树遍历
什么是二叉树
二叉树是一种树形数据结构,每个节点最多有两个子节点,分别叫左子节点和右子节点。整棵树从最顶上的根节点开始,向下延伸。
三种遍历方式
遍历就是按照某种规则把二叉树的所有节点访问一遍。三种遍历方式的规则如下,记忆口诀是看"根"在什么位置:
| 遍历方式 | 顺序 | 根的位置 |
|---|---|---|
| 先序遍历 | 根 → 左子树 → 右子树 | 根在最前 |
| 中序遍历 | 左子树 → 根 → 右子树 | 根在中间 |
| 后序遍历 | 左子树 → 右子树 → 根 | 根在最后 |
从先序和中序还原二叉树的方法
先序遍历的第一个节点一定是根节点。找到根节点后,在中序遍历里定位根节点的位置,根节点左边的所有节点构成左子树,右边的所有节点构成右子树。然后对左子树和右子树各自递归重复这个过程,直到还原出完整的树结构。
实际应用
编译器在处理表达式时会构建语法树,后序遍历语法树就能得到逆波兰表达式(后缀表达式),方便计算机计算。文件系统的目录树删除操作也是用后序遍历,先删子文件夹再删父文件夹,确保不遗漏。
|
某二叉树的中序、先序遍历序列分别为 $\{20, 30, 10, 50, 40\}$、$\{10, 20, 30, 40, 50\}$,则该二叉树的后序遍历序列为( )。 |
| A.50, 40, 30, 20, 10 |
| B.30, 20, 10, 50, 40 |
| C.30, 20, 50, 40, 10 |
| D.20, 30, 10, 40, 50 |
| C |
|
按照"先序第一个是根,中序里根左边是左子树,右边是右子树"的方法,一步步还原树的结构。 第一步,找整棵树的根。先序第一个是 10,所以根节点是 10。 第二步,在中序 ${20 30 10 50 40}$ 里找到 10,它在第3位。10 左边是 ${20 30}$,这是左子树的所有节点10 右边是 ${50 40}$,这是右子树的所有节点。 第三步,处理左子树。左子树节点是 ${20 30}$,在先序里这两个节点的顺序是 ${20 30}$,所以左子树的根是 20。在中序 ${20 30}$ 里,20 左边没有节点,右边是 ${30}$,所以 20 没有左子节点,右子节点是 30。 第四步,处理右子树。右子树节点是 ${50 40}$,在先序里这两个节点的顺序是 ${40 50}$,所以右子树的根是 40。在中序 ${50 40}$ 里,40 左边是 ${50}$,右边没有节点,所以 40 的左子节点是 50,没有右子节点。 到这里树结构完全还原,根是 10,左子树是 20(右子节点为30),右子树是 40(左子节点为50)。 第五步,对还原好的树做后序遍历(左→右→根):先遍历左子树得到 30 20,再遍历右子树得到 50 40,最后输出根 10,完整后序序列是 30 20 50 40 10。 答案是 C。 |
二叉排序树
什么是二叉排序树
二叉排序树(也叫二叉搜索树二叉查找树,BST)是一种特殊的二叉树,它满足一个核心规则:对于任意一个节点,它的左子树里所有节点的值都比它小,它的右子树里所有节点的值都比它大。
digraph bst_rule {
node [shape=circle style=filled fillcolor=lightyellow]
X [label="节点X"]
L [label="左子树\n所有值 < X" shape=box fillcolor=lightblue]
R [label="右子树\n所有值 > X" shape=box fillcolor=red]
X -> L
X -> R
}
插入节点的规则
从根节点开始比较,比当前节点小就往左走,比当前节点大就往右走,走到空位就插入。插入的顺序不同,建出来的树形状也不同,但都满足 BST 规则。这也意味着:第一个插入的节点就是根节点。
最重要的特性:中序遍历有序
对二叉排序树做中序遍历(左→根→右),得到的序列一定是从小到大排好序的。这是 BST 最核心的特性,也是考试最常考的点。注意只有中序遍历才有序,先序和后序遍历不保证有序。
实际应用
BST 是很多数据结构的基础。数据库索引文件系统的目录查找都用到了平衡二叉排序树(如红黑树AVL树)的思想。查找一个值时,每次比较都能排除一半的节点,理想情况下查找效率是 $O(\log n)$,比线性查找快得多。
|
可以构造出下图所示二叉排序树(二叉检索树、二叉查找树)的关键码序列是( )。
|
| A.10, 13, 17, 19, 23, 27, 31, 40, 65, 91 |
| B.23, 40, 91, 17, 19, 10, 31, 65, 27, 13 |
| C. 23, 19, 40, 27, 17, 13, 10, 91, 65, 31 |
| D. 27, 31, 40, 65, 91, 13, 10, 17, 23, 19 |
| B |
|
BST 的插入规则决定了第一个插入的元素就是根节点。图中根节点是 23,所以正确的序列第一个数必须是 23。 选项 A 第一个是 10,排除。选项 D 第一个是 27,排除。剩下 B 和 C 都以 23 开头,需要进一步验证。 验证选项 B:23 40 91 17 19 10 31 65 27 13,按 BST 插入规则一步步插入: 插入 23:根节点 = 23 插入 40:40 > 23,放 23 的右边 → 23 右子节点 = 40 插入 91:91 > 23 走右到 40,91 > 40,放 40 的右边 → 40 右子节点 = 91 插入 17:17 < 23,放 23 的左边 → 23 左子节点 = 17 插入 19:19 < 23 走左到 17,19 > 17,放 17 的右边 → 17 右子节点 = 19 插入 10:10 < 23 走左到 17,10 < 17,放 17 的左边 → 17 左子节点 = 10 插入 31:31 > 23 走右到 40,31 < 40,放 40 的左边 → 40 左子节点 = 31 插入 65:65 > 23 走右到 40,65 > 40 走右到 91,65 < 91,放 91 的左边 → 91 左子节点 = 65 插入 27:27 > 23 走右到 40,27 < 40 走左到 31,27 < 31,放 31 的左边 → 31 左子节点 = 27 插入 13:13 < 23 走左到 17,13 < 17 走左到 10,13 > 10,放 10 的右边 → 10 右子节点 = 13 最终得到的树和图中完全一致,选项 B 正确。 答案是 B。 |
|
以下关于二叉排序树(或二叉查找树、二叉搜索树)的叙述中,正确的是( )。 |
| A.对二叉排序树进行先序、中序和后序遍历,都得到结点关键字的有序序列 |
| B.含有 $n$ 个结点的二叉排序树高度为 $\lfloor \log_2 n \rfloor + 1$ |
| C.从根到任意一个叶子结点的路径上,结点的关键字呈现有序排列的特点 |
| D.从左到右排列同层次的结点,其关键字呈现有序排列的特点 |
| A |
|
逐个分析每个选项。 选项 A 说先序中序后序遍历都能得到有序序列,这是错的。BST 只有中序遍历(左→根→右)才能得到从小到大的有序序列,先序和后序遍历得到的序列不保证有序。以图中的树为例,先序遍历结果是 23 17 10 13 19 40 31 27 65 91,并不是有序的,排除 A。 选项 B 说含有 $n$ 个节点的 BST 高度为 $\lfloor \log_2 n \rfloor + 1$,这只在树完全平衡时才成立。如果按照 1 2 3 4 5 的顺序插入,每个节点只有右子节点,BST 会退化成一条链,高度直接变成 $n$,远大于 $\lfloor \log_2 n \rfloor + 1$,排除 B。 选项 C 说从根到叶子路径上的节点关键字有序排列,这是错的。以图中路径 23→17→10→13 为例,序列是 23 17 10 13,先下降后上升,并不是单调有序的,排除 C。 选项 D 说同层次的节点从左到右排列关键字有序,这是正确的。BST 规定任意节点的左子树所有值都小于该节点,右子树所有值都大于该节点。这个规则层层传递,导致同一层从左到右的节点,左边的节点一定来自某个祖先的左子树,右边的节点来自右子树,因此左边节点的值一定比右边节点的值小,同层节点从左到右天然有序。以图中第三层为例:10 19 31 91,确实是从小到大排列的。 答案是 D。 |
哈夫曼树
什么是哈夫曼树
哈夫曼树(Huffman Tree)也叫最优二叉树,是一种根据字符出现频率构造的特殊二叉树。频率高的字符用短编码,频率低的字符用长编码,这样整体编码长度最短,达到压缩的目的。ZIPJPEG 等压缩格式都用到了哈夫曼编码的思想。
构造哈夫曼树的步骤
第一步,把所有字符的频率看作独立的叶子节点,放入一个集合。
第二步,每次从集合里取出频率最小的两个节点,把它们合并成一个新节点,新节点的频率等于两个子节点频率之和。规则是频率小的放左边,频率大的放右边(左小右大)。
第三步,把新节点放回集合,重复第二步,直到集合里只剩一个节点,那就是根节点。
哈夫曼编码的读取方法
从根节点出发,走左子树记 0,走右子树记 1,走到某个字符叶子节点时,路径上记录的 0 和 1 串起来就是该字符的哈夫曼编码。
| 字符 | 频率 | 从根到叶的路径 | 编码 |
|---|---|---|---|
| a | 40 | 左 | 0 |
| b | 10 | 右→左→左 | 100 |
| e | 14 | 右→左→右 | 101 |
| d | 16 | 右→右→左 | 110 |
| c | 20 | 右→右→右 | 111 |
哈夫曼编码的特点
哈夫曼编码是前缀码,任何一个字符的编码都不是另一个字符编码的前缀,这保证了解码时不会产生歧义。频率越高的字符编码越短,整体压缩效果最优。
实际应用
文件压缩(如 gzip)图片压缩(JPEG)网络数据传输都大量使用哈夫曼编码。比如英文文章里 e 出现频率最高,哈夫曼编码会给 e 分配最短的编码,从而大幅减少存储空间。
|
已知某文档包含 5 个字符,每个字符出现的频率如下表所示。采用哈夫曼编码对该文档压缩存储,则单词"cade"的编码为( )。 | 字符 | a | b | c | d | e |
|
| A.1110110101 |
| B.1100111101 |
| C.1110110100 |
| D.1100111100 |
| A |
|
先从哈夫曼树里读出每个字符的编码。从根节点出发,左走记 0,右走记 1,一直走到叶子节点: a 的编码:从根直接走左,路径只有一步,编码是 0。 b 的编码:根→右→左→左,编码是 100。 e 的编码:根→右→左→右,编码是 101。 d 的编码:根→右→右→左,编码是 110。 c 的编码:根→右→右→右,编码是 111。 现在把"cade"每个字符的编码拼接起来: $$c = 111\quad a = 0\quad d = 110\quad e = 101$$ $$\text{cade} = 111 + 0 + 110 + 101 = 1110110101$$ 对照四个选项,1110110101 就是选项 A。 选项 C 是 1110110100,和 A 只差最后一位,最后一位是 e 的编码末尾,e 的编码是 101 不是 100,所以 C 错。其余选项开头不是 111(c 的编码),也直接排除。 答案是 A,1110110101。 |
图的定义和存储
什么是图
图由顶点和边组成,顶点表示实体,边表示实体之间的关系。边有方向的叫有向图,没有方向的叫无向图。
邻接矩阵
用一个二维数组表示图,行和列都代表顶点,数组里的值表示两个顶点之间是否有边。有向图中,$matrix[i][j]=1$ 表示有从顶点 $i$ 到顶点 $j$ 的边。矩阵大小是顶点数 $\times$ 顶点数。
邻接链表
每个顶点对应一个链表,链表里的每个节点(表结点)代表从该顶点出发能到达的一个顶点。表结点个数等于该顶点的出度(从这个顶点出发的边的数量)。
实际应用
邻接矩阵适合稠密图(边多),查询两点间是否有边速度快。邻接链表适合稀疏图(边少),节省存储空间。导航地图社交网络好友关系都是图结构的典型应用。
|
设一个包含 N 个顶点、E 条边的简单无向图采用邻接矩阵存储结构(矩阵元素 A[i][j] 等于 1/0 分别表示顶点与顶点之间有/无边),则该矩阵中的非零元素数目为( )。 |
| A.N |
| B.E |
| C.2E |
| D.N+E |
| C |
|
解析: 先搞清楚邻接矩阵的结构。邻接矩阵是一个 $N \times N$ 的二维数组,第 $i$ 行第 $j$ 列的值 A[i][j]=1 表示顶点 $i$ 和顶点 $j$ 之间有边,A[i][j]=0 表示没有边。 关键在于这是无向图。无向图里边没有方向,顶点 $i$ 到顶点 $j$ 有边,就意味着顶点 $j$ 到顶点 $i$ 也有边,所以 A[i][j] 和 A[j][i] 同时等于 1。 也就是说,无向图里每一条边在邻接矩阵里会被记录两次:A[i][j]=1 和 A[j][i]=1。 图里一共有 E 条边,每条边对应矩阵中两个非零元素,所以非零元素总数是 $E \times 2 = 2E$。 选项 A 的 N 是顶点数,和非零元素数量无关,排除。选项 B 的 E 只算了每条边一次,忽略了无向图的对称性,排除。选项 D 的 N+E 没有道理,排除。 答案是 C,2E。 |
|
对于如下所示的有向图,其邻接矩阵是一个( )的矩阵 。 |
| A.$5\times5$ |
| B.$5\times7$ |
| C.$7\times5$ |
| D. $7\times7$ |
| A |
|
邻接矩阵的大小由顶点数决定,行数和列数都等于顶点数。这道题的图有顶点 12345,共 5 个顶点,所以邻接矩阵是 $5\times5$ 的矩阵。边的数量不影响矩阵大小,有边的位置填 1,没边的位置填 0。 答案是 A,$5\times5$。 |
|
采用邻接链表存储时,顶点 1 的表结点个数为 2,顶点 5 的表结点个数为 0,顶点 2 和 3 的表结点个数分别为( )。 |
| A.2, 1 |
| B.2, 2 |
| C.3, 4 |
| D.4, 3 |
| A |
|
邻接链表里,每个顶点的表结点个数等于它的出度,也就是从它出发的有向边的条数。 先验证题目已知信息:顶点 1 出发的边有 1→2 和 1→3,共 2 条,表结点个数为 2 ✓。顶点 5 没有任何出发的边,表结点个数为 0 ✓,说明我们读图正确。 顶点 2 出发的边:2→4 和 2→5,共 2 条,表结点个数为 2。 顶点 3 出发的边:3→4,共 1 条,表结点个数为 1。 答案是 A,2 和 1。 |
图的遍历
深度优先遍历(DFS)
从起始顶点出发,沿一条路走到底,走不下去了再回头换另一条路,类似走迷宫。规则是:访问当前顶点,选一个未访问的邻居继续深入,没有未访问邻居时回退。
广度优先遍历(BFS)
从起始顶点出发,先访问所有直接邻居,再访问邻居的邻居,一层一层向外扩展,类似水波纹扩散。用队列实现:访问一个顶点就把它所有未访问的邻居加入队列。
| 对比项 | DFS | BFS |
|---|---|---|
| 数据结构 | 栈(或递归) | 队列 |
| 特点 | 一条路走到底 | 一层一层扩散 |
| 应用 | 拓扑排序连通性判断 | 最短路径层次遍历 |
实际应用
BFS 用于社交网络中寻找"最少几步认识某人",DFS 用于解决迷宫问题检测图中是否有环。
|
某有向图如下所示,从顶点 v1 出发对其进行深度优先遍历,可能得到的遍历序列是( )。
|
| A.①②③ |
| B.①③④ |
| C.①②④ |
| D.②③④ |
| D |
|
DFS 的规则是沿一条路深入到底再回头,一旦进入某个顶点,必须把它的所有邻居都访问完才能回退。 验证①:$V_1→V_2$,此时 $V_2$ 还有未访问的邻居 $V_4$ 和 $V_5$,DFS 不会在这里回退去访问 $V_3$,所以①不可能。 验证②:$V_1→V_3→V_4→V_5$,回退到 $V_4$$V_3$再回 $V_1$,最后访问 $V_2$,$V_2$ 的邻居 $V_4$$V_5$ 都已访问。序列 $V_1V_3V_4V_5V_2$ ✓ 可能。 验证③:$V_1→V_3$,$V_3$ 的邻居有 $V_2$ 和 $V_4$,先访问 $V_2$,再从 $V_2$ 深入 $V_4→V_5$。序列 $V_1V_3V_2V_4V_5$ ✓ 可能。 验证④:$V_1→V_2→V_4→V_5$,回退到 $V_2$($V_5$ 已访问),回退到 $V_1$,访问 $V_3$,$V_3$ 的邻居 $V_2$$V_4$ 都已访问。序列 $V_1V_2V_4V_5V_3$ ✓ 可能。 答案是 D,②③④。 |
|
从顶点 v1 出发对其进行广度优先遍历,可能得到的遍历序列是( )。 |
| A.①② |
| B.①③ |
| C.②③ |
| D.③④ |
| B |
|
BFS 用队列,先把起点的所有邻居入队,再依次处理队列中的顶点。 BFS 情形一(V1 先访问 V2): 访问 $V_1$,将 $V_2V_3$ 入队。队列:$[V_2 V_3]$ 出队 $V_2$,访问,将 $V_4V_5$ 入队。队列:$[V_3 V_4 V_5]$ 出队 $V_3$,访问,$V_3$ 的邻居 $V_2$ 已入队$V_4$ 已入队。队列:$[V_4 V_5]$ 出队 $V_4$,访问,$V_5$ 已入队。出队 $V_5$,访问。 结果:$V_1 V_2 V_3 V_4 V_5$ = ① ✓ BFS 情形二(V1 先访问 V3): 访问 $V_1$,将 $V_3V_2$ 入队。队列:$[V_3 V_2]$ 出队 $V_3$,访问,将 $V_4$ 入队($V_2$ 已入队)。队列:$[V_2 V_4]$ 出队 $V_2$,访问,将 $V_5$ 入队($V_4$ 已入队)。队列:$[V_4 V_5]$ 出队 $V_4$,访问。出队 $V_5$,访问。 结果:$V_1 V_3 V_2 V_4 V_5$ = ③ ✓ 答案是 B,①③。 |
拓扑排序
什么是拓扑排序
拓扑排序只能用于有向无环图(DAG,Directed Acyclic Graph),也就是有方向没有环的图。它把图中所有顶点排成一个线性序列,规则是:如果存在从顶点 v 到顶点 w 的边或路径,则 v 一定排在 w 前面。
拓扑排序的步骤
第一步,找到所有入度为 0 的顶点(没有任何边指向它的顶点),把它们作为候选起点。
第二步,从候选中选一个顶点输出,然后删除它以及从它出发的所有边。
第三步,删除后可能出现新的入度为 0 的顶点,把它们加入候选。
第四步,重复第二步和第三步,直到所有顶点都输出。
入度的概念
入度是指指向该顶点的边的数量。入度为 0 说明没有任何顶点必须排在它前面,所以它可以排在当前最前面的位置。
验证序列是否合法的技巧
从序列的第二个顶点开始,检查它是否有边指向它前面的任意顶点。如果存在这样一条边,说明它应该排在那个顶点后面,序列非法。如果所有顶点都不指向自己前面的顶点,序列合法。
实际应用
大学课程安排就是典型的拓扑排序场景,必须先修完数学才能学线性代数,先学 C 语言才能学数据结构,这种先后依赖关系就用拓扑排序来安排顺序。工程项目管理软件包依赖安装(如 npmpip)也都用到拓扑排序来确定安装顺序。
|
拓扑序列是有向无环图中所有顶点的一个线性序列,若有向图中存在弧 $\langle v, w \rangle$ 或存在从顶点 v 到 w 的路径,则在该有向图的任一拓扑序列中,v 一定在 w 之前。下面有向图的拓扑序列是( )。
|
| A.41235 |
| B.43125 |
| C.42135 |
| D.41325 |
| A |
|
先看每个顶点的入度,入度为 0 的才能排在最前面: 顶点 1:有 4→1 指向它,入度为 1,不能第一个。 所以第一个顶点只能是 4,排除选项 A(以 4 开头但第二个是 1,可以继续验证)。四个选项都以 4 开头,继续看第二个顶点。 删掉 4 及其出边(4→14→3)后,顶点 1 的入度变为 0,顶点 2 入度仍为 1,顶点 3 入度变为 2,顶点 5 入度仍为 1。此时入度为 0 的只有顶点 1,所以第二个必须是 1。 选项 B 第二个是 3,排除。选项 C 第二个是 2,排除。剩下 A(41235)和 D(41325)。 删掉 1 及其出边(1→21→3)后,顶点 2 入度变为 0,顶点 3 入度变为 1,顶点 5 入度仍为 1。此时入度为 0 的只有顶点 2,所以第三个必须是 2,排除 D(第三个是 3)。 继续验证 A(41235):删掉 2 及其出边(2→32→5)后,顶点 3 入度变为 0,顶点 5 入度变为 0,3 和 5 都可以排第四,A 选择先排 3 再排 5,合法。 答案是 A,41235。 |
|
对有向图 G 进行拓扑排序得到的拓扑序列中,顶点 $V_i$ 在顶点 $V_j$ 之前,则说明 G 中( )。 |
| A.一定存在有向弧 $\langle V_i, V_j \rangle$ |
| B.一定不存在有向弧 $\langle V_j, V_i \rangle$ |
| C.必定存在从 $V_i$ 到 $V_j$ 的路径 |
| D.必定存在从 $V_j$ 到 $V_i$ 的路径 |
| B |
|
拓扑序列的定义是:如果存在从 v 到 w 的边或路径,则 v 排在 w 前面。反过来理解:$V_i$ 排在 $V_j$ 前面,说明要么有边 $\langle V_i V_j \rangle$,要么有从 $V_i$ 到 $V_j$ 的路径,要么两者都没有(只是因为没有约束关系,被随机排在前面)。逐个分析选项: 选项 A 说一定存在有向弧 $\langle V_i V_j \rangle$,这是错的。$V_i$ 排在 $V_j$ 前面,可能只是因为它们没有任何直接或间接的连接关系,拓扑排序只是把它们按某种合法顺序排列,不一定有直接的边,排除 A。 选项 B 说一定不存在有向弧 $\langle V_j V_i \rangle$,这是正确的。如果存在 $\langle V_j V_i \rangle$,根据拓扑排序的定义,$V_j$ 必须排在 $V_i$ 前面,但题目说 $V_i$ 排在 $V_j$ 前面,这产生矛盾。所以 $\langle V_j V_i \rangle$ 这条边绝对不可能存在,B 正确。 选项 C 说必定存在从 $V_i$ 到 $V_j$ 的路径,这是错的。$V_i$ 排在前面可能只是因为它的入度恰好先变为 0,和 $V_j$ 之间没有任何路径关系,排除 C。 选项 D 说必定存在从 $V_j$ 到 $V_i$ 的路径,这显然更错,如果有这条路径,$V_j$ 应该排在 $V_i$ 前面,和题目矛盾,排除 D。 答案是 B,一定不存在有向弧 $\langle V_j V_i \rangle$。 |
五、算法基础
算法的效率
什么是时间复杂度
时间复杂度用来衡量一个算法随着输入规模 $n$ 增大,执行时间增长的速度。我们不关心具体跑了多少秒,只关心大致的增长趋势,用大 $O$ 符号表示。
| 复杂度 | 名称 | 举例 |
|---|---|---|
| $O(1)$ | 常数阶 | 直接取数组第一个元素 |
| $O(\log n)$ | 对数阶 | 二分查找 |
| $O(n)$ | 线性阶 | 遍历一个数组 |
| $O(n\log n)$ | 线性对数阶 | 归并排序 |
| $O(n^2)$ | 平方阶 | 冒泡排序两层嵌套循环 |
| $O(n^3)$ | 立方阶 | 三层嵌套循环 |
| $O(2^n)$ | 指数阶 | 暴力枚举所有子集 |
从上到下效率越来越低,$O(1)$ 最快,$O(2^n)$ 最慢。
蛮力法的复杂度分析
蛮力法(暴力法)就是把所有可能性都试一遍。比如长度为 $n$ 的序列 X,它的子序列一共有 $2^n$ 个(每个元素选或不选),对每个子序列再去 Y 里验证是否存在需要 $O(n)$ 时间,所以总时间复杂度是 $O(n \cdot 2^n)$,写作 $O(n2^n)$。
动态规划的复杂度分析
动态规划把大问题拆成子问题,用表格记录子问题的解,避免重复计算。最长公共子序列(LCS)的动态规划算法需要填一张 $n \times n$ 的表格,每格只需要 $O(1)$ 时间计算,所以总时间复杂度是 $O(n^2)$。
最长公共子序列(LCS)的递推公式
$$C[i][j]=\begin{cases}0 & \text{若 }i=0\text{ 或 }j=0\\C[i-1,j-1]+1 & \text{若 }i,j>0\text{ 且 }X_i=Y_j\\\max(C[i-1,j],\ C[i,j-1]) & \text{其他}\end{cases}$$
含义是:如果两个序列当前末尾字符相同,公共子序列长度在 $C[i-1][j-1]$ 的基础上加 1如果不同,就取去掉其中一个字符后两种情况的最大值。
实际应用
Git 的 diff 功能(比较两个文件的差异)DNA 序列比对文本相似度计算,底层都用到了 LCS 动态规划算法。蛮力法虽然简单,但 $O(n2^n)$ 在 $n=50$ 时就已经需要计算 $50 \times 2^{50} \approx 5.6 \times 10^{16}$ 次,完全不实用。
|
求解两个长度为 $n$ 的序列 X 和 Y 的一个最长公共子序列,可以采用蛮力法,对 X 的每一个子序列,判断其是否也是 Y 的子序列,最后求出最长的即可,该方法的时间复杂度为( )。 |
| A.$O(n^2)$ |
| B.$O(n^2 \lg n)$ |
| C.$O(n^3)$ |
| D.$O(n \cdot 2^n)$ |
| D |
|
蛮力法的思路是枚举 X 的所有子序列,再逐一到 Y 里验证。 第一步,X 的长度是 $n$,每个字符可以选或不选,所以 X 一共有 $2^n$ 个子序列。 第二步,对于其中每一个子序列,要判断它是否是 Y 的子序列,需要用双指针在 Y 里逐字符比对,时间是 $O(n)$。 两步合起来,总时间是 $2^n \times O(n) = O(n \cdot 2^n)$,即 $O(n2^n)$。 选项 A 的 $O(n^2)$ 是动态规划的复杂度,不是蛮力法。选项 B 和 C 也都比 $O(n2^n)$ 小得多,不符合蛮力法的实际开销。 答案是 D,$O(n2^n)$。 |
|
经分析发现该问题具有最优子结构,可以定义序列长度分别为 $i$ 和 $j$ 的两个序列 X 和 Y 的最长公共子序列的长度为 $C[i,j]$,采用自底向上的方法实现该算法,则时间复杂度为( )。 |
| A.$O(n^2)$ |
| B.$O(n^2 \lg n)$ |
| C.$O(n^3)$ |
| D.$O(n \cdot 2^n)$ |
| A |
|
自底向上的动态规划就是按照递推公式,从最小的子问题开始,逐格填写一张表格,最终得到答案。 $C[i][j]$ 是一张 $i$ 从 0 到 $n$$j$ 从 0 到 $n$ 的二维表格,共有 $(n+1) \times (n+1)$ 个格子,数量级是 $n^2$。 填每个格子的时候,根据递推公式只需要看 $C[i-1][j-1]$$C[i-1][j]$$C[i][j-1]$ 三个已知值,然后做一次比较或取最大值,时间是 $O(1)$。 所以总时间 $= n^2 \times O(1) = O(n^2)$。 选项 B 的 $O(n^2 \lg n)$ 和选项 C 的 $O(n^3)$ 都比 $O(n^2)$ 慢,说明还有额外的工作,但动态规划填表每格只需常数时间,不需要那么多。选项 D 的 $O(n2^n)$ 是蛮力法的复杂度,动态规划正是为了避免这么高的代价才被设计出来的。 答案是 A,$O(n^2)$。 |
查找算法
顺序查找
从第一个元素开始逐个比较,直到找到目标或遍历完全部。时间复杂度 $O(n)$,对有序无序表都适用。
折半查找(二分查找)
只能用于有序表。每次取当前范围的中间元素比较,目标比中间值小就去左半段,大就去右半段,每次排除一半元素。时间复杂度 $O(\log n)$。
折半查找的比较过程本质上是在一棵二叉判定树上走路径,每次比较后当前的搜索区间必须满足严格的上下界约束,后面比较到的每个值必须始终在当前合法区间内。
哈希查找(散列查找)
用散列函数 $H(key)$ 把关键码映射到表中某个位置,理想情况下直接定位,时间复杂度接近 $O(1)$。当两个不同的关键码映射到同一个位置时,发生冲突,线性探查法的解决方式是依次往后找第一个空槽存入。
| 查找方法 | 适用条件 | 时间复杂度 | 典型应用 |
|---|---|---|---|
| 顺序查找 | 无要求 | $O(n)$ | 链表小规模数据 |
| 折半查找 | 有序表 | $O(\log n)$ | 字典数据库索引 |
| 哈希查找 | 散列表 | $O(1)$ | 缓存密码验证 |
实际应用
数据库的 B+ 树索引本质是多路折半查找。Python 的 dictJava 的 HashMap 都是哈希表,查找极快。DNS 域名解析也用哈希表把域名快速映射到 IP 地址。
|
对某有序表进行折半查找(二分查找)时,进行比较的关键字序列不可能是( )。 |
| A.42, 61, 90, 85, 77 |
| B.42, 90, 85, 61, 77 |
| C.90, 85, 61, 77, 42 |
| D.90, 85, 77, 61, 42 |
| C |
|
折半查找的核心约束是:每次比较后,下一个被比较的元素必须严格落在当前搜索区间的上下界之内。如果某个元素超出了当前合法区间,这个序列就不可能由折半查找产生。 用区间追踪法逐一验证,每次记录当前合法的 $(\text{下界} \text{上界})$: 验证选项 C:90 85 61 77 42 比较 90,初始区间 $(-\infty +\infty)$,90 在区间内,合法。目标 < 90,下次搜索区间变为 $(-\infty 90)$。 比较 85,85 在 $(-\infty 90)$ 内,合法。目标 < 85,区间变为 $(-\infty 85)$。 比较 61,61 在 $(-\infty 85)$ 内,合法。目标 > 61,区间变为 $(61 85)$。 比较 77,77 在 $(61 85)$ 内,合法。目标 < 77,区间变为 $(61 77)$。 比较 42,42 在 $(61 77)$ 内吗?42 < 61,完全不在这个区间里!这是不可能出现的。 所以选项 C 不可能由折半查找产生。 验证其他选项时都能找到合法的区间路径:选项 ABD 各步骤的元素始终在合法区间内,均可能出现。 答案是 C。 |
|
|
| A. |
| B. |
| C. |
| D. |
| B |
|
按顺序逐一计算每个关键码的散列地址,发生冲突就往后找第一个空槽。 $23 % 11 = 1$,槽 1 空,存入 1。 $40 % 11 = 7$,槽 7 空,存入 7。 $91 % 11 = 3$(因为 $91 = 8 \times 11 + 3$),槽 3 空,存入 3。 $17 % 11 = 6$,槽 6 空,存入 6。 $19 % 11 = 8$,槽 8 空,存入 8。 $10 % 11 = 10$,槽 10 空,存入 10。 $31 % 11 = 9$(因为 $31 = 2 \times 11 + 9$),槽 9 空,存入 9。 $65 % 11 = 10$(因为 $65 = 5 \times 11 + 10$),槽 10 已被 10 占用,发生冲突!线性探查往后找:槽 $10 + 1 = 11$,超出范围循环回槽 0,槽 0 空,存入 0。 $26 % 11 = 4$(因为 $26 = 2 \times 11 + 4$),槽 4 空,存入 4。 最终结果:0→65,1→23,2→空,3→91,4→26,5→空,6→17,7→40,8→19,9→31,10→10。 这与选项 B 完全吻合。选项 A 把 10 和 65 的位置弄反了,说明错误地让 65 先于 10 插入。 答案是 B。 |
排序算法
排序算法选择原则
选择排序方法时要根据实际情况判断:$n$ 较小时用直接插入选择或冒泡数据基本有序时用直接插入(此时近似 $O(n)$)$n$ 很大且关键字位数少时用基数排序$n$ 很大时优先用 $O(n\log_2 n)$ 的快速排序堆排序或归并排序。
各排序算法对比
| 排序方法 | 平均时间复杂度 | 最优情况 | 最差情况 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|---|
| 直接插入 | $O(n^2)$ | $O(n)$(基本有序) | $O(n^2)$ | $O(1)$ | 稳定 |
| Shell排序 | $O(n^{1.3})$ | — | — | $O(1)$ | 不稳定 |
| 直接选择 | $O(n^2)$ | — | — | $O(1)$ | 不稳定 |
| 堆排序 | $O(n\log_2 n)$ | — | — | $O(1)$ | 不稳定 |
| 冒泡排序 | $O(n^2)$ | $O(n)$(基本有序) | $O(n^2)$ | $O(1)$ | 稳定 |
| 快速排序 | $O(n\log_2 n)$ | — | $O(n^2)$(基本有序) | $O(1)$ | 不稳定 |
| 归并排序 | $O(n\log_2 n)$ | — | — | $O(n)$ | 稳定 |
| 基数排序 | $O(d(n+rd))$ | — | — | $O(rd)$ | 稳定 |
堆的概念
堆是一棵完全二叉树,用数组存储,下标从 1 开始。大顶堆要求每个节点的值都大于等于其子节点,根节点是最大值。构建堆时从最后一个非叶子节点(位置 $\lfloor n/2 \rfloor$)开始,从右到左逐个做"下沉"调整。
快速排序的划分
选一个基准元素,把比它小的放左边,比它大的放右边,基准元素到达最终位置,这叫一趟划分。一趟划分需要扫描整个区间一遍,时间是 $O(n)$。
哪些排序第一趟能确定某元素的最终位置
冒泡排序第一趟结束后最大元素沉底,位置确定。简单选择排序第一趟找到最小元素放到第一位,位置确定。堆排序建堆后取出堆顶,位置确定。快速排序第一趟划分后基准元素位置确定。直接插入和归并排序第一趟后不能保证任何元素到达最终位置。
实际应用
数据库 ORDER BY 底层用归并排序(稳定且适合大数据)。Java 的 Arrays.sort() 对基本类型用快速排序,对对象用归并排序(保证稳定性)。优先队列(如任务调度)用堆实现。
|
对数组 $A=(2,8,7,1,3,5,6,4)$ 构建大顶堆为( )(用数组表示)。 |
| A.(1,2,3,4,5,6,7,8) |
| B.(1,2,5,4,3,7,6,8) |
| C.(8,4,7,2,3,5,6,1) |
| D.(8,7,6,5,4,3,2,1) |
| C |
|
原数组 $(28713564)$ 对应的完全二叉树结构如下:
括号内是数组下标。$n=8$,最后一个非叶子节点是下标 $\lfloor 8/2 \rfloor = 4$,即值为 1 的节点,从它开始往前逐个下沉调整。 下标4节点(值1): 左子节点是下标8(值4),1 < 4,交换,变成 $(28743561)$。 下标3节点(值7): 左子节点下标6(值5),右子节点下标7(值6),7 已经是最大,不用交换。 下标2节点(值8): 左子节点下标4(值4),右子节点下标5(值3),8 已经最大,不用交换。 下标1节点(值2): 左子节点下标2(值8),右子节点下标3(值7),8 最大,交换2和8,变成 $(82743561)$。2 下沉到下标2,再看下标2的子节点:左下标4(值4),右下标5(值3),4 最大,交换2和4,变成 $(84723561)$。2 下沉到下标4,下标4的子节点是下标8(值1),2 > 1,不用再交换。 最终大顶堆为 $(84723561)$,对应的树结构:
每个节点都大于其子节点,满足大顶堆。 答案是 C,$(84723561)$。 |
|
对于一个初始无序的关键字序列,在下面的排序方法中,( )第一趟排序结束后,一定能将序列中的某个元素在最终有序序列中的位置确定下来。 |
| A.①②③⑥ |
| B.①②③⑤⑥ |
| C.②③④⑤ |
| D.③④⑤⑥ |
| C |
|
解析: 逐个分析每种排序第一趟结束后的情况: ①直接插入排序: 第一趟把第 2 个元素插入到它与第 1 个元素的正确相对位置,但整体还没排好,两个元素都不一定是最终位置。❌ ②冒泡排序: 第一趟从左到右两两比较,最大的元素被"冒"到最右端,最大元素的最终位置(最后一位)确定。✓ ③简单选择排序: 第一趟从全部元素中找出最小值,放到第一位,第一个位置的元素确定为最终值。✓ ④堆排序: 建完初始堆后,堆顶是最大元素,第一趟把堆顶取出放到末尾,末尾元素的最终位置确定。✓ ⑤快速排序: 第一趟划分结束后,基准元素落到最终位置,左边全小于它,右边全大于它。✓ ⑥归并排序: 第一趟是把相邻两个元素两两归并成有序对,例如 $(28)$$(17)$……,每对内部有序,但任何一个元素都不能确认是全局最终位置。❌ 满足条件的是②③④⑤。 答案是 C,②③④⑤。 |
信息安全
信息安全基础知识
信息安全的三个核心属性(CIA三角)
| 属性 | 含义 | 举例 |
|---|---|---|
| 保密性 | 信息不被未授权的人获取 | 加密通信访问控制 |
| 完整性 | 信息未经授权不能被修改,保持真实准确 | 数字签名哈希校验 |
| 可用性 | 授权用户能够在需要时正常访问信息 | 备份容灾防DDoS |
其他重要安全属性
不可抵赖性(也叫不可否认性):通信双方不能事后否认自己发送或接收过某条消息,通过数字签名实现。
可认证性:能够验证通信方的真实身份,通过数字证书实现。
实际应用
银行系统要保证转账金额不被篡改(完整性)医疗记录不能被无关人员查看(保密性)电商平台要保证用户下单后商家无法否认(不可抵赖性)。
加密技术与认证技术
对称加密与非对称加密
| 类型 | 原理 | 优点 | 缺点 | 代表算法 |
|---|---|---|---|---|
| 对称加密 | 加密和解密用同一把密钥 | 速度快 | 密钥分发困难 | AESDES |
| 非对称加密 | 公钥加密,私钥解密(或反之) | 密钥分发安全 | 速度慢 | RSA |
数字证书
数字证书由权威的第三方机构(CA,证书颁发机构)签发,里面包含用户的公钥和身份信息。其他人通过验证证书来确认"这个公钥确实属于这个人",解决了公钥的真实性问题,用于身份认证。
数字签名
数字签名用发送方的私钥对消息(或其哈希值)进行加密生成签名,接收方用发送方的公钥验证。因为私钥只有发送方持有,所以签名无法伪造,发送方也无法事后否认,用于保证不可抵赖性。
digraph digital_sign {
rankdir=LR
node [shape=box]
A [label="发送方A"]
B [label="接收方B"]
sign [label="用A的私钥\n对消息签名"]
verify [label="用A的公钥\n验证签名"]
A -> sign -> B -> verify
verify -> ok [label="验证通过\n=身份确认+不可否认"]
ok [label="不可抵赖\n✓" shape=ellipse style=filled fillcolor=lightgreen]
}
数字证书 vs 数字签名的区别
数字证书解决的是"你是谁"的问题(身份认证),数字签名解决的是"这消息确实是你发的"的问题(不可抵赖)。
实际应用
HTTPS 网站用数字证书让浏览器确认服务器身份。电子合同用数字签名保证签名方无法否认。网上银行同时用两者:证书认证身份,签名保证交易不可抵赖。
|
所有资源只能由授权方或以授权的方式进行修改,即信息未经授权不能进行改变的特性是指信息的( )。 |
| A.完整性 |
| B.可用性 |
| C.保密性 |
| D.不可抵赖性 |
| A |
|
题目的关键句是"未经授权不能进行改变",强调的是信息不能被随意修改,只有授权的人才能改动。这描述的是信息的完整性——信息保持真实未被篡改的状态。 逐个排除其他选项。可用性强调的是授权用户能随时访问和使用信息,和"能不能改"无关,排除B。保密性强调的是信息不被未授权的人获取和查看,关注的是"看"而不是"改",排除C。不可抵赖性强调的是通信方不能否认自己的行为,和信息能否被改变无关,排除D。 答案是 A,完整性。 |
|
用户 A 和 B 要进行安全通信,通信过程需确认双方身份和消息不可否认。A 和 B 通信时可使用( )来对用户的身份进行认证。 |
| A.数字证书 |
| B.消息加密 |
| C.用户私钥 |
| D.数字签名 |
| A |
|
题目问的是"对用户身份进行认证",也就是要解决"你是谁你的公钥是否真实可信"的问题。 数字证书由权威CA机构签发,里面包含用户的公钥和身份信息,CA用自己的私钥背书,其他人可以用CA的公钥验证证书的真实性。通过验证数字证书,就能确认"对方的公钥确实是对方的",完成身份认证。这正是数字证书的核心用途。 消息加密只保证内容保密,不能证明身份,排除B。用户私钥是自己保存的,不能直接用来给对方认证自己的身份,排除C。数字签名用于保证不可抵赖,是第二问的答案,不是身份认证,排除D。 答案是 A,数字证书。 |
|
A 和 B 通信时可使用( )确保消息不可否认。 |
| A.数字证书 |
| B.消息加密 |
| C.用户私钥 |
| D.数字签名 |
| D |
|
题目问的是"确保消息不可否认",也就是发送方事后不能否认自己发过这条消息。 数字签名的原理是:发送方用自己的私钥对消息做签名,接收方用发送方的公钥验证。因为私钥只有发送方一个人持有,签名无法伪造,发送方也无法否认"这个签名不是我做的"。所以数字签名天然保证了不可抵赖性。 数字证书用于身份认证,是第二题的答案,排除A。消息加密只保证内容不被第三方看到,不能防止发送方否认,排除B。用户私钥是生成签名的工具,但直接说"用私钥"是不完整的表述,而且私钥不对外公开,接收方无法用私钥来验证,完整的机制叫"数字签名",排除C。 答案是 D,数字签名。 |
面向对象技术
|
|
| A. cat 和 bird 可看作是 Animal 的对象 |
| B.parrot 和 bird 可看作是 Animal 的对象 |
| C.bird 可以看作是 Parrot 的对象 |
| D.parrot 可以看作是 Bird 的对象 |
| C |
|
面向对象里有一条核心规则叫"里氏替换原则":子类的对象可以被看作父类的对象,但父类的对象不能被看作子类的对象。用大白话说就是,儿子可以顶替父亲的位置,但父亲不能顶替儿子的位置。 选项 A:cat 是 Cat 的对象,Cat 是 Animal 的子类,所以 cat 可以向上看作 Animal 的对象。bird 是 Bird 的对象,Bird 是 Animal 的子类,所以 bird 也可以看作 Animal 的对象。A 正确。 选项 B:parrot 是 Parrot 的对象,Parrot 继承自 Bird,Bird 继承自 Animal,继承关系可以传递,所以 parrot 也可以向上看作 Animal 的对象。bird 同 A 中分析。B 正确。 选项 C:bird 是 Bird 的对象,Parrot 是 Bird 的子类,也就是说 Parrot 比 Bird 更具体功能更多。bird 只是一个 Bird,它不具备 Parrot 特有的能力,所以 bird 不能被看作 Parrot 的对象。这就像"鸟"不能被当作"鹦鹉",因为鹦鹉是鸟的子集,不是所有鸟都是鹦鹉。C 不正确,这就是答案。 选项 D:parrot 是 Parrot 的对象,Parrot 是 Bird 的子类,所以 parrot 可以向上看作 Bird 的对象。D 正确。 答案是 C,bird 不能看作是 Parrot 的对象。 |
|
假设 Animal 类中定义接口 move(),Bird、Cat 和 Parrot 分别实现自己的 move(),调用 move() 时,不同对象收到同一消息可以产生各自不同的结果,这一现象称为( )。 |
| A.封装 |
| B.继承 |
| C.消息传递 |
| D.多态 |
| D |
|
解析: 题目描述的现象是:同一个方法名 move(),bird 调用时是飞,cat 调用时是走,parrot 调用时是爬树或飞,同样的消息(调用 move())发给不同对象,得到不同的行为结果。这就是多态的定义。 逐个排除其他选项。封装是指把数据和操作数据的方法包在一起,隐藏内部实现细节,对外只暴露接口,强调的是"隐藏",和"同一消息不同结果"无关,排除 A。继承是子类获得父类的属性和方法,强调的是"复用",不是"同一消息不同结果",排除 B。消息传递是面向对象里对象之间通信的方式,题目说的是消息产生的不同结果,不是通信机制本身,排除 C。 多态的本质就是:同一个接口,不同的实现,调用时根据对象的实际类型决定执行哪个版本的方法。BirdCatParrot 各自重写了 move(),运行时根据对象类型自动选择对应的实现,这就是多态。 答案是 D,多态。 |
|
UML 中关联是一个结构关系,描述了一组链。两个类之间( )关联。 |
| A.不能有多个 |
| B.可以有多个由不同角色标识的 |
| C.可以有任意多个 |
| D.多个关联必须聚合成一个 |
| B |
|
关联(Association)在 UML 类图中表示两个类之间存在某种结构性的联系,比如"学生"和"课程"之间的选课关系。 两个类之间确实可以存在多个关联,但这些关联必须有所区分,不能完全相同。区分的方式是给每个关联的端点标上不同的角色名(Role Name)。比如一个"人"和"公司"之间,可以同时存在"雇员"关联和"客户"关联,这两个关联用不同的角色名来标识,是完全合法的。 选项 A 说不能有多个,这是错的,两个类之间可以有多个关联,排除 A。 选项 B 说可以有多个,但必须用不同的角色来标识,这是正确的。不同角色名让每个关联都有明确的语义,互相区分,不会混淆。 选项 C 说可以有任意多个,表述过于宽泛,没有说明需要用角色区分的限制条件,不够准确,排除 C。 选项 D 说多个关联必须聚合成一个,这是完全错误的,聚合(Aggregation)是另一种关系,和这里讨论的情况无关,排除 D。 答案是 B,两个类之间可以有多个由不同角色标识的关联。 |
计算机网络
|
以下关于网络层次与主要设备对应关系的叙述中,配对正确的是( )。 |
| A.网络层——集线器 |
| B.数据链路层——网桥 |
| C.传输层——路由器 |
| D.会话层——防火墙 |
| B |
|
现在逐个分析每个选项: 选项 A 说网络层对应集线器,这是错的。集线器工作在第1层物理层,它只负责把电信号放大转发,完全不看地址,不做任何判断,就像一个简单的电信号广播器。网络层对应的设备是路由器,排除 A。 选项 B 说数据链路层对应网桥,这是正确的。网桥工作在第2层数据链路层,它能识别 MAC 地址(数据链路层地址),根据 MAC 地址决定是否转发数据帧,能隔离不同网段的冲突域。现在常见的交换机本质上也是多端口网桥,同样工作在数据链路层。B 正确。 选项 C 说传输层对应路由器,这是错的。路由器工作在第3层网络层,它识别 IP 地址,根据路由表决定数据包转发路径。传输层对应的设备是网关或四层交换机,排除 C。 选项 D 说会话层对应防火墙,这是错的。防火墙可以工作在多个层次,但通常说的防火墙工作在网络层到传输层,检查 IP 地址和端口号来过滤流量,不是会话层的设备,排除 D。 答案是 B,数据链路层——网桥。 |
软件工程基础知识
|
敏捷开发方法 Scrum 的步骤不包括( )。 |
| A.Product Backlog |
| B.Refactoring |
| C.Sprint Backlog |
| D.Sprint |
| B |
|
Scrum 是一种敏捷开发框架,它的核心流程包含三个关键步骤: Product Backlog(产品待办列表)是第一步,把所有需求功能按优先级列成一张清单,整个项目要做什么都在这里。 Sprint Backlog(迭代待办列表)是第二步,从 Product Backlog 里挑出本次迭代要完成的任务,形成本轮的任务清单。 Sprint(冲刺/迭代)是第三步,团队用 1-4 周时间完成 Sprint Backlog 里的任务,交付可运行的软件。 Refactoring(重构)是极限编程(XP,eXtreme Programming)的核心实践,不是 Scrum 的步骤。重构是在不改变软件外部行为的前提下改善代码内部结构,它属于 XP 的工程实践,Scrum 本身并不包含这一步骤。 答案是 B,Refactoring。 |
|
以下关于软件维护的叙述中,不正确的是( )。 |
| A.软件维护解决软件产品交付用户之后运行中发生的各种问题 |
| B.软件维护期通常比开发期长得多,投入也大得多 |
| C.软件的可维护性是软件开发阶段各个时期的关键目标 |
| D.相对于软件开发任务而言,软件维护工作要简单得多 |
| D |
|
考点:第5章 软件工程基础知识 · 5.6 运行和维护知识 逐个分析每个选项。 选项 A 说软件维护解决的是交付之后运行中的各种问题,这是软件维护的标准定义,完全正确。 选项 B 说维护期比开发期长投入也更大,这也是正确的。软件工程统计数据表明,一个软件系统整个生命周期中,维护阶段的时间通常占 60%-80%,花费的总成本也远超开发阶段,因为软件要持续使用多年甚至几十年。所以 B 是正确的说法。 选项 C 说可维护性是软件开发各个阶段的关键目标,这也是正确的。从需求分析到设计到编码,每个阶段都要考虑将来怎么维护,比如代码写得清晰模块化程度高,都是为了提高可维护性。 选项 D 说软件维护工作比软件开发简单得多,这是错误的。软件维护实际上非常复杂,原因有很多:维护人员往往不是原来的开发人员,要看懂别人写的代码很难代码经过多次修改后结构越来越混乱改了一处可能引发其他地方的连锁问题维护文档往往不完整。实际上软件维护的难度丝毫不亚于开发,有时甚至更难。 答案是 D,软件维护工作要简单得多这一说法是不正确的。 |
|
UP(统一过程)是用例驱动的、以架构为核心、迭代和增量的软件过程框架,它提供了一种( )的特性。 |
| A.演进 |
| B.敏捷 |
| C.测试驱动 |
| D.持续集成 |
| A |
|
解析: UP(Unified Process,统一过程)是软件工程里一个重要的过程框架,它有四个核心特征需要记住:用例驱动以架构为核心迭代增量。题目已经在描述里说出了三个,然后问第四个特征对应哪个选项。 题目中明确提到了"迭代和增量",这两个词加在一起,描述的是软件不是一次性开发完成的,而是一轮一轮地迭代,每轮都在上一轮的基础上增加新功能,整个系统像生物进化一样逐步完善。这种"一步步进化逐步完善"的特性就叫演进(Evolutionary)。 敏捷是另一类开发方法的统称(如 ScrumXP),虽然 UP 有些敏捷思想,但"敏捷"不是 UP 的核心定义特征,排除 B。 测试驱动(TDD)是极限编程(XP)的核心实践,先写测试再写代码,和 UP 的特征无关,排除 C。 持续集成(CI)是 DevOps 和敏捷开发中的工程实践,指频繁地把代码合并到主干并自动构建测试,同样不是 UP 的定义特征,排除 D。 答案是 A,演进。 UP 通过迭代和增量的方式让系统逐步演进,每次迭代都产生可运行的版本,系统在迭代中不断完善,体现的就是演进的特性。 |
面向对象技术
|
在下列机制中,( )是指过程调用和响应调用所需执行的代码在运行时加以结合;而( )是指过程调用和响应调用所需执行的代码在编译时加以结合。 |
| A.消息传递 |
| B.类型检查 |
| C.静态绑定 |
| D.动态绑定 |
| E.消息传递 |
| F.类型检查 |
| G.静态绑定 |
| H.动态绑定 |
| DG |
|
绑定是指把一个方法调用和具体执行哪段代码关联起来的过程。 动态绑定是指这个关联在程序运行时才确定。比如 Python 里调用 静态绑定是指这个关联在编译时就确定了。比如 C++ 里普通函数调用,编译器在编译阶段就已经知道调用哪个具体函数,运行时直接执行,速度快但不灵活。题目第二空说"在编译时加以结合",对应的是静态绑定。 消息传递是面向对象里对象之间通信的机制,和绑定时机无关,两空都排除 A。类型检查是验证变量类型是否合法的过程,也和绑定时机无关,两空都排除 B。 问题1答案是 D,动态绑定。问题2答案是 G,静态绑定。 |
数据库技术基础
|
若关系 R(H, L, M, P) 的主键为全码(All-key),则关系 R 的主键应( )。 |
| A.为 HLMP |
| B.在集合 {H, L, M, P} 中任选一个 |
| C.在集合 {HL, HM, HP, LM, LP, MP} 中任选一个 |
| D.在集合 {HLM, HLP, HMP, LMP} 中任选一个 |
| A |
|
先理解几个基本概念。主键是能唯一标识一条记录的最小属性集合。全码(All-key)是一个特殊情况,指关系的所有属性合在一起才能唯一标识一条记录,缺少任何一个属性都不行。 全码的含义就是:整个属性集 {H L M P} 共同构成主键,少一个都不能唯一确定一条记录。换句话说,单独一个属性不能做主键,任意两个属性也不能做主键,任意三个属性也不能做主键,只有四个属性全部放在一起才能做主键。 选项 B 是从四个属性里任选一个做主键,那是单属性主键,不是全码,排除。 选项 C 是任选两个属性做主键,也不是全码,排除。 选项 D 是任选三个属性做主键,也不是全码,排除。 选项 A 说主键是 HLMP,即四个属性全部作为主键,这正是全码的定义,正确。 答案是 A,主键为 HLMP。 |
评论区(0 条)
发表评论⏳ 加载编辑器…