ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

834数据结构与计算机网络:知识地图与高频题手算流程

834数据结构与计算机网络:知识地图与高频题手算流程

简介:面向备考华中科技大学834计算机综合的考生,这份2019年考研真题PDF可用于自测与考点复盘,帮助把握专业课命题范围与题型分布。包内为1个PDF文件,大小约1.37MB,集中呈现原卷题目,便于打印或平板标注练习。已有585人学习下载,具备一定参考热度。内容覆盖选择题、填空题、判断题及综合计算题,涉及串与线性表、链表与栈队列操作、数组访问、二叉树遍历、图遍历与邻接表、排序与哈夫曼编码等数据结构要点,并延伸至计算机网络的多址访问、FTP、SR、CSMA/CA、IP地址、OSPF、DNS及OSI/TCP参考模型等考点。适合需要按真题查漏补缺、训练答题节奏并梳理高频知识模块的考研复习者使用。

1. 知识地图:834 这张卷子的两条主干

834 这份卷子的知识地图只有两条主干,一条是数据结构,一条是计算机网络,两条主干在题型上高度对称:选择题考概念边界,填空题考手工推演,判断题考“反例意识”,计算题考流程熟练度。数据结构那条主干从存储结构出发,依次经过串、链表、数组、栈队、树、图、查找、排序;计网那条主干从分层模型出发,依次经过链路层、网络层、传输层、应用层,中间穿插 CRC、路由表、子网划分这类必须动笔的计算。适合两类人:已经把教材过了一遍、但做题时仍然“知道概念却写不出过程”的;以及时间不多、想靠一份地图快速定位薄弱模块的。这份笔记的写法是先给结构代价表,再给每类题的手算流程,最后落到易漏点和复习清单,图的是让你从“看得懂”走到“写得出”。

2. 存储与操作:从串、链表到树和图的同一套追问

数据结构这一半的题,看起来分散在七八个章节里,但提问方式高度统一:给定一种存储结构,问某种操作的时间代价;或者给定一种操作频次,问该选哪种存储结构。把这条主线抓住,选择题里至少一半的题不用背答案,推一遍就出来了。下面按“概念边界—指针操作—遍历还原—度数与表结点”四层往下拆。

2.1 线性表与串:概念题的三个判据

串这类题的坑不在算法,在定义边界。判据只有三个:元素类型、长度是否可为 0、操作单位是单个元素还是子串。串的元素限定为字符,长度可以为 0,基本操作以子串为对象而不是单个元素,所以它是“特殊的线性表”,特殊在数据元素被限定成字符集里的一个字符。

常见说法判定判据
空串就是空白串错空串长度为 0;空白串由空格字符组成,长度至少为 1
串中元素只能是字母错字符可以是字母、数字、符号,取决于字符集
串的长度必须大于零错空串是合法串,长度为 0
串是一种特殊的线性表对逻辑结构同线性表,元素限定为字符

最容易混的是“空串”和“空格串”:前者一个字符都没有,后者由空格构成,长度等于空格个数。判断题里还有一种问法绕在存储上:“两个串相等”指长度相等且对应位置字符相等,和它们在内存里占多少字节没有关系,顺序存储和链式存储只影响操作代价,不影响相等判定。这条判据立住,串相关的选择题和判断题基本不会失分。

2.2 链表与栈队:结构选型与指针操作

链表选型题的题干几乎都是同一句话:“最常用的操作是在最后一个结点之后插入一个结点、删除第一个结点,采用哪种结构效率最高。”解法是把两个操作的代价分别算出来,再看有没有并列,并列时再比空间开销。先把四种常见结构的代价摆出来:

结构尾部插入删除第一个结点
带头指针的单向循环链表O(n),要走到尾O(1),head->next 即首元
带尾指针的单向循环链表O(1)O(1),rear->next 即首元
带头指针的双向循环链表O(1),head->prev 即尾O(1)
带尾指针的双向循环链表O(1)O(1)

判据是“时间打平之后看辅助空间”:双向链表每个结点多一个前驱指针,同样的数据要多占一份指针空间;带尾指针的单向循环链表只多一个尾指针变量,两个操作都是 O(1)。梳理下来会发现,这一题最容易出错的地方不是不会算复杂度,而是算完第一列就下结论——只看到“删除第一个结点 O(1)”就选了带头指针的结构,尾部插入那列直接忽略。写成代码更直观:

