恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

吃透线性表:顺序表与单链表的内存模型、边界条件与工程选型

  • 首页
  • 资讯中心
  • /
  • 吃透线性表:顺序表与单链表的内存模型、边界条件与工程选型

相关资讯

企业微信聊天记录导出来之后,谁能看、能给谁?隐私边界与共享控制的完整攻略 2026/10/18 7:01:34
场地预定小程序+收银+会员系统:锁场、支付回调与对账实战 2026/10/18 7:01:34
AI落地案例全解析:从场景选型到工程化避坑 2026/10/18 7:01:34

最新资讯

CORBA Explorer实用指南:IOR解析与命名服务排障技巧
高通SNPE部署实战:从DLC转换到DSP量化推理完整指南
A*与DWA算法融合:全局路径规划与局部避障实战指南
7z发布包安全解压与静态审查:从黑匣子到可信运行的完整流程
RRT路径规划算法核心原理与工程实践:从随机搜索到渐进最优
Vue3最小项目深度拆解:从HelloWorld看工程化与响应式全流程

今日推荐

基于GMM的语音识别系统原理与MATLAB实现详解
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

本周热门

基于GMM的语音识别系统原理与MATLAB实现详解
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

吃透线性表:顺序表与单链表的内存模型、边界条件与工程选型

