☰
数据结构第二遍怎么学?从代码实现到双端队列与空间复杂度
2026/10/3 2:59:12 网站建设 项目流程

数据结构学到第二遍,很多人会发现一个扎心的现象:第一遍课堂上好像都听懂了,代码也照着敲过一遍,但合上书自己写个链表反转、写个快排,愣是憋半天憋不出来。这个阶段最典型的感受就是“脑子会了,手不会”。如果你正处于这个状态,别慌,这恰恰说明你从“应付考试”进入了“真正理解”的关口。这篇内容就是写给卡在这个阶段的人——不管你是正在准备期末、备战考研408,还是单纯想补上数据结构这块短板,都可以当一份第二阶段的学习参考来用。后面会聊到一些具体的结构(比如双端队列)、一条比较特别的算法梳理主线(空间复杂度),以及从理论到代码落地的实践方法,全部是过来人经验,拿去就能用。

1. 数据结构学习的第二关:从“看得懂”到“写得出”

1.1 为什么第一遍学完感觉像是没学

很多人学数据结构的第一遍是靠“看”完成的:看教材上的图示,看老师PPT里的动画,看别人写好的代码。看得多了,会产生一种虚假的熟悉感。比如单链表删除节点,书上写p->next = p->next->next;,你在纸上画一画,觉得很有道理——但是关上书,让你自己写一个带虚拟头结点的删除函数,你很可能连要不要保存被删节点、什么时候释放内存都理不清。

这不是你笨,而是数据结构的核心能力不是“看懂逻辑”,而是“在约束条件下把逻辑变成可运行的代码”。第一遍学习时,信息是别人咀嚼过喂给你的,你没有经历过那些“卡壳”的瞬间,所以知识是浮着的。到了第二遍,必须切换学习方式:从“看书”变成“造轮子”。我个人的经验是,每个经典结构看完讲解后,立刻合上书,在空白编辑器里从头实现一遍。链表、栈、队列、二叉树,一个一个来。一开始很痛苦,但实现两三个之后,你会明显感觉到那些结构不再是书上的图,而是你脑子里可以随意调用的积木。

1.2 从“写得出”到“写得对”的调试方法

当你开始动手写,就会遇到真正的拦路虎——段错误、死循环、结果不对。这里分享一个特别实用的调试习惯:画指针图,打印验证。以链表为例,每次修改指针之前,先在注释里写清楚“当前prev指向谁、cur指向谁、next是谁”,然后每完成一个关键步骤,打印链表的全部节点地址和值。这样做的好处是,指针错误会非常直观地暴露出来——你会看到某个节点突然从链表里“消失”了,或者出现循环引用。

另一个容易被忽视的细节是边界条件。很多人写代码能跑通常规例子,但一遇到空链表、只有一个节点的链表、删除头结点这类情况就崩。第二遍学习时,我建议养成一个条件反射:每次写完一个数据结构操作,立刻在脑子里过三个测试用例——空结构、单元素结构、满结构(如果涉及容量)。这三个用例能挡住绝大多数隐蔽bug。这个过程不需要多高深的理论,纯粹是熟练度和敏感度的问题,但偏偏是考试和实际开发中最拉分的部分。

1.3 建立自己的“结构-操作-复杂度”对照表

第二遍学习结束时,你应该能在纸上快速写出一个对照表:每种数据结构支持哪些操作(插入、删除、查找、访问),每个操作在平均情况和最坏情况下的时间、空间复杂度,以及这个结构适合解决什么类型的问题。比如哈希表擅长快速查找但不适合有序遍历,平衡树查找稍慢但能维持顺序,跳表在有序性和实现难度之间取了个巧。这张表是你后续做算法题、应对面试和考研选择题的底层武器。

很多人学数据结构越学越乱,就是因为脑子里只有零散的知识点,没有形成这张表。数据结构之间不是孤立的,它们是在不同的“约束条件”下做出的不同取舍。当你开始理解这种取舍关系,才算真正入了门。