/* 带尾指针 rear 的单向循环链表:尾插和删首都是 O(1) */ typedef struct Node { int data; struct Node *next; } Node; /* 尾插:新结点先接住原来的首元,再挂到旧尾之后,最后自己变成新的尾 */ void push_back(Node **rear, Node *t) { t->next = (*rear)->next; /* 新结点的 next 指向原首元,维持循环 */ (*rear)->next = t; /* 旧尾结点的 next 指向新结点 */ *rear = t; /* 尾指针前移,指向新的尾结点 */ } /* 删首:首元就是 rear->next,一步就能摘下来 */ int pop_front(Node *rear) { Node *first = rear->next; /* 首元结点 */ int v = first->data; /* 先存值,再释放 */ rear->next = first->next; /* 尾结点直接跨过首元 */ free(first); return v; }

两个函数的参数含义要记清:rear是指向尾指针变量的指针,因为尾插要修改尾指针本身;pop_front只读尾指针,所以直接传Node *。删除时如果链表只剩一个结点,rear->next指向自己,跨过之后rear会悬空,实际实现里要单独判一次长度,这也是手写代码题常见的扣分点。

栈和队列的题几乎都是“指针操作序列”四选一。链式栈删除栈顶就是top = top->next,因为栈顶指针本来就指向待删结点,删完自然落到下一个;链式队列入队则要看rear的定义,如果rear指向队尾元素,新结点要挂在rear后面再前移;如果rear指向“将要入队的空位置”,写法就变成先填rear再前移。这两种约定在很多教材里并存,题目一定会给出前提,读题时先把“rear 指向什么”圈出来,比背四种写法管用。

循环队列的判断条件也是同样的思路:牺牲一个单元区分队空队满时,队空是front == rear,队满是(rear + 1) % MAX == front;用计数器或标志位时判断条件就换一套。判断题里“循环队列入队可能发生上溢”这句话本身是对的——队列满时再入队就是上溢,循环只是把假溢出消除了,真溢出依然存在。这类题的失分点通常不是不会,而是把“假溢出”和“上溢”当成一个东西。

2.3 二叉树:两种遍历序列推第三种

遍历还原题给了两种序列,求第三种,解法是固定的三步:从前序(或后序)里取根,到中序里用根的位置切出左右子树,再对两棵子树递归。整个过程全靠中序提供“左右分界”的信息,所以题型里若只给前序和后序,答案不唯一,这是判断题常考的点。

拿一组结构相同的例子走一遍,前序ABDEC,中序DBEAC:

步骤前序中序结论
1A B D E CD B E A C根是 A,左子树 DBE,右子树 C
2B D ED B E左子树根是 B,左 D,右 E
3CC右子树只有一个结点

先左后右地写后序:D、E、B、C、A,得到DEBCA。手写时建议每一步都在中序串上划一道竖线,把左右子树圈出来,否则递归到第三层很容易把左右弄反。另一个高频错法是前序序列抄错一位,比如把ABDEC记成ABDCE,后面全盘皆输,所以做题时先把给的序列原样抄到草稿纸最上方,再开始推。

如果题目反过来,给中序和后序求前序,流程一样,只是根从后序的末尾取。给前序和后序求中序则答“不能唯一确定”,理由是前序和后序只能确定根和子树的边界,无法区分只有一个孩子时它挂在左边还是右边。这条“不能确定”本身就是考点,不要硬凑一个答案。

2.4 哈夫曼树与图:度数、表结点与遍历起点

哈夫曼树的题集中在两个结论上。第一个是度数分布:哈夫曼树是严格二叉树,每个非叶结点都有两个孩子,所以不存在度为 1 的结点,问“度为 1 的结点有几个”答案恒为 0;配合n0 = n2 + 1和n = n0 + n2,任意给定结点总数都能反推出叶子数(n + 1) / 2。第二个是编码长度:两个字符频度相同,编码长度不一定相同,因为哈夫曼树的形态取决于每次合并时选哪两个结点,频度相同只是权重相同,深度仍可能不同。这一条在判断题里出现的频率很高,判错的原因多半是把“权重相同”直接等同于“路径长度相同”。

