☰
数据结构期末冲刺指南:线性表、栈队列、树与并查集核心考点
2026/9/30 3:09:17 网站建设 项目流程

眼看数据结构期末考试一天天近了,教材翻了好几遍,线性表、栈、队列、二叉树、并查集这些名词看着都眼熟,可真拿到往年卷子还是一脸懵?这种状态我太熟悉了——几乎每个期末都有一批同学在考前几天才开始“真正学数据结构”,然后就陷入了“什么都看过,什么都写不出来”的焦虑。

这门课和高等数学不一样,它的难点不是公式推导,而是逻辑链条。你背下了顺序表和链表各自的优缺点,但考试把说法换了一下你就开始犹豫;你会默写中序遍历的递归代码,但要求改成非递归就无从下手;你听说过并查集这个名字,却完全不知道它到底能干什么。原因很简单:数据结构考的不是记忆,而是把逻辑用代码表达出来的能力。

这篇文章就是冲着这个痛点来的。本篇冲刺指南(上)覆盖课程前半段最常考的五个模块:线性表、栈队列、数组串、树与二叉树、并查集。每个模块按“考点是什么→为什么考→最常见的失分点→可以直接背的模板”来展开,中间穿插我这几年刷题和辅导期末攒下来的经验。不管你是用严蔚敏老师的经典教材,还是学校自编讲义,这套复习思路都通用。如果你正处于“感觉会了一点但又什么都不会”的阶段,跟着这篇走就行。

1. 期末冲刺的考点地图:先知道分数藏在哪

1.1 为什么“背了定义也不会做题”

数据结构的定义都很短,比如“栈是限定仅在表尾进行插入或删除的线性表”,这句话背下来只要一分钟,但考试根本不直接考这句话。真正考的是:给你一个序列,问能不能由某个栈操作得到;给你一段中缀表达式,要求转成后缀;给你一棵二叉树,让你写出非递归遍历。

所以定义只是入场券,核心是“操作逻辑”。你要把这个逻辑在脑子里过成一条线:栈只能在栈顶操作,所以它天然适合做“需要反悔、需要回溯”的事情;队列只能两头操作,所以它天然适合做“先来先服务、需要排队”的事情。带着这种理解去复习,定义自然就记住了,题目也自然会做了。

1.2 五模块复习优先级与时间分配

先说结论:树与二叉树是绝对的大头,栈队列和线性表是必拿分的基础,数组串属于“公式+模板”的性价比高地,并查集则是很多学校期末的加分项或小压轴。下面是我基于常见期末卷子估算的占比,具体以你学校考纲为准:

模块核心考点常见题型期末占比(估算)优先级
线性表顺序表/链表对比、插入删除、逆置合并选择、填空、代码题10%高
栈和队列进出栈序列、括号匹配、表达式求值、循环队列选择、填空、简答、代码题15%高
数组与串地址计算、稀疏矩阵三元组、KMP的next数组选择、填空、计算题10%中
树与二叉树遍历、非递归、线索化、哈夫曼、BST选择、填空、代码题、应用题25%极高
并查集find/union、路径压缩、简单应用代码题、应用题5%-10%中

如果距离考试只剩三天,我的建议分配是:一天半给树,半天给栈队列和线性表,半天给数组串和并查集。如果还有多余时间,再去研究深度不常考的冷门知识点,别本末倒置。

1.3 冲刺复习不要“看书”,要“输出”

期末复习最常见的误区是拿着书一遍一遍看,看到最后每一页都眼熟,合上书还是写不出来。我辅导过几百个学生,发现真正有效的冲刺方式是“输出式复习”,具体做法只有三条:

一是默写代码模板。把单链表插入、循环队列入队、二叉树递归遍历、并查集find这些经典模板,不看笔记在纸上手写一遍,写不出来就再看再写,直到完全肌肉记忆。

二是限时做真题。做题必须掐时间,比如一道代码题15分钟,一道计算题5分钟,模拟真实考场的节奏。很多同学平时慢慢想能想出来,一上考场就慌,就是因为没练过限时输出。

三是给自己讲题。复习完一个模块,假装对面坐着一个同学,把这个模块的考点从头到尾讲一遍。讲不清楚的地方,就是你还没掌握的地方。

2. 线性表:两道高频题就能看清本质

2.1 顺序表与链表的对比要“带着场景记”

线性表这个模块其实不复杂,难点在于很多同学把顺序表和链表的优缺点背得滚瓜烂熟,但一做题就不知道该用哪个。我的建议是放弃干背,换成场景记忆。

