数据结构第二周,往往是整个学期的一道分水岭。第一周还在聊抽象数据类型、时间复杂度的概念题,第二周立刻切换到线性表、链表的建存取删,要求你手写代码,还要求能应付笔试里的各种变形。很多同学就在这个节点开始焦虑,说链表指针看懂了,自己一实现就错;栈和队列规则都懂,一上机就乱。这里我可以直接说:很正常。数据结构这门课,是把“逻辑结构”和“存储实现”第一次强行绑在一起,它需要的是想清楚再写,而不是边写边想。这篇文章围绕第二周的学习内容,把线性表、栈、队列、双端队列这些重点拆开讲,再聊一聊实验报告、常见坑和调试经验,希望能帮正在啃这本书的人少走几步弯路。
1. 第二周学习内容:先看清地图再动手
1.1 从课程大纲看第二周的位置
绝大多数学校的《数据结构》课程,第一周是绪论,第二周就开始进入线性表。有的教材把顺序表和链表分开成章,有的像严蔚敏版,线性表一章讲完;王道、天勤、李春葆这些辅导书基本也是这个顺序。也就是说,第二周的目标非常明确:把顺序表、单链表搞到能默写的程度,再开始接触栈和队列的应用。
为什么这么强调“默写”?因为线性表是后面所有存储结构的母型。树的孩子表示法要用链表拼,图的邻接表也要用链表拼,栈和队列本身就是线性表的受限版本。这一周如果只是“会做题”,没有把链表的底层操作写透,第三周开始学树和图就一定会吃力。我在带学生的时候见过太多例子:前面线性表靠背应付过去,后面指针数组混在一起,代码基本只能看着参考答案读,完全提不出自己的实现。
但也要有个取舍。第二周时间就这么多,不可能把顺序表、单链表、双链表、循环链表、栈、队列一遍全精通。我建议第一优先级是顺序表和单链表,第二优先级是栈和队列,双链表和循环链表可以放在第三周强化。因为前四个是后面绝大多数算法题的底座,后两个更多是变形应用,绝大多数老师第二周也只是点到为止。
1.2 顺序表 vs 链表:理解背后的选择逻辑
第二周最容易遇到的问题,是顺序表和链表放到一起比,看完了感觉都会,可一合上书又觉得什么都没抓住。这里我推荐一个办法:自己画一张对比表,然后回答三个问题——随机访问谁快?中间插入谁方便?内存空间谁更省?
| 维度 | 顺序表 | 链表 |
|---|---|---|
| 存储方式 | 连续的一段内存,靠数组下标访问 | 分散节点,靠指针串起来 |
| 随机访问第 i 个元素 | O(1),直接用 a[i-1] 定位 | O(n),要从头一个个走到第 i 个 |
| 末尾插入/删除 | O(1),只要容量够 | O(1),有尾指针的话 |
| 中间插入/删除 | O(n),要把后面元素整体搬动 | O(1) 定位之后,改指针即可 |
| 额外空间 | 基本无,但要预分配 | 每个节点多存一个指针 |
| 缓存友好度 | 高,连续内存访问快 | 低,节点分散,容易缺页 |
很多同学把链表的查找复杂度记成 O(1),理由是“链表插入删除都很快”。这里要纠正一个误区:定位和操作是两件事。单链表在头部插入确实是 O(1),但你得先找到头部;如果要在中间任意位置插入,光“找到那个位置”就已经 O(n) 了,改指针本身才是 O(1)。考试和面试里很喜欢考察这个区分,比如“在已知节点 p 后面插入一个新节点”和“在未知节点位置插入一个新节点”,复杂度完全不同,前者可以做到 O(1),后者必须先遍历定位。
实际操作上,第二周写顺序表比写链表容易,但链表更能暴露一个人的功底。我的建议是:先细细地写一个顺序表的插入删除,再写一个带头节点的单链表建立、遍历、插入、删除。完成这两个小实验,再去碰后面的栈和队列,会顺很多。
2. 栈、队列与双端队列:操作受限的线性表
2.1 栈:后进先出,内存管理的王者
栈的实现其实很简单,你用一个数组加一个 top 指针就能模拟。真正难的是理解它为什么无处不在。函数调用要压栈,表达式求值要压栈,浏览器的后退也是栈,撤销操作还是栈。第二周学栈,我建议别急着背题目,先做个具体的案例:括号匹配。
比如对字符串([]{}),用一个栈依次扫描字符:遇到左括号就入栈,遇到右括号就和栈顶匹配,如果匹配成功就弹出栈顶,最后栈为空才说明括号是合法配对的。整个过程不到二十行代码,却把栈“先进后出、只能从栈顶操作”的核心体现得淋漓尽致。等你写完这个,再看中缀表达式转后缀、函数调用栈,会自然产生联系。
写栈代码时有一个常见的坎:top 初始值到底是 -1 还是 0。两种写法都有人用,关键是入栈出栈条件别写反。如果 top 初值为 -1,入栈要先 top++ 再赋值;出栈要先取值再 top--。如果 top 初值为 0,入栈是 a[top++] = x,出栈是 top--。我见过太多人把这两个搞混,结果栈里永远少一个元素或者越界。建议挑一种固定下来,每次上机前默写一遍,几周后就不会再纠结。
第二周用数组模拟栈通常比用链表模拟栈更稳妥。链表栈虽然看起来高级,但初学者很容易在内存释放和指针悬空上翻车。第一阶段先把数组栈写熟,链表栈可以放到后面和链表操作一起练。
2.2 队列:先进先出,环形队列的边界问题
队列的现实应用同样非常多:操作系统进程调度、消息队列、打印机缓冲、广度优先搜索。第二周要实现的通常不是简单的链式队列,而是循环队列——因为顺序队列反复入队出队会出现“假溢出”,明明是空位置却用不了。
循环队列的难点在边界判断。核心公式就两个:队尾入队rear = (rear + 1) % MaxSize,队头出队front = (front + 1) % MaxSize。判断队满是(rear + 1) % MaxSize == front,也就是说牺牲一个存储单元来区分队空和队满。如果你判断队满写成rear == MaxSize,那排队列一超过数组长度就会越界,或者明明没满也被误判为满。
我建议在纸上画一个四个格子的环,手动模拟入队、出队、再入队的过程,把 front 和 rear 的移动标清楚。这个动作看起来初级,却是解决一切循环队列问题的最快方法。等你能徒手画出队列满与空的状态,那些队长计算题就不会再错——比如(rear - front + MaxSize) % MaxSize这个求队长公式,不理解画圈的人很容易写成绝对值。
队列的另一种实现是链式队列,用一个头节点加 front、rear 指针。第二周如果时间不够,链式队列可以简单带过,但至少要明白它不会假溢出,所以循环队列主要用来解决顺序存储的浪费问题。
2.3 双端队列:看似灵活,最容易懵
近年不少教材和考试把双端队列加了进来,很多人在查“数据结构 双端队列”这个关键词。双端队列就是允许在两端插入删除的队列,听起来好像只是放宽了限制,但考试题经常让人头大。
常见题型是给你一个输入序列,限定“输出受限”或“输入受限”的双端队列,问能得到哪些输出序列。这里的关键是先把“受限”两个字圈出来。输入受限是指只能在某一端插入、另一端不能插入,但两端都可以删除;输出受限则反过来。很多人丢分是因为凭直觉脑补操作,把受限当成两端都可入可出。正确做法是画一个横着的双端队列,把输入输出方向标成箭头,每一步只能按题目允许的操作移动元素。
实际工程里,Python 的collections.deque就是双端队列,适合做滑动窗口、最近使用列表、任务队列等。但在第二周,真正的重点还是回到“它是线性表的受限版本”这个本质上:双端队列的底层可以用链表做,也可以用循环数组做。你只要掌握了顺序表和链表,双端队列的代码实现反而容易。刚开始不要贪多,把限制条件分析清楚比手写双端队列代码更重要,这也是第二周笔试中比较容易得分的一块。
3. 排序算法与复杂度:第二周有必要热个身
3.1 这周适合碰哪几个排序算法
很多教材把排序算法放在后半学期,但多年经验告诉我,第二周最好写一遍直接插入排序、冒泡排序、简单选择排序。原因很简单:这三种排序的主体操作都是数组遍历、元素比较和交换,刚好能复习顺序表;而且理解了它们,后面学 O(nlogn) 那一票高级排序时才有对比基础。
直接插入排序是最贴近“扑克牌理牌”思路的算法。核心代码也不长:
void insertSort(int a[], int n) { int i, j, tmp; for (i = 1; i < n; i++) { tmp = a[i]; // 先把待插入元素保存 j = i - 1; while (j >= 0 && a[j] > tmp) { a[j + 1] = a[j]; // 比 tmp 大的元素往后挪 j--; } a[j + 1] = tmp; // 找到位置,插入 } }这段代码如果第二周能不看答案写出来,说明你对数组下标边界和元素覆盖顺序已经有了直觉。写不出来也没关系,但一定要画图理解:为什么从后往前搬,而不是从前往后。从前往后会直接把还没比较的元素覆盖掉,这是初学者最常见的错。排完序后再算一下它的平均复杂度 O(n^2)、最好 O(n)、最坏 O(n^2),第一周学的时间复杂度概念就有东西落地了。
冒泡排序和选择排序同样值得手写。冒泡排序要理解“每一趟把最大的元素浮到末尾”,选择排序要理解“每一趟待排序区里选出最小的放到最前面”。它们看起来很像,但稳定性不同,这部分下面重点说。
3.2 时间复杂度和空间复杂度别再靠背
第二周真正需要建立的计算能力,是把“时间复杂度”从名词变成工具。很多同学只记住排序是 O(n^2),却说不清为什么。实际上你只需要数一数嵌套循环的执行次数:插入排序最坏情况是每个元素都要往前比对 i 次,累加起来是 1+2+...+n,约等于 n^2/2,所以是 O(n^2)。
空间复杂度是一个更容易被忽略的点。排序里像插入排序、冒泡排序、选择排序,都只用了几个临时变量,辅助空间是 O(1)。而归并排序需要额外的一个数组,所以辅助空间是 O(n)。递归算法还要把递归调用栈的空间算进去,比如快速排序的空间复杂度是 O(log n),但递归版的最坏情况会到 O(n)。这些细节笔试里经常考,尤其考研 408 真题喜欢在“空间复杂度”上做文章。
再来看稳定性,这是第二周最容易混淆的概念。稳定不是“排序性能稳定”,而是“相等元素的相对顺序不改变”。比如先按姓名排序,再按成绩排序,如果算法稳定,成绩相同的同学还能保持姓名顺序;如果不稳定,可能乱掉。插入排序和冒泡排序都是稳定的,选择排序不稳定。举个例子:序列[5, 8, 5, 1],第一趟选择排序会把最小的 1 换到第一位,导致两个 5 的相对顺序可能改变。理解这个例子之后就不会再瞎背稳定性结论了。
| 排序算法 | 平均时间复杂度 | 最坏时间复杂度 | 辅助空间 | 稳定性 |
|---|---|---|---|---|
| 插入排序 | O(n^2) | O(n^2) | O(1) | 稳定 |
| 冒泡排序 | O(n^2) | O(n^2) | O(1) | 稳定 |
| 选择排序 | O(n^2) | O(n^2) | O(1) | 不稳定 |
| 归并排序 | O(nlogn) | O(nlogn) | O(n) | 稳定 |
| 快速排序 | O(nlogn) | O(n^2) | O(logn) | 不稳定 |
第二周不需要急着掌握全部,但至少要把前面三行的复杂度来源和稳定性道理讲清楚。后面学快排和堆排时,再对比它们的平均最好最坏情况。
4. 用什么语言、看什么书:第二周最容易纠结的问题
4.1 C、C++ 还是 Python,别把语言当成数据结构本身
如果学校用的是 C 语言版教材,第二周一定要亲手把结构体、指针、动态内存分配用起来。很多书上伪代码写得很好,但到机器上跑起来问题百出,多是因为对malloc和指针类型转换不熟。建议先写两个小项目:一个顺序表,一个带头节点的单链表,每天至少过一遍创建、插入、删除、查找。
有 C++ 基础的同学可以用引用简化参数传递,写起来更顺手,但别跳过指针。比如链表节点定义struct Node { int data; Node* next; };,你要清楚Node*和Node的区别,清楚p->next和(*p).next是同一个东西。读懂指针,后面学树的时候才能看懂root->left、root->right。
Python 的写法则完全不同。Python 里的list已经把很多线性表操作封装好了,你可以直接用list.append模拟入栈,用list.pop模拟出栈,写起来很快。但如果你要用 Python 交数据结构上机作业,一定要展示你理解底层逻辑,而不是只调用现成容器。比如用类实现链表 Node,自己写insert、delete方法,而不是拿list假装是链表。
有些课程方向偏数据应用,会提到 pandas 创建数据结构这类说法。pandas 中的 Series 和 DataFrame 确实是 Python 数据领域里的常用结构,但它们是建立在更底层的数据结构之上的抽象,和数据结构教材里的栈、队列、链表不是同一层概念。第二周看到这些名词不要慌,记住:数据结构培养的是抽象能力,具体语言只是实现工具。
4.2 严蔚敏、王道、李春葆这些教材怎么用
教材选型也是第二周绕不开的问题。严蔚敏的《数据结构(C语言版)》是很多学校指定的教材,体系严谨,但代码和类 C 伪代码混在一起,初学者经常看完算法步骤却不知道在哪里写 main。我的经验是:把它当字典和原理书用,不要指望照抄它的代码到编译器里就能跑。
王道考研数据结构书适合题型总结和刷题,尤其适合准备 408 的人。但它毕竟不是教材,很多知识点直接以结论形式给出来,如果你连基本概念都还没建立,直接刷王道会变成“背题”。更好的节奏是:先在学校教材里看懂原理,再用王道检验掌握程度,最后拿历年真题练速度。
李春葆的教材整体实例比较多,适合做上机实验参考。第二周如果遇到实验报告,不妨翻翻他的例题思路。但有一点要避开:部分版本的《学习指导》存在印刷勘误,网上也有勘误汇总表。用之前最好先查一下所持版次有没有已知错误,免得拿着错答案对题。如果你还没买书,先去图书馆翻一版,对比例题步骤和习题解析,确认没有明显笔误再入手。
还有不少院校,比如山大软件学院这类实践导向比较强的专业,第二周可能直接抛出综合实验。这个时候不要慌,大胆用学校安排的上机环境,先把功能跑起来,再回头对照教材补概念。不要因为实验题没做过就想着放弃,绝大多数综合实验就是把顺序表、链表的操作串起来而已。
5. 第二周实验报告怎么写才不白写
5.1 一份报告的基本骨架
第二周的实验报告通常围绕线性表展开:要么是顺序表的插入删除,要么是单链表的建立、查找、删除与反转。有些网络教育和电大课程会把它称为形考作业,这也是一种实验报告,只是提交形式更模板化。
我建议一份报告至少包含这么几个部分:
- 实验目的:说明这周实验要验证哪几个知识点。
- 实验内容与需求分析:把题目用自己的话描述一遍,指出输入、输出和核心约束。
- 数据结构设计:说明用到顺序表还是链表,为什么选它。
- 核心算法思路:写出关键函数的设计思想,最好配时间和空间复杂度分析。
- 关键代码片段:只贴核心函数,不要整篇源代码堆上去。
- 运行结果:给出代表性输入输出,最好有边界情况。
- 问题分析与调试过程:记录遇到的错误、排查思路和最终解决办法。
很多学生只写“源代码 + 三张运行截图”,这真的非常可惜。老师想看到的不是你会复制代码,而是你出了问题怎么排错。把一个内存越界或者指针断链的问题写清楚,哪怕没完全解决,都比你贴十段能跑的代码更有价值。
5.2 常见扣分点和加分细节
我是一个经常帮学生看实验报告的人,说几个最常见的扣分点:
一是没有复杂度分析。一个链表的插入删除算法,光写“时间复杂度 O(1)”是不够的,必须说明为什么在“已知前驱节点”的前提下才成立,如果按值查找前驱就还是 O(n)。
二是实验目的空泛,写什么“掌握数据结构的基本操作”。要具体一点,比如“掌握单链表逆置的指针修改过程,理解使用三个辅助指针的遍历方法”。
三是代码没有注释。第二周上机代码不算长,但如果老师要求截止时间前交,没有注释的代码很难证明是你自己实现的。
四是没有对边界条件的测试。顺序表插入第一位置、删除最后位置,链表空表插入,这些情况写进运行结果里绝对是加分项。
还有一个加分细节:如果你能在报告中画出顺序表插入时的元素移动示意图,或者链表反转时每一步指针的变化,报告的档次会明显高出一截。手画可以用 word 箭头,也可以用代码画 ASCII 示意。重点是让老师看到你真的理解了过程,而不是只给了最终结果。
6. 第二周上机最常见的坑与排查方法
6.1 链表指针:画图比空想有用
凡是链表报错,十有八九是对 NULL 的判断和指针赋值顺序出了问题。比如把新节点 p 插入到单链表 q 的后面,正确顺序是:
p->next = q->next; q->next = p;这两行顺序不能反过来。如果先写q->next = p,那么 q 原来后面的节点就丢了,链表从这里断开,后面所有操作都会出问题。
另一个高频 bug 是遍历链表时不让当前指针指向下一个节点,死循环在同一个节点上。比如写while(p != NULL),循环体内却忘了p = p->next,结果程序一直打印同一个节点的 data。这种错误看起来很低级,但在上机压力下真的很常见。
我的习惯是:写链表的修改操作之前,先在草稿纸上画方框和箭头,标出哪些指针要改,再对着图写代码。有人觉得这麻烦,但对新手来说,画图是最省时间的调试方式。等你熟练之后,再尝试直接在脑海里模拟。我在实际带人的过程中,几乎所有链表指针错误,一画图立刻就能自查出来,根本不需要到编译器里反复猜。
链表还有个隐蔽问题,就是内存管理。用 C 语言手动malloc的节点,删除时要记得free;free 之后不要再访问这个节点,否则就成了悬空指针。C++ 里new出来的节点,用delete删除。很多同学上机时不开内存检查工具,直到程序崩溃才发现释放问题。如果环境允许,多用 ASAN(AddressSanitizer)或者简单的日志打印来辅助调试。
6.2 栈和队列的边界条件
栈经常出的问题就那几个:top 初始值是 -1 还是 0,入栈前有没有判满,出栈前有没有判空。可以封装isEmpty()和isFull()两个小函数,避免在每处操作里都重复判断而导致不一致。顺序栈判满的条件通常是top == MaxSize - 1,循环队列判满条件是(rear + 1) % MaxSize == front。 这些公式不难,但如果你二周内不动手写,考前临时背很容易弄混。
循环队列的队满条件之所以要牺牲一个存储单元,是因为如果不牺牲,队空和队满都会出现front == rear,没法区分。另一个办法是使用 size 字段记录当前元素个数,这样可以不牺牲存储单元,但第二周课本大多采用牺牲一个格子的做法。做题时先看题目默认的是哪种规则,用不同的规则,队长计算公式也不一样。
还有一个小坑:队列长度计算公式(rear - front + MaxSize) % MaxSize里多出来的+ MaxSize是为了防止负数取模。很多语言对负数取模的行为和数学上不一致,比如-2 % 5在 C 语言里结果是 -2,不是 3。所以一定要先加上 MaxSize,再取模。
6.3 调试技巧:用日志和最小用例缩短排错时间
第二周开始,我认为最值得培养的习惯是写最小测试用例。比如测试链表删除函数,不要一上来就建 100 个节点的链表,而是先建一个只有 3 个节点的链表,分别测试删除头节点、删除中间节点、删除尾节点的情况。一个节点出问题,你立刻能定位到是哪段逻辑,而不是被一长串输出淹没。
另一个很有用的技巧是在每个关键操作后打印链表当前所有节点。比如在插入、删除后写一个printList(head),就能直观看到链表结构有没有被破坏。这个方法对新手极其友好,比单步调试断点更快,因为链表出问题往往是结构断了,打印一两次就能发现问题位置。
用调试器时注意观察指针变量:在 IDE 的变量面板里展开指针,看 next 是不是指向合理的节点,有没有出现 0x0 地址。如果你发现某个指针指向了完全不可能的地方,那八成是野指针或者 free 后没有置空。
最后说一句关于“卡住了”的心态。数据结构第二周卡住很正常,不要觉得是自己笨。我当时学链表反转也卡了好几天,后来是把每一步指针变化画在纸上才彻底想通的。如果你发现某个概念到第三天还想不明白,休息一下,换个角度,先做会做的题,回头再看那个点。上机时间不在多,而在于是否专注地写透每一处细节。