图的题分两类。一类是计数:有向图的邻接表里,表头结点数等于顶点数 n,表结点数等于边数 m,每个表结点对应一条有向边;如果是无向图,同一条边在两个顶点的链表里各出现一次,表结点数就是 2m,答题时看清是有向还是无向再下笔。另一类是遍历序列:BFS 从某个顶点出发,序列取决于邻接表中边结点的排列顺序,所以“某种序列可以得到”这类问法只要检查层序关系是否成立即可,不必和标准答案逐字对上;而“一次 DFS 能访问到所有顶点”这种说法,只有在图连通时才成立,非连通图要从每个连通分量各起一次,这是判断题的常客。

最小生成树的手算流程要练到不用想:Kruskal 先把边按权值升序排,从小到大取边,用并查集判环,形成回路就跳过,取到 n-1 条边为止;Prim 则从一个顶点出发,每次把“连接已选集合和未选集合的最小边”拉进来。两种方法结果权值相同,但选边顺序可能不同,题目问“依次得到的各条边”时按你用的那种方法写,不要混着写。拓扑排序则每次输出入度为 0 的顶点并删掉它的出边,若同时有多个入度为 0 的顶点,序列不唯一,答题时写一种并说明“答案不唯一”更稳。

3. 查找与排序:判定类题目的固定套路

这一块的题有个共同特征:答案不是“选哪个算法”,而是“第几趟之后序列长什么样”“平均查找长度是多少”。也就是说,考的是手工执行流程的准确度,而不是算法思想的理解深度。四类题各有固定的推演套路,练熟套路比记结论有用。

3.1 散列表:线性探测与平均查找长度

线性探测的流程是:算H(k),位置空就放,被占就往后顺延一位,走到表尾回到表头继续找。给一组关键字(26, 32, 9, 28, 14, 68, 46),表长 7,H(k) = k mod 7,逐个插入:

关键字H(k)探测过程落位
2655 空5
3244 空4
922 空2
2800 空0
1400 占,1 空1
6855 占,6 空6
4644、5、6、0、1、2 均占,3 空3

最终表是0:28, 1:14, 2:9, 3:46, 4:32, 5:26, 6:68。平均查找长度按“查找成功时比较了几次”算:26、32、9、28 各 1 次,14 探测 2 次,68 探测 3 次,46 从下标 4 一路查到下标 3,共 7 次。ASL = (1+1+1+1+2+3+7) / 7 = 16/7 ≈ 2.29。

这段流程写成代码跑一遍最不容易错:

def linear_probe(keys, m): table = [None] * m probes = [] # 记录每个关键字成功查找时的比较次数 for k in keys: idx = k % m # 散列函数 H(k) = k mod m cnt = 1 while table[idx] is not None: # 冲突就顺延,cnt 记录探测次数 idx = (idx + 1) % m cnt += 1 table[idx] = k probes.append(cnt) # 比较次数 = 探测到的位置数 return table, probes table, probes = linear_probe([26, 32, 9, 28, 14, 68, 46], 7) print(table, probes, sum(probes) / len(probes))

probes里存的次数就是每个关键字查找成功时的比较次数,直接求和除以关键字个数就是 ASL。要注意两点:一是“探测次数”和“比较次数”在查找成功的语境下相等,但查找失败时的比较次数要算到遇到空位置为止,两者不能混;二是装填因子越大,线性探测的聚集越严重,ASL 会明显上升,题目如果改表长或改冲突处理方法,流程不变,数字要重算。

3.2 二分查找:判定树与比较次数

二分查找的题三问:判定树长什么样、查某个值比较几次、ASL 是多少。套路是把有序表按mid = (low + high) / 2向下取整反复切分,画成一棵二叉排序树。序列(14, 19, 25, 36, 48, 51, 63, 84, 91)共 9 个元素,第一次mid = 5,取 48 作根;左半 1 到 4,mid = 2,取 19;右半 6 到 9,mid = 7,取 63;继续切,得到第 3 层 14、25、51、84,第 4 层 36、91。