发布时间:2026/10/18 7:01:34
吃透线性表:顺序表与单链表的内存模型、边界条件与工程选型 数据结构这门课绝大多数教材把“线性表”放在第二章也就是我们接触的第一个正经数据结构。很多人翻几页就跳过去了觉得顺序表和单链表太“底层”不如后面的树、图、哈希表高级。但我的看法正好相反线性表是所有数据结构的骨架树不过是加了层次约束的链表图不过是加了多条指针的链表而哈希表底层要么是数组本质是顺序表要么是链表。这块如果只是背代码没有吃透内存模型和边界条件后面学红黑树、跳表、LRU缓存的时候会处处踩坑。这篇文章不打算复读教材定义我想站在实际编程和面试的角度把顺序表和单链表一次讲透它们各自的内存布局、手写实现时最容易翻车的边界条件、复杂度之外的工程选型依据以及面试和考试里最高频的题型套路。不管你是正在上课的学生、准备跳槽的开发者还是想补基础的转行新人这篇都值得认真看完。我会尽量写得直接一点能一句话说清楚的事绝不用三句话糊弄。1. 为什么线性表是第一道必须迈过去的坎1.1 逻辑结构和物理结构理解线性表的钥匙线性表的定义很简单n个数据元素的有限序列。这句话每个教材都有但真正让初学者懵的是“逻辑上相邻物理上不一定相邻”这句话。我打个比方。逻辑结构就是你手里拿着一张清单上面写了五件事第一件事做完做第二件顺序不能乱这是你大脑里认为的“顺序”。物理结构则是这些事真正存在哪儿——可能它们写在一张连续的纸上第一行第二行挨着也可能每件事单独写在小卡片上卡片散落在抽屉各处每张卡片上写着下一张卡片的位置。前者就是顺序表后者就是单链表。这个区分是整个线性表的钥匙。顺序表的“顺序”靠物理存储位置连续来保证就像一排连在一起的座位单链表的“顺序”靠指针串联来保证就像寻宝游戏里每个线索提到下一个线索藏在哪里。这两种结构没有谁绝对更好它们解决问题的方式不同代价也不同。很多同学学到这里喜欢纠结“哪个更强”。我建议尽早抛弃这种想法数据结构里几乎没有“更强”只有“更适合当前场景”。顺序表随机访问快但插入删除要搬动大量元素链表插入删除灵活但想访问中间某个节点必须从头走一遍。这两句话背下来很容易真正困难的是在手写代码的时候你能把这两句话变成对边界条件的敏感。1.2 顺序表和单链表各自擅长什么顺序表在直观上更好理解因为它和数组几乎长得一样。实际上顺序表就是“动态数组”底层是一块连续内存通过记录已使用数量和总容量来维护一块比实际数据更大的空间。它擅长的是“存了就查”比如一个排行榜随时要看第i名是谁那顺序表是最自然的选择下标直接算出偏移量一步到位。单链表则更接近于“一串珍珠”。每个节点是一个结构体既存数据又存指向下一个节点的指针。它擅长的是“要频繁增删但不怎么按编号查找”的场景比如LRU缓存的淘汰队列、操作系统的任务调度队列。因为它做插入删除时只要把相邻节点的指针改一下不需要像顺序表那样把后面所有元素都挪位置。这里我想多说一句。很多初学者第一次手写链表时常常被指针绕晕然后用结构体数组假装链表觉得“这不也能实现吗”。能实现但违背了链表的初衷内存不连续、位置动态变化。如果数据量固定且内存连续直接上顺序表就好没必要套一层链表。学习链表的价值恰恰在于锻炼你处理“动态内存、分散存储、指针串联”的能力这是后面树和图的基础。所以哪怕你觉得链表不实用也得把它的指针操作练到肌肉记忆。2. 顺序表手写实现从结构体设计到动态扩容2.1 为什么成员变量是data、size、capacity三个手写顺序表时很多人会直接从数组开始定义一个int data[100]然后写个insert函数往里塞数据。这样写代码也能跑但它没有抓住顺序表的本质顺序表是对数组的二次封装它需要自己管理容量和大小。标准的结构体设计是三个成员data指针、size当前元素个数、capacity当前容量。data指向一块堆内存size记录里面真正存了几个元素capacity记录最多还能存几个。为什么不能只留一个size因为当size等于capacity时插入操作就必须扩容否则内存就越界。容量这个概念就是为了让你知道“什么时候该搬家”。有一个细节值得注意data的类型是int*用malloc或calloc分配内存而不是定义一个固定数组。固定数组的问题在于你不知道该开多大。开小了不够用开大了浪费内存而且无法在运行时扩大。用堆内存配合capacity才能做到“不够了就再申请一块更大的把旧数据搬过去”。如果你做的是通用顺序表data可以换成void*或者用泛型封装。但学习阶段建议先用int就好把逻辑理清楚再谈抽象。一上来就用泛型往往连指针都搞不明白还要处理内存释放很容易两头都不讨好。2.2 插入删除的核心逻辑与越界红线顺序表的核心操作有四个插入、删除、查找、修改。插入和删除是整个顺序表最容易写错的地方原因在于它们都涉及“移动元素”和“判断边界”两个环节。先说插入。要在位置index插入一个值流程是先把从index到size-1的元素全部往后挪一位然后把新值放到data[index]最后size加一。这里有两个边界条件必须注意第一index不能小于0也不能大于size比如size是5你想插到位置5是可以的那相当于在末尾追加但插到位置6就不行中间会空出一个洞破坏顺序表的连续性。第二插入前要检查size capacity满了就先扩容否则next元素就会写超出分配的内存这是内存安全的大问题。删除和插入是镜像操作。删除位置index的元素流程是先把从index1到size-1的元素全部往前挪一位然后size减一最后可选地把最后一个位置清零。这里最容易忽略的是删除下标为0的元素和删除最后一个元素是完全不同的情况。删除最后一个元素时移动循环根本不会执行但size一样要减否则就会出现“逻辑上删了实际上还能看到旧值”的毛病。我把顺序表插入删除的参考代码贴出来你可以照着实现一遍。#include stdio.h #include stdlib.h typedef struct { int *data; int size; int capacity; } SeqList; void initList(SeqList *list, int cap) { list-data (int *)malloc(sizeof(int) * cap); list-size 0; list-capacity cap; } void destroyList(SeqList *list) { free(list-data); list-data NULL; list-size 0; list-capacity 0; } int insertAt(SeqList *list, int index, int value) { if (index 0 || index list-size) { return -1; } if (list-size list-capacity) { // 扩容稍后会讲具体策略 int newCap list-capacity * 2; int *newData (int *)realloc(list-data, sizeof(int) * newCap); if (newData NULL) { return -1; } list-data newData; list-capacity newCap; } for (int i list-size; i index; i--) { list-data[i] list-data[i - 1]; } list-data[index] value; list-size; return 0; } int deleteAt(SeqList *list, int index, int *deletedValue) { if (index 0 || index list-size) { return -1; } *deletedValue list-data[index]; for (int i index; i list-size - 1; i) { list-data[i] list-data[i 1]; } list-size--; return 0; }2.3 扩容策略、realloc的坑与缩容时机关于扩容最简单粗暴的做法是“每次插入前如果满了就扩大一个固定大小比如加10个元素”。但这样做的时间复杂度很糟糕。假设插入n个元素每次满了都要搬移一次全部数据总搬移次数是102030……整体是O(n²)。扩容如果改成“容量翻倍”虽然偶尔一次扩容很贵但每个元素被搬移的次数摊下来很少平均是O(1)这就是很多人说的“均摊复杂度”。那扩容倍数应该选1.5还是2网上说法很多我直接说结论不同语言运行时用不同倍数背后主要考虑内存分配器的碎片率。倍数为2意味着每次新容量都刚刚超过旧容量一倍连续几次扩容后容量变成2的幂和许多内存分配器的分页大小容易对齐。倍数为1.5则能让扩容后的内存有机会复用之前释放的旧块降低碎片率。对于刷题和学习选2就行代码最简单如果后面写底层库、内存敏感的服务可以改成1.5但那是另一套优化的故事了。写扩容时有一个非常经典的坑就是realloc的直接赋值问题。// 错误示范 list-data (int *)realloc(list-data, sizeof(int) * newCap);realloc失败时会返回NULL这句话如果直接赋值给list-data原来的指针就丢了内存泄漏不说后面再free会出大问题。正确写法是先用一个临时指针接收realloc的返回值判断不是NULL之后再赋给list-data。我上面的参考代码里就是这样的。再就是缩容。很多新手只关心扩容不关心删除后内存会不会浪费。假设一个顺序表存了上亿个int删到只剩几个那块内存还占着几百MB这就是明显的浪费。缩容策略一般用“当size小于capacity的四分之一时容量减半”这样可以防止频繁在阈值边缘抖动。比如缩到一半后插一个元素又立刻扩容来回折腾就得不偿失了。这个四分之一阈值是个经验值不是数学上的最优解但实践里足够稳。3. 单链表手写实现每个指针操作都值得较真3.1 带头节点和不带头节点的差别单链表和顺序表最大的不同在于它的物理内存不连续。每个节点是一个独立的内存块节点里存了数据和指向下个节点的指针。最后一个节点的next指向NULL链子到这里就算断了。写链表前先要决定一个设计选择要不要头节点。这里说的头节点不是第一个数据节点而是一个额外的空哨兵节点它的data可以不管next指向真正的第一个数据节点。很多教材默认带头节点因为它在代码层面特别方便无论操作的是不是第一个数据节点插入删除的逻辑都完全一致不需要对“头部”做特殊处理。不带头节点的话当你删掉第一个节点时整个链表的入口指针就需要更新这就不是改一个节点的next就能解决的了你需要让外部指向链表的指针也改变。处理不好就会出现“链表还在但入口丢了”的经典bug。实际工程里绝大多数链表实现都会带哨兵节点不仅代码更简单还避免了大量分支判断。学习的时候我强烈建议先从带头节点版本入手吃透之后再去尝试不带头节点的版本感受一下两者的区别。3.2 插入、删除、遍历本质上都在找前驱单链表里有一个贯穿所有操作的思维模式想对某个节点做插入或删除你真正需要掌握的不是这个节点本身而是它的前驱节点。因为链表的操作核心就是修改next指针而修改谁、怎么改完全由前驱节点决定。这样说有点抽象我拆开举例。要在节点p后面插入一个新节点s只需要两步先把s-next指向p-next再把p-next指向s。注意顺序不能反。如果先把p-next变成s那原来p-next后面那些节点就找不到了链表就断了。我经常跟人讲这就好比排队时你要插到某人后面得先拉住后面人的手再把自己接到队伍里顺序反了队伍就散了。删除则更直接要删除节点p后面的那个节点只要让p-next跳过它指向它的next就行了。被跳过的节点如果没用了记得free掉不然就是内存泄漏。而想删除某个特定位置上的节点唯一的办法就是从头部出发用一个prev指针跟在扫描指针后面当扫描指针走到目标位置时prev就是目标节点的前驱。这个过程就是遍历找前驱几乎所以链表操作的复杂度都在这里。#include stdio.h #include stdlib.h typedef struct LNode { int data; struct LNode *next; } LNode; // 创建带哨兵节点的空链表 LNode *createList() { LNode *head (LNode *)malloc(sizeof(LNode)); head-next NULL; return head; } // 在位置pos插入节点pos从0开始计 int insertAt(LNode *head, int pos, int value) { LNode *prev head; for (int i 0; i pos prev ! NULL; i) { prev prev-next; } if (prev NULL) { return -1; } LNode *newNode (LNode *)malloc(sizeof(LNode)); newNode-data value; newNode-next prev-next; prev-next newNode; return 0; } // 删除位置pos的节点 int deleteAt(LNode *head, int pos, int *deletedValue) { LNode *prev head; for (int i 0; i pos prev-next ! NULL; i) { prev prev-next; } if (prev-next NULL) { return -1; } LNode *tmp prev-next; *deletedValue tmp-data; prev-next tmp-next; free(tmp); return 0; }上面代码里头节点就是那个prev的初始值。带头节点版本的好处在这里体现得淋漓尽致不管pos是多少逻辑都一样先让prev走pos步然后判空再操作。你想想如果不带头节点当pos等于0时prev一开始就是NULL后面的逻辑全乱套。3.3 链表反转的迭代三指针法与出错点链表反转可以说是单链表最经典的题型几乎每一步面试都会遇到。迭代法的核心是三个指针prev、cur、next。cur指向当前节点prev指向当前节点的前一个节点next用来临时保存cur的下一个节点防止改指针后找不到后面的路了。循环体里的执行顺序是先记录next cur-next然后把cur-next指向prev接着prev前进到curcur前进到next。当cur走到NULL时prev就是反转后链表的头节点。这个题出错率最高的地方恰恰是“三指针的顺序”。很多人在循环里先执行cur-next prev然后才发现cur-next已经不在了找不回后面的节点。所以next的保存必须在改写指针之前。我建议你初学的时候用一张纸画三个节点模拟每一轮循环把三个指针的移动轨迹一步步画出来比自己空想有效得多也能真正理解为什么需要三个指针而不是两个。LNode *reverseList(LNode *head) { LNode *prev NULL; LNode *cur head; while (cur ! NULL) { LNode *next cur-next; cur-next prev; prev cur; cur next; } return prev; }注意这个函数处理的不带头节点的链表返回的也是新的头指针。如果对带头节点的链表做反转要操作的是head-next那个节点最后再把head-next重新指向反转后的头部。两种模式看起来差不多但细节差一个指针写错了很容易段错误。4. 复杂度之外的真实差距缓存、随机访问与内存碎片4.1 为什么时间复杂度不能直接决定工程选型教科书里通常会给一张表顺序表按下标访问是O(1)按值查找是O(n)插入删除是O(n)链表按下标访问是O(n)按值查找是O(n)插入删除是O(1)。这张表本身没错但它特别容易误导人。最大的误区是“链表插入删除是O(1)所以链表比顺序表快”。实际上单链表中说的插入删除O(1)指的是“你已经知道前驱节点在哪”的前提下只修改指针的操作是O(1)。但在真实场景里你往往不知道前驱在哪你得先遍历找到它这个查找过程就是O(n)。把查找和操作合起来看链表的插入删除在大多数情况下并不比顺序表快。顺序表也不是插入删除就一定慢。因为顺序表可以把插入删除的元素搬移操作交给底层非常高效的连续内存拷贝来做现代CPU和内存控制器对连续地址的访问做了大量优化。所以当数据量不大时顺序表的实际运行时间可能反而比链表更好。复杂度分析给你的是数据规模增长时的趋势而不是某个具体场景下的绝对性能。这个区别在数据量小的时候完全体现不出来一旦数据量上去趋势才会拉开。4.2 缓存行与连续内存顺序表被低估的速度优势做底层性能优化的人常说一句话“内存访问的瓶颈不是带宽是缓存命中率。”CPU从主存取数据时会一次加载一小段连续数据到高速缓存这段数据通常叫缓存行常见的是64字节。如果你的程序顺序访问一块连续内存那么第一个数据进入缓存后后面几十个字节的数据很可能已经在缓存里了读取速度极快。顺序表正好享受这个红利。它的元素紧密排列在连续内存里遍历一遍基本就是让CPU顺着地址往后跑缓存命中率非常高。链表就惨了每个节点在堆上单独分配地址是分散的可能上一个节点在地址A下一个节点在地址B中间隔着十万八千里。每跳到一个新节点大概率都要重新从主存加载这就是缓存未命中。数据量大时链表的实际遍历速度可能比顺序表慢一个数量级这个差距在复杂度表里根本看不到。我在某个项目里试过用链表存几十万条记录做遍历结果慢得离谱换成顺序表之后时间直接降了一个数量级。从那以后我对“链表比数组高级”这种观点就彻底无感了。工程上只要内存能装得下、不需要频繁在中间插入删除顺序表往往是最佳选择因为它简单、连续、缓存友好。4.3 什么场景下选顺序表什么场景下选链表从工程角度我会这样给选型建议。如果主要操作是按下标访问或者需要快速读取一大片连续数据比如图像像素、音频采样点、排行榜数据直接用顺序表没有悬念。如果主要操作是高频地在已知位置插入删除而不是按编号访问比如维护一个活跃用户队列、消息推送任务队列链表更合适因为改指针的代价远低于搬移一大段内存。还有一种特殊情况值得用链表数据规模不确定且每个数据对象本身很大。顺序表扩容时一次性搬移所有元素如果元素是重对象搬移成本高。链表每次只新增一个节点没有整体复制的问题。当然你也可以用“索引数据池”这样的复合结构来平衡但那已经超出线性表本身属于进阶设计了。内存碎片也是个不容忽视的点。链表的每个节点独立malloc大量增删后堆内存会产生碎片听起来是操作系统和内存分配器的事但最终表现为你的程序内存占用越来越高、分配速度越来越慢。顺序表用realloc维护一大块空间分配次数少碎片也少。很多做服务端开发的同学会发现服务运行一段时间后内存碎得厉害检查半天发现罪魁祸首就是一个大量建链拆链的链表结构这种情况换成顺序表或者对象池能立刻好转。5. 高频线性表题型的通用解法5.1 合并两个有序链表递归和迭代怎么选面试和考试里经常出现“合并两个升序链表返回一个新的升序链表”。这道题能同时考察递归思维和指针操作很适合用来分层筛选。递归的做法很简洁。比较两个链表当前头节点的值谁小谁就先被选中被选中节点的next指向“剩下的两个链表合并后的结果”。整个过程其实是在描述一种“原地合并”的逻辑每次只解决一个节点剩下的交给下一次函数调用。递归的终止条件是其中一个链表为空此时直接返回另一个链表。递归的问题在于当链表很长时递归深度可能达到O(n)有栈溢出的风险。所以工程实现里我更推荐迭代法。迭代法需要一个哨兵头节点来简化边界处理然后让指针不断选择两个链表中较小的节点接到结果链表的尾部直到其中一个链表走完再把另一个链表剩余部分一次性接上来。这里处理“剩余部分”时可以直接接整段节点而不是逐个复制这是链表合并能保持O(n)的关键。LNode *mergeTwoLists(LNode *l1, LNode *l2) { LNode dummy; dummy.next NULL; LNode *tail dummy; while (l1 ! NULL l2 ! NULL) { if (l1-data l2-data) { tail-next l1; l1 l1-next; } else { tail-next l2; l2 l2-next; } tail tail-next; } tail-next (l1 ! NULL) ? l1 : l2; return dummy.next; }这段代码里dummy是一个栈上的哨兵节点不必在堆上分配。这个技巧可以避免处理“合并后第一个节点到底是谁”的分支判断返回值永远是dummy.next。你想想如果没有这个哨兵你每次往结果链表尾部接入节点时都要判断“现在是不是第一个节点”代码立刻长出一堆if-else。数据结构里这种用哨兵简化边界的手段非常常见值得反复揣摩。5.2 快慢指针找中间节点和执行顺序的讲究另一个高频题型是“找单链表的中间节点”。如果链表长度是奇数返回正中间那个如果是偶数通常返回后一个中间节点。朴素做法是先遍历一遍数出长度n再从头走到n/2这样要扫描两遍。快慢指针可以把扫描次数压到一遍slow每次走一步fast每次走两步fast到末尾时slow正好在中间。这个题看似简单但执行顺序有讲究。正确写法是先判断fast本身不为空再判断fast-next不为空然后才让slow走一步、fast走两步。如果你先让fast走两步再判断那fast可能已经越过NULL接下来就无法继续。很多人在这个题上翻车不是不懂快慢指针而是没有想清楚fast什么时候为空、什么时候为空的下一个。LNode *findMiddle(LNode *head) { LNode *slow head; LNode *fast head; while (fast ! NULL fast-next ! NULL) { slow slow-next; fast fast-next-next; } return slow; }循环条件里fast ! NULL fast-next ! NULL两个判断缺一不可。缺了前者一旦fast已经是NULL你还会去访问fast-next这就是空指针解引用。缺了后者fast能走到倒数第二个节点时下一步fast-next是最后一个节点但你让它一次跳两步直接越过表尾结果就不对了。这两个条件也是判断链表是否有环的那个经典题的同一个套路。5.3 删除倒数第N个节点哑节点的价值“删除链表倒数第N个节点”是另一道常青树。常规思路是先遍历一遍得到长度再算出来倒数第N个是正数第几个。但很多面试官想看到的方法是“快慢指针一次遍历”让快指针先走N步然后快慢指针一起走等快指针走到NULL时慢指针正好停在目标节点的前一个位置。这个题里最值得学习的是哑节点dummy head的价值。如果要删除的恰好是头节点不带头节点的链表就需要特别处理返回新的头节点但是使用一个哑节点作为头部后删除任何节点都变成“删除某个节点的下一个节点”逻辑完全统一返回值永远是dummy.next。这一步处理放到工程里就是少了一整类边界分支。LNode *removeNthFromEnd(LNode *head, int n) { LNode dummy; dummy.next head; LNode *fast dummy; LNode *slow dummy; for (int i 0; i n; i) { fast fast-next; } while (fast-next ! NULL) { fast fast-next; slow slow-next; } LNode *tmp slow-next; slow-next tmp-next; free(tmp); return dummy.next; }注意这里fast和slow都从哑节点出发而不是直接从head出发。为什么因为如果要删除的是倒数第N个节点慢指针要停在目标节点的前驱上。如果从head开始走当slow走到目标节点时已经太晚了你没法删除一个只知道它自己、不知道前驱的节点。从哑节点开始走slow最终停的位置天然就是目标节点的前驱这就是哑节点在“找前驱”这件事上的额外价值。6. 如何验证你真的掌握了线性表6.1 白纸手写代码和画图自测判断一个人是不是真懂线性表最简单的办法是让他拿张白纸手写代码不许查资料不许IDE提示写完自己画图模拟执行。顺序表要能默写结构体、初始化、插入、删除、扩容。链表要能在半小时内默写出带头节点版的插入删除、反转和找中间节点。画图这一步特别关键。很多同学看书时觉得“就这我看懂了”但一让他画出插入一个节点后每个指针的变化他会卡住。根本原因在于视觉上“看懂”不等于手上“会写”。我学习时有个习惯每写一道链表题就在纸上画三个节点把prev、cur、next的每一步移动都画出来画满一整页之后很多题就自然通了。这个方法又土又慢但效果比刷五十道题还好。你也可以给自己出一道自测题不带头节点的链表删除第一个节点时应该怎么处理如果答不上来说明你对“为何带头节点”的理解还不够深。再出一道顺序表删除最后一个元素时移动循环会执行几次如果脑子里不能立刻反应过来是0次说明你对代码的边界条件还缺乏感觉。6.2 在线评测平台做题时的节奏建议我建议在刷题平台上做线性表专项时不要只盯着题解代码而是先把代码关上只对着题目描述自己手写一版。写不出来可以翻教材但翻完合上书再从头写一遍。这个过程看着慢其实是在建立神经连接。做题顺序上先把最基础的题吃透单链表反转、合并两个有序链表、找中间节点、删除倒数第N个节点。然后进阶判断回文链表、链表排序、两数相加链表表示的数字。这些题看起来花样多但基础技巧不外乎哨兵节点、快慢指针、递归、哑节点用到的全是我前面讲的内容。当你能在一道题里自然地把“哑节点快慢指针反转链表”组合起来用说明线性表这部分算是真的过了。至于代码风格我建议从一开始就养成习惯每次malloc之后想一下这个内存谁来释放每次改指针之前想一下原来的指针还有没有其他引用。写链表题的时候宁可多声明一个临时变量也不要让指针凭空消失。这个习惯会一直受益到后面的树和图因为那里指针更多出错更难排查。6.3 为什么要回到内存视角看线性表最后我想强调一点线性表学习的终点不是把代码背下来而是形成一种“内存视角”。看到顺序表你脑子里应该浮现出一块连续的内存知道下标就是地址偏移看到链表你脑子里应该浮现出一串散落的内存块和它们之间的指针连线。有了这个视角后面学哈希表冲突链、B树的叶子节点、Redis的跳表都会顺畅很多。而且这种视角在工作里非常有用。排查线上问题的时候看到一个几百毫秒的接口耗时懂缓存行的人会想到是不是某个链表结构在大量遍历懂内存布局的人会想到是不是连续内存访问顺序不佳。这些都不是教科书直接教你的但都建立在线性表的基本功之上。我自己在带新人的时候很少要求他们背复杂算法第一关就是手写线性表过不了这关后面根本不用谈。这听起来有点残酷但数据结构这行就是这样基础不牢后续都是空中楼阁。希望看完这篇你能静下心把顺序表和单链表亲手写几遍把边界条件一个一个试出来那比读一百篇总结都管用。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号