2. 双端队列:一个常被忽略但实用性极强的线性结构

2.1 双端队列到底是什么,和栈、队列是什么关系

大部分教材和课程把重点放在栈和队列上,双端队列往往一笔带过。但从实用角度来说,双端队列(Deque,Double-Ended Queue)反而是日常工作里更常用的结构。它就是一个两端都可以进行插入和删除操作的线性表,相当于“栈 + 队列”的合体——你既可以把它当栈用(从同一端进出),也可以把它当队列用(一端进另一端出),还能玩出更多花样(两端都能进出)。

这个“多出来的自由度”让它特别适合处理一类问题:数据需要从两端被消费,或者最新最旧的数据都可能是热点。最经典的场景就是滑动窗口最大值问题(LeetCode 239):给定一个数组和一个窗口大小,窗口每次右移一格,要求输出每个窗口内的最大值。如果用普通队列,每次求最大值都要扫描整个窗口,复杂度是O(nk);但用一个双端队列维护“窗口内可能成为最大值的元素下标”,每个元素最多入队出队一次,总体复杂度能降到O(n)。我第一次看懂这个解法时确实有种“原来结构是这样用”的震撼——它不是炫技,而是双端队列特性与问题结构的精准匹配。

2.2 双端队列的实现方式和常见陷阱

双端队列的实现有几种方式,各有取舍。我用过三种,简单说一下:

实现方式优点缺点适用场景
双向链表两端插入删除都是O(1),实现直观每个节点额外存prev指针,内存开销大,cache不友好对内存不敏感,更看重实现简单
循环数组(环形缓冲区)空间紧凑,内存友好,访问快扩容逻辑复杂,需要处理“空”和“满”的状态判断高性能场景,比如Java的ArrayDeque
两个栈拼装实现巧妙,考察对栈的掌握只有一端是O(1),另一端操作平摊O(1)但常数较大面试炫技,或者需要在受限环境里复用已有栈结构

我不建议你在第二遍学习时死磕实现细节,而是要做到两件事:第一,能用语言内置的双端队列(Python的collections.deque、Java的ArrayDeque)熟练解决上面滑动窗口这类问题;第二,至少完整实现一遍循环数组版的双端队列,因为这里面的坑非常典型——head和tail指针如何区分“空”和“满”(通常牺牲一个存储单元来区分)、扩容时旧数据怎么搬迁、取模运算怎么处理负数,每一个都是初学者容易翻车的地方。踩过一遍这些坑,你对“数据结构是受约束的存储管理”这句话的理解会深一大截。

3. 排序算法的一条特殊梳理主线:用空间复杂度重新审视

3.1 为什么说空间复杂度在排序里常被低估

排序算法是数据结构课程里的重头戏,大多数人复习时会按时间复杂度把它们分堆:O(n²)的插入、选择、冒泡,O(n log n)的快排、归并、堆排,再加上一些特殊场景下的线性排序。但如果你只盯着时间维度,会漏掉一个在实际工程里同样致命的东西——内存占用。

空间复杂度这个概念,在理解不深的同学那里经常被压缩成一句话:快排O(log n),归并O(n)。但为什么是这个数,很多人讲不清楚。以快排为例,它不需要额外的大块辅助数组(原地分区),但递归调用本身会在栈上占用空间。每一层递归需要保存一部分上下文信息,平均情况下递归树深度是log n,所以空间复杂度是O(log n);最坏情况下每次分区都极度不平衡,递归深度退化成n,空间复杂度就变成O(n)了。这是一个非常容易被忽略的“隐藏空间”——它不体现在你显式声明的数组里,却真实地在调用栈上消耗资源。

归并排序的空间复杂度则是另一个故事。它需要一个和原数组等长的临时数组来合并两个有序段,所以在任何情况下额外空间都是O(n),不管你是递归版还是迭代版。这也是为什么在内存极度受限的嵌入式环境里,归并排序几乎不被使用——复杂度确实是O(n log n),但O(n)的辅助空间往往比时间更奢侈。