查 84 的过程是 48 → 63 → 84,比较 3 次。ASL 按层累加:第 1 层 1 个结点乘 1,第 2 层 2 个乘 2,第 3 层 4 个乘 3,第 4 层 2 个乘 4,ASL = (1 + 4 + 12 + 8) / 9 = 25/9 ≈ 2.78。这题最容易错的地方是判定树的形状:mid取上整还是下整会画出不同的树,比较次数和 ASL 都会变,所以答题时先写一句“取mid = ⌊(low+high)/2⌋”,把自己的约定交代清楚,阅卷时不会因为形状不同被误判。另外,二分查找只对顺序存储的有序表成立,链表上没法随机访问中间元素,这条在选择题里也常考。

3.3 插入排序与快速排序:每趟结果的推演

直接插入排序的“趟”指的是把第 i 个元素插入前面已排好的子序列,第 1 趟处理前两个元素。以(67, 40, 78, 15, 34, 99, 12)为例:

趟次结果
初始67, 40, 78, 15, 34, 99, 12
第 1 趟40, 67, 78, 15, 34, 99, 12
第 2 趟40, 67, 78, 15, 34, 99, 12
第 3 趟15, 40, 67, 78, 34, 99, 12
第 4 趟15, 34, 40, 67, 78, 99, 12

第 2 趟看起来没变化,是因为 78 比前面所有元素都大,直接放在末尾,这正是手算时容易怀疑自己算错的地方。判断第几趟结束的标志是“前 i+1 个元素局部有序”,不是“整个序列有序”,别提前收工。

快速排序的一趟划分有两种常见写法:挖坑法和交换法,手算时用挖坑法更稳。以(70, 71, 69, 21, 91, 14, 3)为例,取 70 为基准,low指向首元素,high指向末元素:

动作序列状态
初始70, 71, 69, 21, 91, 14, 3
high 找到 3,填到 low3, 71, 69, 21, 91, 14, 3
low 找到 71,填到 high3, 71, 69, 21, 91, 14, 71
high 找到 14,填到 low3, 14, 69, 21, 91, 14, 71
low 越过 69、21,找到 91 填到 high3, 14, 69, 21, 91, 91, 71
low 与 high 相遇,填入基准3, 14, 69, 21, 70, 91, 71

结果左边四个都小于 70,右边两个都大于 70,一趟划分完成。这题的失分点集中在“相遇位置填基准”这一步——很多人忘了把基准值放回去,直接写了个带重复元素的序列。另一种失分是没看清题目问的是“一趟”还是“第一趟结束后再对子区间排序”,前者只做一次划分,后者要做递归。

3.4 堆与堆排序:建堆、重建与“选前 k 小”

堆的题分两步:建初始堆、输出堆顶后重建。以(46, 81, 49, 41, 23, 79, 15, 67)建小根堆为例,按层序存进数组,下标从 1 开始,从n/2 = 4号结点往前逐个下沉:

处理结点动作数组状态
41孩子 67 更大,不动46, 81, 49, 41, 23, 79, 15, 67
49与较小的孩子 15 交换46, 81, 15, 41, 23, 79, 49, 67
81与较小的孩子 23 交换46, 23, 15, 41, 81, 79, 49, 67
46与 15 交换,再与 49 比较后停止15, 23, 46, 41, 81, 79, 49, 67

初始小根堆是15, 23, 46, 41, 81, 79, 49, 67。第一趟把堆顶 15 和末尾 67 交换,堆规模减一,再让 67 下沉:与较小的孩子 23 换,再与 41 换,得到23, 41, 46, 67, 81, 79, 49,后面接已排好的 15。

堆排序还有一个高频应用场景:“从 8000 个记录里用最快的方法选出最小的 14 个”。建一个 14 个元素的大根堆,剩下 7986 个元素逐个和堆顶比,比堆顶小就替换并下沉,最终堆里就是最小的 14 个。时间复杂度是O(n log k),k 是要选的个数,比整体排序的O(n log n)少一个数量级的常数因子。这道题的判据不是“堆排序最快”,而是“选前 k 个时堆最省”,题干里出现“最小/最大的 k 个”就该想到堆。

4. 计算机网络:一条从分层到 CRC 的计算链