顺序表的底层是一块连续内存,数组下标可以直接算出任何一个元素的存储位置,所以它是“随机存取”。代价是在中间插入或删除要移动后面一堆元素,平均时间复杂度是O(n)。链表的底层是分散的节点,每个节点存着下一个节点的地址,插入删除只要改指针,所以是“顺序存取”,按位置找元素必须从头走到尾,时间复杂度是O(n),但真正做插入删除时只需要O(1)的指针操作。

一个常见的判断题是“链表比顺序表快”,这是错的,得分场景:大量插入删除选链表,大量按位置访问选顺序表。搞清楚这一点,比背十遍优缺点都有用。

2.2 头结点到底是干嘛的

头结点是很多期末代码题的“隐秘考点”。它不存数据(data域可以空着),它的指针域指向第一个真正存数据的节点。为什么要设这个哨兵?因为它能让空表和非空表的处理逻辑统一起来,插入、删除都不用专门为“空表”写一套分支。

下面这段代码是带头结点单链表最标准的“在第i个位置插入节点”,建议当作模板默写:

typedef struct LNode { int data; struct LNode *next; } LNode, *LinkList; bool ListInsert(LinkList L, int i, int data) { if (i < 1) return false; LNode *p = L; int j = 0; // 从头结点开始计数 while (p != NULL && j < i - 1) { // 找到第i-1个节点 p = p->next; j++; } if (p == NULL) return false; // i位置不合法 LNode *s = (LNode *)malloc(sizeof(LNode)); s->data = data; s->next = p->next; p->next = s; return true; }

这段代码里最容易错的是两件事:第一,j从0开始,因为p指向的是头结点;第二,先接新节点的next,再改前驱的next,顺序反了会丢掉后面的整条链。

2.3 逆置、合并、找中间节点:代码题的“三板斧”

线性表代码题在期末卷上翻来覆去就那么几道,其中最高频的就是原地逆置、合并有序链表、找中间节点。

原地逆置单链表,不会要求你开新数组,标准做法是三指针就地反转:

void Reverse(LinkList L) { LNode *pre = NULL, *cur = L->next; while (cur != NULL) { LNode *next = cur->next; cur->next = pre; pre = cur; cur = next; } L->next = pre; }

我见过太多同学在这道题上指针顺序搞错:先改cur->next,结果后面的节点找不到了。记住口诀“先存后继,再改指针”,这种低级错误就能避开。

合并两个有序链表,思路是反复比较两个链表的当前节点,小的先接到新链上。找中间节点则是快慢指针,快指针一次走两步,慢指针一次走一步,快指针到末尾时慢指针正好在中间。这道题考的是“双指针”思想,期末代码题爱考,考研也爱考。

3. 栈和队列:别只背“后进先出”四个字

3.1 栈的高频考点:进出栈序列、括号匹配、表达式求值

栈这一章的填空题和选择题集中在三个地方:进出栈序列合法性、括号匹配、表达式转换。

进出栈序列的判断方法是模拟。比如入栈序列是1,2,3,问3,1,2是否可能是出栈序列?你只要模拟一遍:1入、2入、3入,此时栈顶是3,出栈得3;下一个想要1,但栈顶是2,出不来,所以3,1,2不可能。这个模拟过程就是标准做法。更快的口诀是“比当前出栈元素大的必须降序排列”,但对基础薄弱的同学来说,老老实实模拟最稳。

括号匹配的代码题是栈的经典应用。思路是:遇到左括号就入栈,遇到右括号就弹出栈顶检查是否匹配。一旦出现“该弹的时候栈为空”或“栈顶不匹配”,直接判定非法。

表达式中缀转后缀是简答题常客,规则一句话:操作数直接输出;操作符看优先级,栈顶优先级大于等于当前操作符就弹出,再把当前操作符入栈;遇到左括号直接入栈,遇到右括号把栈顶到左括号之间的操作符全部弹出。注意:期末计算题里后缀表达式的求值同样用栈,遇到数字入栈,遇到操作符弹出两个数计算后重新入栈。

3.2 循环队列的判空判满:一道经典题反复考

循环队列几乎是必考知识点,考点集中在两个地方:下标计算和判空判满。

循环队列用数组实现,rear指向队尾元素的下一个位置,front指向队头元素。入队时rear = (rear + 1) % MaxSize,出队时front = (front + 1) % MaxSize。这里的取模操作就是“循环”的体现,防止下标出界。

判空是front == rear,但判满有两种常见方案:

方案判满条件说明
牺牲一个存储单元(rear + 1) % MaxSize == front最常用,最多存MaxSize-1个元素
加一个数据成员sizesize == MaxSize逻辑最直观,但要多维护一个变量
加一个tag标记队满时tag=1,队空时tag=0需要每次操作都更新tag

很多学校用第一种方案,因为代码最简洁。复习时先把这种方案吃透,再把另外两种记住作为选择题储备。判断队内元素个数的公式是(rear - front + MaxSize) % MaxSize,不要死记,理解一下“rear减front取模”的含义就能推出来。

3.3 队列的工程变体:期末只需要知道它们是什么

最近的热搜词里出现了不少和队列相关的工程概念,比如阻塞队列、双端队列、消息队列选型,还有调用栈回溯等。这些内容很多是面试和工程场景里的东西,期末复习不用深挖,但你需要能认出它们的归类。

阻塞队列就是“队列为空时取元素会阻塞等待,队列满时放元素会阻塞等待”,典型应用是线程池的任务队列。双端队列允许两端进出,考试时最多考判别“某个输入序列能否由某种受限双端队列得到”。单调队列是算法竞赛和动态规划优化里的工具,核心是维护一个内部元素单调的队列,用来快速找滑动窗口的最大值或最小值。

栈在工程里最常见的形态反而是“调用栈回溯”,也就是程序运行出错时打印出的那份函数调用链。这个概念能帮你理解栈帧的形成过程:每次函数调用都会在栈上压入一个栈帧,返回时弹掉。期末如果出简答题,问到“栈在系统调用中的作用”,答这一条就够。

4. 数组与串:公式加一个算法,别让送分题丢分

4.1 多维数组地址计算:一个公式用到底

数组串模块的计算题核心就是地址计算,尤其是二维数组。

行优先存储时,元素a[i][j]的地址计算公式是:

LOC(a[i][j]) = LOC(a[0][0]) + (i * m + j) * L

其中m是每行元素个数,L是每个元素占用的字节数。如果是列优先,把公式里的行列换一下就行。

做题时最阴险的坑是下标起点。题目如果说“数组下标从0开始”,那i、j直接代入;如果说“下标从1开始”,你就需要先自己减去偏移量,或者把公式里的i、j换成i-1、j-1。另一个坑是“每行多少个元素”到底算多少个,如果题目给了行数和列数,务必看清是几乘几的矩阵。

这类题白拿分,但每年都有人因为看不清“行优先”还是“列优先”丢分。我的建议是:动笔前先把题目的存储顺序圈出来,再做公式,不会亏。

4.2 稀疏矩阵:三元组是唯一的考点

稀疏矩阵的考点非常单一:用三元组表存储矩阵中所有非零元素。三元组表是(行下标,列下标,值)的数组,代码定义长这样:

typedef struct { int row, col; int value; } Triple; typedef struct { Triple data[MAX]; int rows, cols, nonZero; // 矩阵行数、列数、非零元个数 } TSMatrix;

期末主要考两件事:第一,给定一个稀疏矩阵,让你写出它的三元组表;第二,求转置矩阵的三元组表。前者是顺序填空题,后者要注意转置后它会按“新行号”排序,考试时可以用“列优先扫描原三元组,再依次存入”的思路来做。不需要把快速转置的代码背下来,但思路要知道。

4.3 KMP算法的next数组:手算方法比理解原理更实用

串这章的终极考点就是KMP。很多同学上来就背代码,结果next数组算不对,整个算法稀里糊涂。

KMP的核心优势是主串指针不回溯,匹配失败时把模式串指针回退到next[j]的位置。next数组的定义是:当第j个字符失配时,模式串应该从第几个字符开始重新比较。手算next数组的方法是找“已匹配部分的最长相等前后缀长度”,然后加一。

我以模式串“abaabc”为例,演示一下手算过程:

  • next[1] = 0(固定值)
  • next[2] = 1(固定值,因为只有一个字符的前后缀为空)
  • 第3个字符'a'前,子串“ab”没有相等前后缀,所以next[3] = 1
  • 第4个字符'b'前,子串“aba”的最长相等前后缀是'a',长度1,所以next[4] = 2
  • 第5个字符'b'前,子串“abaa”的最长相等前后缀是'a',所以next[5] = 2
  • 第6个字符'c'前,子串“abaab”的最长相等前后缀是'ab',长度2,所以next[6] = 3

每次算完到对应下标加一,这个“加一”很多同学老是忘,导致后面全错。如果你们老师用的是“nextval”改进版本,原理是在next的基础上跳过重复比较,考到的话再单独背一下改进规则,但基础版next必须会算。

5. 树与二叉树:递归模板和非递归套路一起抓

5.1 三种遍历的递归与迭代写法

树与二叉树是期末的绝对重心,因为代码题灵活性最大,可联动考察链表、栈、队列等多个知识点。三种遍历的递归写法本质上是同一段代码换顺序,模板直接背:

void Traversal(BiTree T) { if (T == NULL) return; // 前序:visit(T); Traversal(T->lchild); // 中序:visit(T); Traversal(T->rchild); // 后序:visit(T); }

非递归写法才是区分度所在。前序和中序的非递归都可以用“栈+模拟函数调用”来完成,核心套路是:沿着左子树一路入栈,走到空时弹栈访问,然后转向右子树。唯一区别是visit时机不同:

前序是“入栈前先访问根节点”,中序是“出栈时访问根节点”。这段代码建议默写:

// 中序非递归 void InOrder(BiTree T) { Stack S; InitStack(S); BiTree p = T; while (p != NULL || !IsEmpty(S)) { if (p != NULL) { Push(S, p); p = p->lchild; } else { Pop(S, p); visit(p); p = p->rchild; } } }

后序非递归比较麻烦,要用两个栈,或者记录“上一个访问的节点”来判断右子树是否已经访问过。期末如果只要求掌握一种后序非递归,双栈法最不容易错:一个栈做常规遍历,另一个栈负责倒序输出。理解起来就是“左右根”的反向是“根右左”,你按根右左的顺序入栈,再倒出来就是左右根。

5.2 二叉树程序总报运行时错误?先查这三个地方

最近热搜里有一条“写二叉树程序时为什么总是报运行时错误”,这个话题太真实了。期末上机或笔试写二叉树代码,十个报错九个出在下面三个地方。

第一个是空指针问题。很多同学写完递归函数不写“T == NULL”这个递归出口,或者写了但放错了位置。二叉树递归的本质是不断向空子树深入,没有空指针判断,递归根本停不下来。检查顺序:进函数第一件事就是判空。

第二个是构建二叉树时没有“把新节点接回去”。如果你在函数里新建了节点,但主调函数里的根节点指针没有被修改,那整棵树就是散的。C语言里要通过二级指针或返回新节点的方式才能把根节点带出来,比如:

BiTree CreateBiTree() { int val; scanf("%d", &val); if (val == -1) return NULL; // -1代表空 BiTree T = (BiTree)malloc(sizeof(BiTNode)); T->data = val; T->lchild = CreateBiTree(); T->rchild = CreateBiTree(); return T; }

第三个是递归里使用了全局变量但没有恢复。比如统计节点数、求高度这类问题,有些同学用全局count累加,反复调用函数时count没有清空,结果越加越多。我的建议是优先用“返回累加值”的写法,比如求高度的经典代码:

int TreeHeight(BiTree T) { if (T == NULL) return 0; int left = TreeHeight(T->lchild); int right = TreeHeight(T->rchild); return (left > right ? left : right) + 1; }

这种写法的好处是递归的每一层返回值都是独立的,天然防错。上机前把一个“安全的递归框架”记住:先判空,再递归计算,最后汇总返回。

5.3 线索二叉树、哈夫曼树、BST:小题的高频收割区

这部分出不了太复杂的代码题,但选择题、填空题特别密集,是最好拿分的地方。

线索二叉树的理论基础是“n个节点的二叉树一共有n+1个空指针域”。线索化的目的是利用这些空指针记录前驱和后继,让中序遍历不需要栈也能线性完成。考点最常落在“某种遍历顺序下的前驱/后继是谁”,这时抓住线索二叉树的本质:它的线索指针指的就是遍历序列中的前驱或后继。

哈夫曼树常考两件事:构造过程和WPL(带权路径长度)计算。构造规则很简单:每次从森林里选权值最小的两棵树合并,新节点的权值是二者之和。WPL算的是所有叶子节点权值乘深度的总和。需要避开的坑是:越接近根节点的叶子权值越小,WPL才最小。

二叉排序树BST的核心性质是中序遍历有序。插入操作好说,删操作是重灾区:被删节点有两个孩子时,通常用前驱节点或后继节点来顶替。填空选择只要答出“用中序前驱/后继顶替”就行,代码题不常考。

6. 并查集:性价比最高的“边缘模块”

6.1 并查集是干什么的:连通性问题的利器

很多同学看到“并查集”三个字就发怵,因为教材里它往往出现得比较晚,课时又少。但我要说,这个模块的思维量很低、模板很短、背下来就能拿分,性价比极高。

并查集解决的是连通性判断问题。比如在一个社交网络里,A和B是好友,B和C是好友,问A和C是否在同一个圈子里?只要不断执行“合并”操作把好友关系并到一起,最后“查找”两个人是否属于同一个集合即可。经典应用还有 Kruskal 最小生成树算法中判断“这条边会不会成环”,以及计算一个无向图里连通分量的个数。

它的数据结构非常精简:一个一维数组,下标表示元素编号,数组值存放它的父节点。合并就是把两个不同集合的根节点连起来,查找就是沿着父节点一路走到根。

6.2 三行代码的核心:find与union

并查集的核心代码很短,我建议你考前一天默写三遍:

int parent[MAXN]; int find(int x) { if (parent[x] != x) { parent[x] = find(parent[x]); // 路径压缩 } return parent[x]; } void unionSet(int x, int y) { int rootX = find(x); int rootY = find(y); if (rootX != rootY) { parent[rootX] = rootY; // 也可以按秩合并 } }

初始化时让每个元素的父节点都指向自己,parent[i] = i。查找用递归版的路径压缩,意思是查找过程中把沿途所有节点的父节点直接改为根节点,下次找就快了。经过路径压缩后,并查集操作的时间复杂度接近常数级别O(1),这是它能被广泛应用的原因。

很多学校讲“按秩合并”,也就是让“矮树”的根节点接在“高树”的根节点下面,避免形成一条长链。实现上维护一个rank数组,只在两棵树高度相同时才让高度加一:

if (rank[rootX] > rank[rootY]) { parent[rootY] = rootX; } else if (rank[rootX] < rank[rootY]) { parent[rootX] = rootY; } else { parent[rootY] = rootX; rank[rootX]++; }

考试时如果只让写一个模板,我会选“路径压缩+按秩合并”都写的完整版。如果时间紧,只写路径压缩也能拿大部分分。

6.3 带权并查集:学有余力的加分操作

部分学校期末考试会在最后一题放一个带权并查集,用来区分高分。它的难点是:集合里的元素之间不仅有“同一集合”的关系,还有“相对关系”。典型题是食物链问题和种姓关系问题。

思路是给每个节点到父节点的路径额外存一个“权值”,用来表示它和父节点的相对类别。查找时在原来路径压缩的基础上,同步更新每个节点累加到根节点的权值;合并时先算出两个根节点应该满足的权值关系,再手动修改一个根节点。代码模板比普通并查集长不了太多,但画图理解非常关键:

int parent[MAXN], weight[MAXN]; // 带权查找 int find(int x) { if (x != parent[x]) { int in = find(parent[x]); weight[x] = (weight[x] + weight[parent[x]]) % MOD; parent[x] = in; } return parent[x]; }

这里weight表示x与parent[x]之间的偏移量,取模是为了处理相对关系的周期循环。期末复习带权并查集,我的建议是不要死磕代码,先把“偏移量相加取模”这个数学模型搞懂,再去看两道例题的合并过程。如果考纲里明确不考,果断放弃,把时间留给树的遍历。

7. 考前一天,还能做这几件事

这篇冲刺指南的上篇到这里,刚好覆盖了考前大多数人最慌的几个模块。写这部分的时候我想起很多次期末前学生们追问“老师还有没有更快的办法”,我的回答一直没变过:考前24小时别再看新题,把已经会的模板守住就是胜利。

具体来说,你可以按这个顺序做三件事。第一,把单链表插入、循环队列入队出队、二叉树递归遍历、并查集find这几个模板在纸上默写一遍,任何一个卡壳就立刻翻书补上,再默写。第二,做一套往年真题的选择和填空题,重点看那些考定义判断题的题,错题涉及的知识点立刻回到对应章节翻一眼。第三,把树这一章的简答题清单过一遍:什么是满二叉树与完全二叉树的区别、哈夫曼编码为什么能得到最优前缀码、为什么中序线索二叉树能加快中序遍历。这些是口头提问的高频题,读一遍就能留下印象。

最后分享一个我个人的小技巧:考前一晚把模板写在a4纸上,不是用来背着进考场,而是用来“睡前过电影”。闭上眼睛,在脑子里默想循环队列的rear怎么加一取模、非递归中序遍历的栈什么时候弹、并查集的find怎么压缩路径。能完整想下来,明天进考场心里就有底。

祝这门课顺利,下篇(排序、查找与图)见。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询