3.2 以空间换时间的典型代表:桶排序、计数排序、基数排序

讨论空间复杂度的时候,不能不提那类“空间换时间”的排序算法。计数排序的基本思想很简单:如果待排序元素都是小范围整数,就开一个计数数组,遍历一遍记录每个值出现多少次,再按顺序输出。它的时间复杂度可以达到O(n+k)(k是数值范围),看起来非常诱人,但代价是空间复杂度同样是O(n+k)——如果k巨大,比如你要排序几个稀疏分布的大整数,直接开计数数组就是灾难。桶排序和基数排序也是类似的思路:用额外的桶来换取线性时间。在实际工程里,除非数据分布明确且范围可控,否则很少有人直接用这类算法,但它们背后的思想(哈希、分桶)在很多数据处理的场景中都有变体版本。

这里我想给你一个非常实际的建议:复习排序算法时,别只背稳定性表格和复杂度表格,而是亲手做一次“空间复杂度推导”。比如堆排序,很多人只知道它时间复杂度是O(n log n)、空间复杂度是O(1),但为什么能压到O(1)?因为它原地建堆、原地调整,和快排相比连递归栈都不需要。搞清楚这个问题,你才算真正掌握了堆排序,而不仅仅是记住了它的复杂度数字。

3.3 排序算法选型的实战逻辑

到了应用层面,排序算法的选择往往是在“时间、空间、稳定性、实现复杂度”四个维度之间做权衡。举几个具体的场景:

  • 数据量很小(比如十几个元素),直接插入排序往往是最优解。它实现简单,空间O(1),而且对小规模近乎有序的数据接近O(n)。Timsort(Python和Java内置排序的底层算法)在检测到小块近乎有序数据时,用的就是插入排序——这不是巧合,而是在真实数据分布下统计出来的经验。
  • 数据量大且追求稳定排序,归并排序是主流选择,代价是O(n)空间。Java的Arrays.sort()对对象数组用的就是归并排序的优化版本,因为对象排序要求稳定性。
  • 数据量很大且不要求稳定,快速排序通常是首选。虽然它最坏情况退化到O(n²),但通过“三数取中”或“随机选取基准值”等手段,实际几乎不可能踩中最坏情况。加上它对cache友好(原地访问连续内存),工程表现非常优秀。
  • 数据量巨大且必须外部排序(数据在磁盘上,一次装不进内存),归并排序的思想是基石——分块读入、块内排序、多路归并,每一步都是在用磁盘IO换内存空间。

当你把空间维度纳入考量之后,再去回答“为什么大多数语言内置排序不用堆排序”这类问题,思路就开阔多了:堆排序在比较排序里空间表现极佳,但它的访问模式是跳跃式的(父子节点下标间不是连续的),对CPU缓存极其不友好,而且不稳定。理论复杂度和实际运行速度,从来不是一回事。

4. 从理论到考场:期末复习与408备考的实用链路

4.1 数据结构这门课,复习到底在复习什么

先回答一个问题:数据结构考试和408考研里,数据结构部分到底考什么能力?拆开看,就三样东西:知识体系的完整性(每个结构都有印象、知道边界)、复杂度分析能力(能推导、能手算、能比较)、以及代码实操能力(手写伪代码或真代码)。这三个能力对应三种不同的复习行为,不能混为一谈。

第一种是搭建框架。拿一张白纸出来,凭记忆画出数据结构知识树:线性表(数组、链表、栈、队列、双端队列)、树(二叉树、BST、AVL、红黑树、B树)、图(存储、遍历、最短路径、最小生成树)、查找(顺序、二分、哈希)、排序(各类算法的复杂度与稳定性)。画不出来或者画不全的地方,就是你知识的薄弱点——先补这部分框架,再去抠细节。这个“凭记忆画图”的方法非常有效,因为它逼着你的大脑主动提取信息,而不是被教材牵着走被动浏览。