计网这半张卷子的题有个明显特征:概念题和计算题各占一半,而且计算题都能落到具体数字上——子网地址、窗口大小、ACK 序号、CRC 余数。复习时按“分层归属 → 地址与路由 → 传输层序号 → 校验码”这条链往下走,比按教材章节顺序看更省时间。

4.1 体系结构:OSI 与 TCP/IP 的对应

两个模型的对应关系是必背项,答题时画一张对照表最清楚:

OSI 七层TCP/IP 四层典型协议/设备
应用层、表示层、会话层应用层HTTP、FTP、DNS、SMTP
传输层传输层TCP、UDP
网络层网际层IP、ICMP、ARP、OSPF
数据链路层、物理层网络接口层以太网、CSMA/CD、CSMA/CA

容易错的是 ARP 和 ICMP 的归属:ARP 负责把 IP 地址解析成 MAC 地址,工作在网络层与链路层之间,教材通常归到网络层;ICMP 是网络层的控制报文协议,和传输层的 TCP/UDP 不是一回事。CSMA/CD 和 CSMA/CA 都工作在数据链路层的 MAC 子层,不是物理层,这个点在填空题里出现过。

4.2 链路层:多址访问、CSMA/CD 与 CSMA/CA

多址访问协议分三大类:随机接入、受控接入、信道划分。随机接入包括 ALOHA、CSMA、CSMA/CD、CSMA/CA,特点是站点竞争信道、冲突后重传;受控接入包括轮询和令牌传递,特点是按序访问、不会冲突但有空闲开销;信道划分包括频分、时分、码分,按规则把信道切开。填空题问“多址访问协议可分为随机接入、受控接入和轮流协议”,填的就是这三类。

CSMA/CD 用于有线以太网,思路是“先听后发、边发边听、冲突停发、随机重发”,因为有线介质上可以检测冲突;CSMA/CA 用于无线局域网,无线网卡发送时无法同时可靠地监听,所以改成“先听后发、预约信道、确认应答”,用 RTS/CTS 和 ACK 规避冲突。两条协议的核心差别不是“一个检测一个避免”,而是“能不能在发送时检测到冲突”,答题时把这一句写上去,比罗列流程更能拿分。

4.3 网络层:子网地址、路由算法与 OSPF

192.168.211.99/24这类题要会三件事:写掩码、算网络地址、算可用主机数。/24表示前 24 位是网络位,掩码是255.255.255.0,网络地址把主机位全部清零得到192.168.211.0,广播地址把主机位全部置一得到192.168.211.255,可用主机地址是2^8 - 2 = 254个。算的时候把第四个字节转成二进制,按位与一遍,比背公式可靠。

路由选择算法按“是否随网络状态变化”分为静态路由和动态路由,按“交换的信息”分为距离向量算法和链路状态算法。距离向量算法每个结点只和邻居交换整张路由表,收敛慢,但实现简单;链路状态算法把链路状态泛洪到全网,每个结点用 Dijkstra 算最短路径,OSPF 用的就是这一套,所以“OSPF 的核心算法”填 Dijkstra 或最短路径优先。固定路由表题的做法是:从目标结点倒着找“下一跳”,比如结点 5 到结点 1,先看 5 的邻居里谁到 1 最近,再顺着填。

4.4 传输层与应用层:TCP 序号、FTP 双连接、DNS 查询

TCP 的 ACK 序号规则只有一条:确认号等于“期望收到的下一个字节的序号”,也就是已正确收到的最后一个字节序号加一。题目给一张报文交互图时,先找每个报文携带的数据长度,再累加,别把序号和确认号看反。SYN 洪泛攻击则是利用三次握手:攻击者发大量 SYN 报文但不发最后的 ACK,服务器维持大量半连接,耗尽资源。

选择重传 SR 的窗口约束是“发送窗口 + 接收窗口 ≤ 2^n”,n 是帧序号的位数,通常取发送窗口和接收窗口相等,此时最大为2^(n-1)。32 位序号时接收窗口最大是2^31,这个数字大得不像话,但公式就是这个,题目考的是公式不是实际工程。

