ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

数据结构入门第一天:先建立体系,再吃透数组和链表

数据结构入门第一天:先建立体系,再吃透数组和链表 1. 第一天学数据结构先定好路线再动手后台经常有读者问我说数据结构到底怎么入门买什么书要不要直接刷题还有人一上来就甩一句“data structure day01”然后就没了下文。我特别理解这种状态——刚决定好好学习打开书翻了十分钟人已经睡着了。我自己当年学数据结构也踩过不少坑。最开始的误区就是翻着严蔚敏的教材试图把每行伪代码当C语言来背结果前四章反复看了三遍看到链表的时候已经是“马冬梅”状态——面熟但合上书啥也不记得。后来换了思路把数据结构当成一门“工具语言”来学先搞清楚每个结构解决什么问题、长什么样、怎么用再回头抠实现细节整个学习曲线一下就顺了。所以如果你是今天刚打开《数据结构》这门课先别急着抄代码、背定义。第一天最该做的事情是把接下来一个月的学习脉络摸清楚知道自己要打哪些“怪”每关要拿什么技能书这样后面学起来才不会迷失方向。先说结论第一周的核心就四个字——数组、链表。因为这两个结构是所有后续内容的基石。栈、队列、哈希表、树、图底层要么是数组要么是链表要么是两者的组合变形。你把这俩吃透了后面至少省一半力气。我对第一天的规划是这样的不碰代码也行先做三件事——搞清楚“数据结构到底研究什么”、搞懂“时间复杂度和空间复杂度是干嘛的”、徒手画出数组和链表的内存模型。这三件事做完你对整门课的坐标系就有了。那这篇博文就按我刚才说的路线展开从“为什么要先建立体系”说起再把第一天该掌握的概念和代码一个个过一遍最后附上我踩过的坑和资源清单。内容比较多建议收藏后再看。2. 别急着写代码先搞懂数据结构到底在解决什么问题2.1 数据结构不是“数据的集合”而是一套“存取方案”如果你去翻教材第一章定义通常写得文绉绉的“数据结构是相互之间存在一种或多种特定关系的数据元素的集合。”这句话没有错但对新手来说几乎等于废话背下来也没用。我更喜欢用生活场景来理解。你去超市买菜货架上的商品摆放是有规律的蔬菜在一个区零食在一个区冰柜单独一排。为什么这么摆因为这样你找东西最快。如果所有东西乱堆在一起虽然数据商品都在但你要拿一包盐得翻半天。数据结构干的事情完全一样。数组就是把元素按顺序排成一排你告诉我下标我一步就能找到——这是连续存储链表是每个元素攥着下一个元素的手一串串连起来——这是离散存储。不同的存储方式换来的是不同的“查找速度”和“增删成本”。所以判断一个数据结构好不好不是看它“高级不高调”而是看它在你最关心的操作上是否高效。这个思维模式一定要在一开始就建立起来否则后面学红黑树、B树你会直接懵掉——满脑子都是旋转、分裂却忘了它们本质上是“为了在大量数据里快速查询而设计的一种自平衡方案”。2.2 时间和空间复杂度数据结构的“性价比指标”既然数据结构是一套存取方案那怎么比较方案的优劣于是就有了时间复杂度和空间复杂度这两个指标。教材里会给你讲大O记法、最坏情况、平均情况全是概念轰炸。第一天你只需要抓住一句话时间复杂度描述的是“数据量变大时操作耗时增长的节奏”空间复杂度描述的是“数据量变大时内存占用增长的节奏”。有人可能觉得这是纯理论其实不是。举个具体例子你有一个猜数字游戏范围是1到100让你猜一个数。你如果从1开始一个个往上猜最坏要猜100次数据量翻倍到1000最坏就要猜1000次——这就是O(n)线性增长。如果你每次猜中间范围每次缩小一半数据量翻倍到1000你最多只需要猜10次2的10次方是1024——这就是O(log n)对数增长。差距是不是一下就出来了第一天不用去背所有复杂度排序但建议你记住最常见的几个量级O(1)、O(log n)、O(n)、O(n log n)、O(n²)。在脑子里放一把尺子O(1)是神仙O(log n)很优秀O(n)能接受O(n²)是能过但要想办法优化的水平。这块我多说一句不要小看复杂度分析在面试里的地位。现在很多公司算法面试考的其实不是你会不会背红黑树旋转而是你能不能把手上的O(n²)暴力解法优化到O(n log n)甚至O(n)。数据结构选型本质上就是在做这种复杂度博弈。2.3 逻辑结构和物理结构一对容易混淆的孪生概念教材第一章还会讲逻辑结构和物理结构存储结构很多初学者容易把这两个概念搅在一起。我当年也迷糊了很久后来用一句话就理清了逻辑结构是“数据之间是什么关系”物理结构是“这条关系在内存里怎么落地”。逻辑结构主要分四种集合元素之间没有关系就是凑一堆、线性结构一对一像排队、树形结构一对多像公司组织架构、图状结构多对多像地铁线路网。物理结构就两种顺序存储内存里挨着放和链式存储内存里不一定挨着但用指针串起来。关键来了同一个逻辑结构可以用不同的物理结构实现。线性结构既可以用数组实现顺序表也可以用链表实现链式存储。这就是为什么你会看到“顺序栈”“链栈”“循环队列”“链队列”这些成对出现的名词。第一天如果你能把这张“逻辑结构 x 物理结构”的对应表在大脑里画出来后面学习每种具体结构时你就知道自己其实是在同一个框架里填格子而不是每学一种结构就推翻重来。到了复习阶段这张表还能帮你做知识串联效率会比别人高一大截。3. 手把手拆解两大核心结构数组和链表3.1 数组内存里的“连排座位”数组太常见了以至于很多人学数据结构时觉得它没什么好学的。但我要说数组是理解后续所有结构的基础值得认真对待。数组在内存里是一段连续的空间就像电影院里一整排座位。如果你知道第1个座位在哪里那么第n个座位的地址一算就出来了起始地址 (n - 1) × 每个座位宽度。这就是数组“随机访问”能力强的根本原因时间复杂度O(1)一步直达。但数组的缺点也同样明显插入和删除很麻烦。假设一排10个座位都已经坐满现在有一位新观众要坐在第3个位置怎么办得把第3到第10个人全部往后挪一位。数据量大的时候这个操作的时间复杂度是O(n)。在C语言里数组还要求长度预定。你开一个int a[100]想存101个数据就不行了。所以学数组时你要建立两个意识第一访问快是它的核心竞争力第二长度固定、插入删除代价高是它的命门。实操层面我建议第一天就把“用数组实现一个动态扩容的顺序表”过一遍。这是一个从“会用数组”到“理解数组”的分水岭。所谓动态扩容就是数组快满时重新malloc一块更大的空间把旧数据搬过去再把旧空间释放掉。听起来简单做起来能踩的坑不少后面我会专门讲。3.2 链表内存里的“寻宝游戏”链表和数组正好相反。链表在内存里不需要连续每个节点可以散落在任何地方节点之间靠指针连接。你可以把链表想象成寻宝游戏你手里拿着一张纸条上面写着“宝物A在这里它的箱子里还有一张纸条写着宝物B的位置”。你要找第三件宝物必须先拿到第一张纸条找到第一件宝物再顺着它找到第二件再找到第三件。这就是链表“不支持随机访问”的原因查找第n个节点时间复杂度O(n)。但链表插入和删除非常快。还是寻宝游戏的例子如果你想在宝物A和宝物B之间塞一个宝物C你只需要改两条线索A的纸条改成指向CC的纸条指向B。其他的都不用动O(1)搞定。而在数组里这可能是O(n)的搬移操作。链表又分好几种单链表、双向链表、循环链表。第一天先把单链表搞明白就行。单链表每个节点长这样typedef struct Node { int data; // 数据域 struct Node *next; // 指针域指向下一个节点 } Node;注意看这个定义struct Node *next是指在“还没有Node类型定义完”的时候就使用了struct Node指针。这在C语言里是合法的因为指针只要知道类型名就能定义不需要完整结构体定义。这是链表实现里非常关键的一个语法细节很多新手在这里卡住过。3.3 数组和链表的对比不是“谁替代谁”而是“谁适配谁”我见过太多人在数组和链表之间纠结“哪个更好”其实这问题毫无意义。数据结构是一门“取舍”的学问没有绝对优劣只有适配场景。给你列一张对比表是我当年在一张便签纸上总结的现在还在用对比维度数组链表内存布局连续分散随机访问O(1)一步到位O(n)必须从头遍历插入/删除O(n)需要搬移元素O(1)只改指针前提是已经定位到位置空间占用无额外指针开销但有闲置空间每个节点多一个指针天然有额外开销扩容方式需要整体搬移天然动态随加随创建缓存友好性高局部性原理低节点可能分散在不同缓存行这里的“缓存友好性”值得展开说说。数组是连续内存CPU加载一个元素时会把附近一片内存一起加载进缓存所以遍历数组非常快。链表节点是散落的每次访问都可能触发一次缓存未命中性能瓶颈往往不是计算量而是内存访问。这也是为什么很多高性能场景下即使理论复杂度相同数组实现也比链表快得多。所以初学者选型时可以记住一个朴素原则经常按位置查找、读多写少的优先数组频繁在任意位置增删、数据量不确定的优先链表。但真实工程里还要考虑缓存、内存碎片、实现复杂度等因素不能只看理论复杂度。3.4 动手实现一个单链表别偷懒第一天就实现完整可用的带头节点单链表是最扎实的练习。别觉得“会用就行”手写一遍和只看代码是完全不同的体验。我建议按下面这个顺序逐步完成定义节点结构体创建空链表带头节点头插法插入节点尾插法插入节点按值查找节点在指定位置插入节点删除指定节点打印链表销毁链表下面是核心部分我贴一个参考实现。注意看注释里写的坑都是我用debug时间换来的经验#include stdio.h #include stdlib.h typedef struct Node { int data; struct Node *next; } Node; // 创建带头节点的空链表 Node* createList() { Node *head (Node*)malloc(sizeof(Node)); if (head NULL) { printf(内存分配失败\n); exit(1); } head-next NULL; return head; } // 头插法新节点插到头节点之后 void insertAtHead(Node *head, int value) { Node *newNode (Node*)malloc(sizeof(Node)); newNode-data value; newNode-next head-next; // 新节点先指向原来的第一个节点 head-next newNode; // 头节点再指向新节点 } // 尾插法遍历到末尾再插入 void insertAtTail(Node *head, int value) { Node *newNode (Node*)malloc(sizeof(Node)); newNode-data value; newNode-next NULL; Node *p head; while (p-next ! NULL) { // 注意是判断 p-next不是 p p p-next; } p-next newNode; } // 按值删除第一个匹配的节点 void deleteByValue(Node *head, int value) { Node *p head; while (p-next ! NULL p-next-data ! value) { p p-next; } if (p-next NULL) { printf(未找到值为%d的节点\n, value); return; } Node *toDelete p-next; p-next toDelete-next; // 跳过待删除节点 free(toDelete); // 释放内存 } // 打印链表 void printList(Node *head) { Node *p head-next; while (p ! NULL) { printf(%d - , p-data); p p-next; } printf(NULL\n); } // 销毁链表 void destroyList(Node *head) { Node *p head; while (p ! NULL) { Node *temp p; p p-next; free(temp); } } int main() { Node *list createList(); insertAtTail(list, 10); insertAtTail(list, 20); insertAtHead(list, 5); printList(list); // 5 - 10 - 20 - NULL deleteByValue(list, 10); printList(list); // 5 - 20 - NULL destroyList(list); return 0; }这里面有一个特别容易犯的错就是删除节点时忘了保存toDelete直接写p-next p-next-next然后把p-next当成free的参数去释放。这看着没问题实际上你free的已经是被重新赋值后的节点了原来的目标节点就内存泄漏了。正确做法就是先把指针存下来再改链接再释放。3.5 动态扩容顺序表的几个细节再补充一下动态扩容顺序表。刚才说数组的长度固定那“动态数组”是怎么回事其实它底层还是数组只是“快满了就换大房子”。核心逻辑是一个扩容函数#define INIT_CAPACITY 4 typedef struct { int *data; int size; // 当前元素个数 int capacity; // 当前容量 } SeqList; void ensureCapacity(SeqList *list) { if (list-size list-capacity) { return; } int newCapacity list-capacity * 2; int *newData (int*)malloc(sizeof(int) * newCapacity); if (newData NULL) { printf(扩容失败\n); exit(1); } for (int i 0; i list-size; i) { newData[i] list-data[i]; } free(list-data); list-data newData; list-capacity newCapacity; }这里有个设计问题值得琢磨为什么扩容倍数是2而不是“每次多扩10个”因为如果每次固定加10个假设从4扩到14再扩到24每插入10个元素就会发生一次搬移均摊下来每次插入的时间复杂度是O(n)。而如果容量翻倍4到8到16到32越往后扩容次数越少均摊到每次插入的时间复杂度就是O(1)。这种“摊还分析”的思想第一天先有感知后面学到动态数组那节会再深究。4. 第一个程序别贪多栈和队列留到明天4.1 栈先进后出递归和表达式求值的基础我知道很多人第一天就想把栈和队列也一起学了觉得“反正都是线性结构”。但我劝你忍一忍。栈和队列虽然也是线性结构但它们引入了一个新概念——操作受限。栈只能在栈顶操作队列只能一端进另一端出。这种“受限”才是它们的灵魂。栈在计算机系统里无处不在函数调用的调用栈、浏览器的后退按钮、撤销操作、括号匹配检查、表达式求值全是栈的典型应用。你光会用数组模拟栈还不行你得理解“为什么限制只能在一端操作反而提高了抽象能力”——因为调用者不需要关心栈中间发生了什么只需要入栈、出栈两个动作。如果第一天时间充裕可以用栈做一道经典的编程题括号匹配。给你一个只包含( ) [ ] { }的字符串判断括号是否合法。思路很简单遇到左括号就压栈遇到右括号就弹出栈顶检查是否匹配。这个题是后面所有“栈应用”题型的入门非常值得在第一天练手。4.2 队列先进先出排队论的暴力美学队列的思路就更生活化了——食堂排队、打印任务排队、消息队列、CPU任务调度全是FIFO先进先出思想。实现队列有一个特别经典的坑如果用数组实现循环队列你会发现“队空”和“队满”两个状态很难区分。比如front和rear都指向同一个位置到底是空队列还是满队列解决方法是少用一个存储单元或者额外用一个size变量记录元素个数。这个细节很多人学到“循环队列”那节才反应过来但我建议你第一天就知道因为它是“工程实现细节影响逻辑设计”的绝佳例子。队列的应用同样很多二叉树层序遍历、BFS搜索算法、操作系统进程调度、消息队列削峰填谷。初学阶段不要想着全掌握知道“队列是先进先出底层可以用数组或链表实现”就够了。4.3 为什么我不建议第一天就碰排序和树有个热搜词是“数据结构排序算法”很多新手看到排序就兴奋冒泡、快排、归并、堆排觉得快速排序又短又酷一上来就背。但我真不建议第一天干这事。排序算法涉及递归思想、分治策略、交换和比较的复杂度分析这是学习数据结构第二周甚至是第三周的内容。第一大任务应该是先把线性结构吃透然后搞清楚栈、队列、递归这三者是怎么联动的。排序算法里最难理解的快排本质上就是两件事找基准值 递归处理左右子区间。如果递归都还没搞明白背快排代码只会让你越来越没信心。树的内容更不用急。二叉树、遍历、BST、平衡树这是一个完整的体系起码需要两到三天的专项时间。第一天就把它们列进学习计划只会给自己制造焦虑。我见过不少人的学习路径是第一天热血沸腾学了一堆概念第二天发现全忘了然后开始怀疑自己智商。其实不是智商问题是步子迈太大了。学习数据结构有一个特别好的节奏口诀概念要“用”过一遍代码要“抄”过一遍题要“刷”过一遍。每一步都需要时间沉淀急不来。5. 新手常见问题与避坑经验5.1 null指针和野指针C语言数据结构的“头号杀手”写链表代码最常遇到的就是Segmentation Fault。新手一看程序崩了第一反应是“我代码逻辑哪里不对”其实十有八九是空指针问题。我总结了几条血泪经验每次malloc之后立刻判断是否成功。虽然考试时老师不检查但工程里malloc失败是真实存在的尤其在嵌入式环境。养成习惯后面写项目会感谢自己。遍历链表时循环条件判断的是p-next ! NULL还是p ! NULL要想清楚。如果是尾插法你得停在最后一个节点上所以判断p-next ! NULL如果你要遍历每个节点做操作那判断p ! NULL。释放节点后最好把指针置为NULL。不然这个指针就变成了野指针后续万一不小心使用崩溃位置和错误原因完全对不上排查起来能熬到凌晨。5.2 内存泄漏面试官最爱问新手最容易忽略C语言不像Java有垃圾回收你malloc出来的内存必须自己free。内存泄漏的恶性影响是滞后的程序跑起来可能几个小时甚至几天后才因为内存耗尽而崩溃排查难度极高。写链表练习时销毁链表必须一个节点一个节点地释放不能只free头节点。我见过的新手错误是free(head)以为整个链表都释放了实际上后面的节点成了无人管理的内存孤岛。正确做法参考前面我贴的destroyList函数用一个临时指针遍历逐个释放。另外free和delete如果是C的规则是“谁malloc/new谁负责释放”。你如果在一个函数里创建了链表另一个函数里释放这没问题但你如果在一个函数里创建又在一个线程里释放另一个线程还在用它那就是典型的use-after-free崩起来毫无规律。5.3 为什么会“一看就会一写就废”这个现象太普遍了几乎每个学数据结构的人都经历过。原因很简单你看代码时是“顺着作者的思路走”大脑产生了“我会了”的错觉但让你自己写时你要自己设计思路、处理边界条件、应对编译错误难度完全不同。破解办法就一个合上代码纯手写。我当年练链表是拿一张A4纸从头写一遍完整代码不参考任何资料写完再对照书修订。第一遍一定漏洞百出很正常坚持三天后再写你会发现肌肉记忆已经有了。这个方法没有技术含量但极其有效。还有一个小技巧写完代码后自己当“人肉编译器”走一遍。比如删除节点函数你画出两个指针自己模拟一轮删除过程把每个指针的值都写出来。这个过程能帮你建立指针操作的直觉比看十遍教程都管用。5.4 第一天能做完什么一个合理的小目标写到这里给第一天画个停靠点。如果你是零基础第一天可以只做这些事情理解逻辑结构和物理结构的区别能把数组和链表的优缺点各写不少于3条能手写单链表的创建、插入、删除、遍历能说出栈和队列各自的特点和一种应用场景如果你有余力可以再看一遍动态扩容顺序表理解“摊还”的思想。如果这些都搞定了那第一天的效率已经相当高可以安心休息第二天再向栈和队列进军。6. 学习资源和工具推荐别被“资料海洋”淹死6.1 教材怎么挑严蔚敏 vs 王道 vs 其他热搜词里有“严蔚敏”和“王道”这两个方向代表了两类完全不同的学习目标。严蔚敏的《数据结构C语言版》是经典教材优点是体系完整、思维严谨但缺点也明显语言偏学术伪代码风格数学要求高。如果你是计算机专业、时间充裕、打算系统打好理论基础它是最佳选择。但如果你只为了应付面试或考研直接啃它会比较痛苦。王道的数据结构辅导书主要是配合机考更偏应试把考点压缩得很紧凑例题和真题都标注好了。如果你目标是考研408或找工作刷题王道视频课的效率会高很多。我的建议是第一遍学习不要过度纠结选哪本书挑一本主教材哪怕就是学校的课件把线性结构、树、图、查找、排序这五大块学明白。第二遍复习时再拿另一本做交叉对照因为不同教材的切入角度不同能帮你把知识网络织得更密。6.2 刷题平台LeetCode和Codeforces怎么选国内刷题首选LeetCode力扣题目分类清晰、讨论区质量不错。数据结构入门阶段按“数组”“链表”“栈”“队列”的标签筛选从简单题开始刷。Codeforces更适合打竞赛、训练思维敏捷度但对新手不太友好题目没有中文难度曲线也陡。如果你不是奔着竞赛去可以先把Codeforces放一放。还有一个很多人忽略的资源是学校OJ平台。很多高校的课程OJ积累了数年的平时作业题和期末题题目风格和考研408比较接近针对性强。你如果是在校生可以用起来。6.3 可视化工具让抽象的数据结构“看得见”强烈推荐两个可视化网站对建立直觉非常有帮助VisuAlgo新加坡一个算法可视化网站支持排序、链表、BST等几十种结构和算法的动画演示。Python Tutor支持C/C/Python/Java能逐行显示程序执行时内存中变量的变化。写链表代码遇到底层指针理不清时用它走一遍瞬间通透了。我当年学二叉树时就是靠VisuAlgo的AVL树动画终于弄明白了左旋右旋到底在干嘛。数据结构的抽象逻辑用眼睛看一遍往往比在脑子里硬想半小时更高效。7. 实操总结第一天的正确打开方式最后再分享一个我自己的实操习惯每天学数据结构一定要以“写一个能编译运行的小程序”结束。不需要复杂哪怕只是用链表存5个学生成绩再遍历打印出来。这个仪式感会给你正反馈让你第二天有动力继续翻开书。第一天的内容总结下来就是三句话建立体系感吃透数组和链表做完一个手写实现。不贪多、不求快、不跟别人比进度按自己的节奏来。如果这篇对你有帮助建议收藏下来第二天学栈和队列之前再翻一遍。数据结构这门课前期慢就是快基础打牢了后面树、图、排序学起来会顺畅很多。
返回列表