第二种是复杂度推导的专项训练。这一块是最容易被忽略的,因为很多题目的答案都是现成的,你看着觉得“嗯,O(n log n),记住了”,但实际上你并没有掌握“怎么算”的能力。复习时我建议拿几道典型的题目亲手推导,例如:两层嵌套循环为什么是O(n²),递归解T(n)=2T(n/2)+O(n)为什么解出来是O(n log n),二分的递归树深度为什么是log n。把这些推导过程写出来,远比你背十个复杂度结论有用。

第三种是代码实操的题型化训练。数据结构代码题绝大多数有固定套路:链表题考察指针操作和边界处理;树的题考察递归遍历(前中后序)和层序(BFS);图的题考察DFS/BFS和拓扑排序;排序题考察算法过程模拟和复杂度分析。把每种类型的经典题刷上若干道,归纳出通用模板,考试时至少能把基础分稳稳拿到手。

4.2 教材和资料怎么选、怎么配合用

市面上的数据结构教材一大堆,风格差异很大,选哪本取决于你的目标。我提几个常见的,各有各的适用场景:

  • 严蔚敏的《数据结构(C语言版)》:经典老大哥,C语言视角,原理叙述严谨,适合计算机科班学生打底。但坦白说,它的代码风格偏老,初学容易觉得生硬。
  • 《大话数据结构》:如果第一遍看严蔚敏看出一头雾水,这本书是很好的替代入门读物。它用了大量生活化类比,读起来像故事书,先把“是什么、为什么”讲清楚。缺点是不够深,考试深度不够时还得回到正经教材。
  • 《数据结构与算法分析》系列(有C、Java等版本):很多学校考研推荐用书,讲原理讲得很透,尤其适合想深入研究算法的同学。Java语言描述版本对以后走Java方向的人很友好,可以直接对照JDK源码学习。
  • 王道考研系列:针对408考生设计,知识点归纳和真题练习紧密结合,适合中后期刷题冲刺阶段使用,不适合作为第一本从头到尾学的新手教材。

我的建议是:入门用《大话数据结构》之类的轻松读物建立概念,系统学习用科班教材(按你考研/目标语言选),冲刺阶段用王道或历年真题。一本书从头啃到尾反而低效——不同阶段需要的信息密度和讲解方式不同,一本吃通全书的精神值得佩服,但性价比不高。

4.3 实验报告不只是交差:它其实是最高效的复习

热词里出现“数据结构实验报告”,大部分学生把它当成任务敷衍过去,这是很可惜的事情。数据结构实验课设置的初衷,是逼你完成“从理论到代码”的闭环。写实验报告的过程,其实是一次完整的自我复盘:问题描述(你理解了什么)、设计思路(你怎么解决问题)、数据结构设计(为什么选这个结构)、核心代码(实现细节)、测试结果(你怎么验证正确性)、复杂度分析(你的方案有多好)。你应该反过来用这个框架复习:挑几个经典算法题,按这个格式给自己写“实验报告”,然后对照标准答案看差距。我试过用这个方法复习链表和二叉树章节,效果比漫无目的地刷题好得多——写报告逼着你想清楚每一步的“为什么”,而刷题时你的大脑很容易开小差。

4.4 李春葆教材和“勘误”这件事

热词里还有一条“《李春葆数据结构第五版学习指导勘误汇总》”,可见很多人在学习过程中发现教材有错误。这里我想专门说两句:教材有错是常态,尤其是一些“学习指导”类的辅助资料,为了凑题量和覆盖面,答案出错的比例不低。遇到跟参考答案对不上的情况,别急着怀疑自己。正确的做法是:用自己的代码或推导结果去验证,然后把疑问发到课程群、论坛去讨论。这个过程其实是一种高级学习——你不是在被动接受正确答案,而是在主动构建自己的判断标准。等你到了真实工作环境,文档、代码、需求之间互相打架的情况多的是,那时候你会发现,提前练就一套“理性质疑”的方法论,比死记硬背一百个正确答案值钱得多。

5. 语言视角下的数据结构:从C语言到Python、pandas