FTP 用两个连接:控制连接走 21 端口,传命令和应答,整个会话期间保持;数据连接走 20 端口(主动模式下由服务器发起),每传一个文件建一次、传完就断。DNS 的查询分递归和迭代:递归查询是“你替我查到底”,本地域名服务器扛下所有后续工作;迭代查询是“你告诉我下一步该问谁”,本机自己一层层问。区分标志是“谁承担后续查询”,而不是“查了几次”。

4.5 CRC:模 2 除法的手算流程

CRC 的题三问:是否出错、发送的比特序列是什么、校验码是什么。生成多项式G(x) = x^4 + x^3 + 1对应比特串11001,最高次为 4,所以校验码 4 位。步骤是:数据后面补 4 个 0,用模 2 除法除以11001,余数就是 FCS,发送序列是原数据加余数。接收端用同样的多项式除整个序列,余数为 0 就没出错。

def crc_remainder(data_bits: str, poly: str) -> str: """data_bits 为原始比特串,poly 为生成多项式对应的比特串(最高位必为 1)""" r = len(poly) - 1 # 校验位长度 = 多项式阶数 work = list(data_bits + "0" * r) # 右侧补 r 个 0 for i in range(len(data_bits)): if work[i] == "1": # 当前位为 1 才需要做模 2 减法 for j in range(len(poly)): work[i + j] = str(int(work[i + j]) ^ int(poly[j])) return "".join(work[-r:]) # 余数即 FCS print(crc_remainder("1101011011", "11001")) # 输出 4 位校验码

模 2 减法就是异或,不进位也不借位,所以手算时把11001对齐到当前最高位的 1 上,逐位异或,再把结果前面的 0 划掉,重复到剩余位数小于 5 为止。最常见的错法是“补零个数”和“余数位数”搞混:补的零个数等于多项式阶数,余数也取这么多位,少了要左补零。另一个错法是多项式漏掉中间项,比如把x^4 + x^3 + 1写成111,中间缺的项必须用 0 占位。

5. 易漏点:五个最容易在考场上翻车的地方

这一章集中处理那些“看答案觉得简单、自己做就错”的点。它们不属于某个孤立知识点,而是判据、约定、前提条件上的偏差,单独拎出来过一遍,比刷十道同类题有用。

易漏点一:把大 O 记号当成算法优劣的唯一判据。复杂度为 O(n) 的算法一定优于 O(n²) 的算法,这句话在判断题里几乎必错。为什么漏:教材讲复杂度时为了方便比较,默认讨论的是“同一问题的不同算法在 n 趋于无穷时的增长趋势”,一旦脱离这个前提就不成立。怎么补:答题时补上三个限定——问题规模足够大、常数因子可比、比较的是最坏或平均情况;如果题目没给这些条件,就判“不能确定”。实际做题时还要考虑空间换时间、数据初始有序程度这些因素,比如插入排序在近乎有序的数据上接近 O(n),比快排还快。

易漏点二:链表选型只看时间不看空间。前面那道链表题,时间上双向循环链表和带尾指针的单向循环链表都能做到两个操作 O(1),差别只在每个结点多一个前驱指针。为什么漏:复习时习惯背“双向链表删除方便”,没养成把两个操作的代价同时列出来的习惯。怎么补:遇到“效率最高”的题,先在草稿纸上画一张两列的表,把题干提到的每个操作各算一次,打平之后再比空间和实现复杂度,把判据写出来,选择才有依据。

易漏点三:默认图的遍历序列唯一。同一个图,邻接表中边结点的排列顺序不同,BFS 和 DFS 得到的序列就不同。为什么漏:教材例题只给一个标准答案,做题时形成了“对答案”的习惯。怎么补:把“序列是否合法”当成判据,BFS 检查层序关系——序列中每个顶点的前驱必须在它之前出现且层数不增;DFS 检查是否存在“能走却没走”的分支。只要满足规则就是合法答案,题目问“可以得到”时不要去找唯一解。

易漏点四:静态链表“不移动元素”和“不分配空间”混为一谈。静态链表用数组模拟指针,插入删除只改游标,确实不移动元素;但它的空间是预先分配的,容量固定,这是它相对动态链表的劣势。为什么漏:两个“不”字挨得太近,背

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进