5.1 为什么考试用C语言,工程用Python/Java

热词里“数据结构C语言版”和“数据结构与算法C语言”占了很大比例,这和国内高校教学传统和考研要求有关。学数据结构的核心目标是理解“数据在内存里是怎么组织和移动的”,而C语言恰好把指针和内存管理暴露在你面前——链表节点里那个next指针到底是什么,动态数组扩容到底发生了什么,这些在C语言里是无法回避的物理事实,而在Python里list可以随意append,你根本感知不到底层的realloc过程。所以不是C语言比Python更适合“学习数据结构”,而是C语言让你“看得见”数据结构。

但到了实际开发,你几乎不会再手写链表——Java有LinkedList,Python有deque,C++有STL。这时候更重要的能力是:把你在教材里学到的概念映射到语言自带的数据结构上。我一开始学Python的时候有个困惑:为什么list既能当栈(append/pop)又能当队列(pop(0))?“万金油”一样的东西,还有必要区分栈和队列吗?后来才意识到,pop(0)的时间复杂度是O(n),用list当队列用是非常差的习惯。而collections.deque的popleft()才是O(1)。教材里“队列先进先出”的概念,在具体语言里对应的是“应该选哪个内置结构”,这层映射关系,才是考试和面试真正想考察的工程判断力。

5.2 pandas的“数据结构”和教材里的“数据结构”是一回事吗

如果准备的是数据分析方向,你一定碰到过“pandas数据结构创建”这个关键词,也用过Series和DataFrame这两个东西。很多人会困惑:这和课上学的“数据结构”有什么关系?我的理解是:pandas里的Series(带标签的一维数组)、DataFrame(带标签的二维表格),本身就是在“数组”“索引”“哈希表”这些基础结构之上封装出来的高级结构。你给DataFrame设置行索引,底层涉及索引如何组织才能快速定位;你按条件筛选数据,底层涉及类似哈希或树形索引的查找逻辑。学好数据结构的原理,再去看pandas的文档和源码,你会突然明白很多“为什么”——为什么iloc按整数位置取数比loc按标签取数在某些场景快,为什么某些操作要避免在循环里逐行执行,背后都是复杂度分析在说话。

网上有不少在线实训平台提供了pandas数据结构创建相关的练习,这类动手任务的意义在于:把你抽象的“结构意识”落到具体的数据处理流程里。建议学有余力的同学认真做一遍,用数据分析这个场景反过来验证数据结构课上学到的概念,是一种很高效的交叉学习。

5.3 从教材代码到项目代码,还差哪几步

最后一个绕不开的话题:很多人学完了数据结构,但还是不会写项目代码。中间到底差了什么?我觉得是三件事:封装、复用和性能意识。教材代码为了讲清楚原理,常常把所有逻辑平铺在几个函数里,结构简单但不具备工程性;而项目代码要求你把数据和操作封装成清晰的类/模块,考虑异常处理、资源释放、扩展维护。这是“学到”和“做到”的分水岭。

我的建议是走一个三步过渡:第一步,用面向对象的方式把教材里的经典结构重写一遍,比如把链表封装成一个类,对外只暴露insert、delete、search等方法,内部自己管理节点——这能帮你建立“接口与实现分离”的意识;第二步,给这个类加上完整的边界处理(空结构、唯一元素、超大数据量压测),相当于做一次代码审查;第三步,找一道综合一点的算法题(比如实现一个LRU缓存、一个带优先级的调度器),把多个数据结构组合起来用。走完这三步,你会发现数据结构不再是单独的知识点,而变成了你工具箱里随时可调用的零件。

这一篇“数据结构学习(2)”主要聊了从看懂到写得出的跨越、双端队列这样容易被低估的结构、以空间复杂度为主线的排序新视角,以及不同目标场景下的复习和落地路径。数据结构的奇妙之处在于,你每次回头重看,都能在同一块内容里发现新的层次。希望这篇里的具体方法和踩坑经验,能帮你在这个阶段走得更顺一点。

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

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

立即咨询