很多人在学数据结构的时候第一关就是线性表。这门课学得顺不顺手很多时候就看线性表这一章有没有真正学明白。它不只是一张表那么简单后面几乎所有数据结构栈、队列、串、树、图都建立在“把一组数据组织成一个序列”或者“反过来对序列做增删改查”这套思维方式上。甚至可以说如果你把线性表吃透了再学后面的内容会顺畅得多。这篇文章主要面向三类读者正在上数据结构课的学生要考408或者自主命题考研的同学以及自学数据结构准备面试的开发者。我会用C语言来讲解核心代码因为C语言在考研、面试和实验报告里都是主流语言另外还会穿插一些我在实际写代码和带人过程中积累的经验。只要你能耐心读完并且跟着敲一遍代码线性表这一章基本就没问题了。1. 线性表到底是个什么东西1.1 一句话解释逻辑上排队的元素先别去背定义。线性表本质上就是一群元素排成一条队伍。队伍里每一个人除了第一个没有前一个最后一个没有后一个其他人都恰好有唯一的前一个和唯一的后一个。这句话包含了线性表的三个关键信息元素之间是一对一的关系不存在“一个元素有两个直接后继”的情况每个元素有位置通常叫位序从1开始不是从0开始C语言数组下标才是从0开始这个区别很多人会搞混操作都是针对这个序列做的比如在第i个位置插入一个元素、删除第i个元素、查找某个值。我的建议是学的时候先把“逻辑结构”和“物理结构”分开。逻辑结构描述的是数据元素之间的关系线性表就是一种逻辑结构物理结构描述的是这些数据在内存里到底怎么放的。同样的逻辑结构可以有不同的物理实现方式这就引出了接下来的两种主力实现顺序表和链表。1.2 两种物理存储思路顺序存储与链式存储顺序存储很好理解找一块连续的内存空间把元素一个个挨着放进去就像一排连号座位你坐1号座旁边就是2号座。链式存储就不一样了每个元素除了存数据还要额外存一个“指向下一个元素的地址”就像寻宝游戏你拿着线索找到下一个人那个人再告诉你下一个线索在哪。这两种方式没有绝对的好坏只有适不适合。顺序存储适合随机访问链表适合频繁插入删除——这个结论背后的原因后面我会展开讲。结论先放在这里线性表是“逻辑上排队的数据”顺序表和链表是它的两种物理实现。理解了这一层下面学代码才有框架感不然纯粹是在背代码。2. 顺序表一个数组搞定一切2.1 结构定义与初始化顺序表在C语言里常用的实现方式是动态数组。为什么不用静态数组因为静态数组的大小编译时就得定死你没法预知实际要存多少数据开大了浪费内存开小了不够用。动态分配就灵活很多不够了可以扩容。先看结构体定义#define INIT_CAPACITY 10 typedef struct { int *data; // 指向动态分配数组的指针 int length; // 当前实际元素个数 int capacity; // 当前容量 } SeqList;初始化的时候先分配一块初始容量的内存然后把length置为0。这里有一个我踩过很多次坑的地方如果你在初始化函数里用malloc分配了内存那么使用完这个顺序表之后一定要free(data)否则就是内存泄漏。写实验报告的同学尤其容易忽略这一点因为程序运行一次就退出了泄漏了也看不出来。LeetCode刷题多了内存泄漏不报错但大型项目中这是很严重的问题。void initSeqList(SeqList *L) { L-data (int *)malloc(INIT_CAPACITY * sizeof(int)); L-length 0; L-capacity INIT_CAPACITY; }2.2 插入和删除为什么必须移动元素顺序表最核心的操作就是插入和删除几乎所有面试题和考试题都围绕这两个操作展开。先看插入的逻辑想把元素e插到第i个位置首先得判断i是否合法既不能小于1也不能大于当前length1然后从最后一个元素开始依次往后挪一个位置给第i个位置腾出空位最后写入elength加1。int insertSeqList(SeqList *L, int i, int e) { if (i 1 || i L-length 1) { return 0; // 位置不合法 } if (L-length L-capacity) { // 扩容重新分配更大的内存或用realloc L-capacity * 2; L-data (int *)realloc(L-data, L-capacity * sizeof(int)); } for (int j L-length; j i; j--) { L-data[j] L-data[j - 1]; } L-data[i - 1] e; L-length; return 1; }注意这里的for循环方向从后往前移动而不是从前往后。如果你从前往后移动前面的元素就会覆盖后面的元素数据就乱了。我自己刚学的时候犯过这个错误把整个数组搞成一串重复的数字。这个细节也是很多数据机构实验课老师喜欢问的考点“为什么插入时要倒着移动”删除操作逻辑类似第i个元素后面的所有元素都往前挪一个位置然后length减1。理论上被覆盖的最后一个位置不需要清理因为后续插入会覆盖它但养成良好的习惯可以手动把data[length]置0方便调试时观察内存状态。int deleteSeqList(SeqList *L, int i) { if (i 1 || i L-length) { return 0; } for (int j i; j L-length; j) { L-data[j - 1] L-data[j]; } L-length--; return 1; }删除的时间复杂度是O(n)因为在最坏情况下删除第一个元素所有元素都得往前挪。平均也是O(n)。插入同理。为什么顺序表插入删除慢答案就是“挪元素”本身成了瓶颈。这也是后面选择用链表来优化这个场景的直接原因。2.3 顺序表的优缺点和适用场景优点很明确随机访问快。你只要知道下标直接O(1)就能拿到元素不需要从头找。这个特性在需要频繁“按位置查数据”的场景里是压倒性优势。另一个容易被忽略的优点是对缓存友好。数组在内存里是连续存储的CPU访问一次内存会把相邻的一段数据都加载到高速缓存里下次访问相邻元素直接命中缓存速度非常快。相比之下链表结点分散在内存各处每一次跳转都可能发生缓存未命中。缺点也很明显插入删除要搬大量元素扩容可能涉及整块内存的拷贝realloc可能重新分配并把旧数据复制过去。所以如果你提前就知道元素个数基本固定或者需要频繁随机访问顺序表是最佳选择。3. 链表不连续也能线性3.1 单链表的结构与结点定义单链表是链表家族最基础的一种。它的结点包含两个部分数据域存数据指针域存下一个结点的地址。C语言里用结构体表示typedef struct LNode { int data; struct LNode *next; } LNode;每个结点都散落在内存各个角落通过指针串成一条链。这种“物理上不连续逻辑上连续”的思想经常让初次接触的人觉得抽象。我的经验是链表的操作中你要时刻记住一句话永远不要在你需要用到指针之前把它弄丢。很多链表bug都是因为指针指向的位置在你不知情的情况下变了结果后续操作全乱了。3.2 头插法和尾插法如何构造一个链表构造链表有两种最常见的方法头插法和尾插法。头插法每次把新结点插到链表的头部。代码非常短插入顺序和结果顺序相反。也就是说你按1、2、3的顺序插入最终链表里存的是3、2、1。这个特性经常被拿来面试考察——链表反转的经典做法之一就是用头插法重新构造链表。LNode *createByHead(int arr[], int n) { LNode *head NULL; // 头指针初始为空 for (int i 0; i n; i) { LNode *newNode (LNode *)malloc(sizeof(LNode)); newNode-data arr[i]; newNode-next head; // 新结点指向旧的第一个结点 head newNode; // 头指针指向新结点 } return head; }尾插法每次把新结点插到链表尾部。这个更符合直觉插入顺序和结果顺序一致。但注意尾插法需要维护一个尾指针否则每次插入都要遍历到链表结尾复杂度就变成O(n²)了。LNode *createByTail(int arr[], int n) { LNode *head NULL, *tail NULL; for (int i 0; i n; i) { LNode *newNode (LNode *)malloc(sizeof(LNode)); newNode-data arr[i]; newNode-next NULL; if (head NULL) { head newNode; // 第一个结点既是头也是尾 } else { tail-next newNode; } tail newNode; } return head; }关于头结点很多教材尤其严蔚敏版会用到头结点。头结点是一个不存实际数据的结点它存在于链表的头部目的是统一空表和非空表的操作逻辑。有了头结点在头部插入/删除的时候就不需要单独修改头指针。但408统考和很多面试题里面一般默认链表不带头结点或者会明确问你“带头结点还是不带头结点”。写代码前一定要先确认清楚否则你的操作逻辑可能完全不一样。我自己在带学生的时候就发现很多人没搞清头指针和头结点这两个概念导致写“删除第一个元素”这种基本操作都会报错。3.3 链表的插入删除修改指针的“三步走”原则单链表的插入删除也是高频操作。插入一个结点的核心是找到前驱结点p然后修改指针。// 在p结点之后插入一个新结点s s-next p-next; p-next s;注意这两条语句的顺序不能颠倒。如果你先执行p-next s那么原来的p-next也就是s后面的那个结点就找不到了s的新链就断了。所以先接后断先让s指向p原本的后继再让p指向s。同样的道理也适用于删除删除p的后继结点只需要让p-next p-next-next然后free掉被删的结点。为什么操作这么简单因为单链表删除的本质就是“绕过被删除的结点”不需要像顺序表那样搬动大量元素。这也是链表插入删除快的根本原因。但要注意链表的插入删除虽然只需要O(1)时间前提是你已经知道前驱结点的位置。如果需要先找到前驱查找本身又要O(n)那就没有想象中那么香了。3.4 链表查找时间换空间的典型链表的随机访问很弱想找第i个元素必须从头指针开始一个个跳过去时间复杂度O(n)。很多人刚学链表的时候总觉得链表很高级、很灵活但真正写代码才发现它麻烦不能直接取下标遍历才能访问内存还要每结点多花一个指针的空间。这个认知很有必要纠正一下——链表并不是顺序表的全面升级版它是在某些特定场景下才更合适的替代方案。链表还有一个特点它对内存的要求很低数据可以分散存放。顺序表必须找一块大且连续的存储区链表只需要一个一个的小空间。所以如果你面对的是碎片化内存、插入删除频繁、数量不确定的场景链表会更有优势。但现实工程里顺序表数组仍然是绝对的主流原因后面详细说。4. 顺序表 vs 链表选型不是拍脑袋4.1 从时间复杂度、空间开销、缓存友好三个维度对比很多学生问我到底什么时候用顺序表什么时候用链表这个问题的标准回答不应该是“看情况”而是一套清晰的判断标准。我习惯从三个维度来比较维度顺序表链表随机访问O(1)直接下标O(n)需要遍历头部插入/删除O(n)所有元素都得挪O(1)只需改指针尾部插入已知尾指针O(1)直接写到末尾O(1)但需要维护尾指针中间插入O(n)挪元素O(n)查找O(1)插入合计O(n)额外空间基本无每个结点多存一个指针缓存友好性高连续存储低离散存储注意上面表格里的中间插入很多人以为链表中间插入是O(1)这是个经典错误。链表只是“指针修改”是O(1)但你要先找到插入位置这个查找过程是O(n)。所以如果你要在一大堆数据里反复做中间插入链表并不比顺序表快多少。4.2 结合实际场景的选型判断标准我总结了一套简单的选型逻辑拿去就直接用如果你主要操作是按位置访问比如第n个元素是谁无脑选顺序表如果你主要操作是按值查找顺序表和链表都要O(n)但顺序表常数项更小优先顺序表如果你主要操作是头部插入删除且数据量很大链表占优如果你无法预估数据量上限链表更灵活因为顺序表扩容有代价如果内存碎片严重、大块连续内存分配不出来只能链表。这里额外插一句实际工程中数组顺序表的使用频率远高于链表。原因除了缓存友好之外还有一个容易被忽视的点——链表每访问一个结点都要做一次指针跳转而每一次跳转都可能触发内存访问延迟。在现代CPU架构下访问连续内存比随机散布的内存要快得多这种性能差异在数据量大时会变得非常明显。所以不要一听“链表插入删除快”就觉得它更高级。这个误解几乎每个初学者都经历过包括当年的我。4.3 关于“链表比数组厉害”的一个澄清再展开说一句这个误解。链表在面试里经常出现给人的感觉是“考得多的东西更难更有用”。实际上链表之所以常被考查是因为它本身容易出边界问题空指针、头结点、指针修改顺序等适合用来考察你有没有真正理解指针和内存而不是因为它在实际工程里更强。真正的大规模数据存储和检索靠的是数组、哈希表、跳表、B树这些。链表更多是作为这些结构的底层组件出现。理解了这层你就不会把时间浪费在“到底哪个好”的口水仗上了。5. 实验报告/考试常见问题与避坑指南5.1 写实验报告时最容易忽略的几个点写数据结构实验报告的同学我每次批改都会发现几类共性问题这里统一说下第一初始化函数里的参数传递问题。如果你用void initSeqList(SeqList L)在函数内部修改L的data和length是不会影响到外面的L的。因为C语言的函数参数是值传递形参是实参的一份拷贝。正确做法是传指针void initSeqList(SeqList *L)。很多同学写完了报错但检查半天找不出原因就是因为这个基础问题没搞懂。第二野指针和空指针。链表删除最后一个结点之后那个结点的指针如果不置NULL后面再次遍历时会访问一个已经free掉的内存区域这种行为是未定义的。写代码的时候不要以为free了就万事大吉建议被free的指针手动赋NULL。第三逻辑边界没考虑。比如插入位置的判断i0或者ilength2这两种越界情况都要排除。很多同学的代码在“正常情况”下能运行但一测边界就挂因为压根没做过边界检查。5.2 操作中必踩的坑我在自己写代码和带人的过程中总结了几个经典坑几乎是必踩的插入时忘记检查位置合法。不检查i的范围直接操作数组或者链表会写出越界内存轻则数据错乱重则程序崩溃。删除时没有释放内存。C语言不像Java那样有垃圾回收你不free内存就一直占着。考试题一般不查这个但实验报告里老师会看。链表指针修改顺序错了。这个前面强调过先接后断。写错了你会发现链表断成了两截后半截找不回来。遍历时指针跑到NULL。比如想遍历到倒数第二个结点来删除最后一个结点循环结束条件写错了指针变成NULL下一轮还想访问它的next直接段错误。**参数想改头结点却传了LNode *而不是LNode****。这也是链表头插法经常遇到的问题。*其实是在说你想在函数里改变外部head的值必须传head的地址即LNode **head。这是一个非常高发的错误我在带人时反复强调过。5.3 考研/面试高频考点速查如果你在准备408或者面试这里给你一个高频考点速查表比漫无目的地翻书效率高很多考点判断要点线性表的抽象类型定义能说出基本操作集合及含义InitList、ListInsert、ListDelete、LocateElem顺序表插入/删除最坏情况O(n)平均移动n/2个元素顺序表扩容realloc可能搬移整块数据最坏O(n)单链表头插法特点结果顺序与输入顺序相反可用于实现链表反转链表删除结点修改前驱的next指针删除最后一个结点需要找到倒数第二个结点双链表插入需要修改前后两个方向共4个指针顺序同样关键循环链表判空条件头结点的next是否指向它自己带头结点时链表找中间结点快慢指针法快指针到末尾时慢指针指向中间结点合并两个有序链表归并思想递归或迭代都行注意空链表边界表格里的每一项对照你自己的理解如果都能说出“是什么、为什么、怎么写”线性表这章就没问题了。如果某些术语陌生赶紧回去翻课本针对性补。6. 实操心得从“背代码”到“会写”的转变6.1 画图画图画图学链表的正确姿势不是盯着代码看而是先在纸上画图。你先画一个方框代表结点方框分成两半左半写数据右半画一个箭头指向下一个结点。然后在纸上模拟插入、删除操作把一个箭头拆掉接上新的箭头。每一步都画出来你会发现链表操作其实是一套非常机械的规则。一旦在纸上画通了写代码就是翻译的过程不会卡壳。我记得自己大二学链表的时候也是云里雾里后来在纸上画了一整页的箭头和方框突然就开窍了。后来我带学弟学妹推荐这个方法反馈基本都是“画完就懂了代码照着画写就对了”。真的建议所有觉得链表难的人都试一下这个方法不花一分钱但效果比看十段视频都强。6.2 亲手写一遍、调试一遍比看十遍视频有用不少初学者把视频看了一遍又一遍觉得“老师讲的我都听懂了”一动手就废。原因很简单听只是输入写才是输出。线性表的代码真的不多你完全可以自己从零开始把顺序表和单链表各写一遍不要抄写完再对比标准答案。写错的地方就是你没有理解的地方调试器里看变量变化比任何讲解都直观。对于时间和精力有限的考研党我的建议是重点掌握顺序表的插入删除、单链表的头插法建表、尾插法建表、按值查找、按位查找、插入删除这几个核心函数能用C语言默写出来。这是很多408考生总结出来的基础纸面功夫。6.3 一点给考研同学的考前复习建议最后给考研的朋友加个餐。线性表这一章在408数据结构科目里属于选择题和算法题的基础内容。选择题喜欢考复杂度分析和细微概念大题经常把线性表和其他结构栈、队列、树结合来出比如用栈实现括号匹配、树的孩子兄弟表示法等。所以复习的时候不要只盯着代码要把“复杂度的计算过程”和“结构的转换关系”也讲给自己听。按我个人的经验考前一个星期把线性表的代码拿出来重新默写一遍尤其是插入删除和链表反转手不能生。数据结构这门课不像文科只要熟悉就能拿分它必须靠写。多花30分钟在代码上比多背30分钟理论强很多。建议拿一支笔一张纸对着题目直接写代码写完再对着教材核对自己哪里漏了、哪里错了。这个方法我亲测有效带过的学生里面反馈也很好。
企业数字化 ERP 产品动态
相关推荐
手机浏览器可运行中秋祝福代码 <!DOCTYPE html>
<html lang"zh-CN">
<head>
<meta charset"UTF-8">
<meta name"viewport" content"widthdevice-width, initial-scale1.0, maximum-scale1.0, user-scalableno">
<title>中秋节快… · 2026/9/26 6:03:22
Claude Code模板体系实战:从提示词沉淀到AI编程稳定输出 去年年底开始重度使用 Claude Code 之后,我养成了一个习惯:每次动手写代码之前,先想清楚要给模型喂什么。因为踩过的坑太多了——同一段代码,上午让它审查是一套输出,下午再跑一遍又是另一套,差别大到你以为… · 2026/9/26 6:03:22
Cursor账户登录报错与避坑指南:从风控机制到订阅计费 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 6:03:16
OpenRouter Batch API批量推理半价实战:异步批处理省钱指南 1. 批量推理这件事,为什么值得单独聊做AI应用开发的朋友,十有八九都经历过这样的场景:产品上线前要跑一轮全量数据评测,或者半夜定时任务要处理几万条用户提交的文本,又或者做数据清洗时需要对几十万条记录逐条过一遍大… · 2026/9/26 7:01:57
Claude Code 模板工程化:用 CLAUDE.md 与指令模板固化高效工作流 上个项目折腾了一个星期的 Claude Code 配置,最终发现“模板”才是真正拉开效率差距的东西。这个项目标题叫 claude-code-templates,说白了就是围绕 Claude Code 的一套可复用配置与工作流模板,核心文件是 CLAUDE.md,配合各种指令… · 2026/9/26 7:01:57
OpenRouter Batch API 批量推理实战:半价成本与工程化避坑指南 1. 批量推理这件事,为什么值得单独聊做AI应用开发的朋友大概率都遇到过这种场景:白天用户请求稀稀拉拉,晚上跑数据清洗、内容打标、离线摘要的时候,几万条文本要过一遍大模型。这时候你会发现两件事——第一,钱烧得比想… · 2026/9/26 7:01:57
A-MLE智能体框架:广告排序模型自动化实验实战指南 1. 广告排序模型实验为什么需要智能体框架广告排序模型是推荐和广告系统里最核心的模块之一,它决定了每一次曝光机会该给哪条广告、出价多少、排序位置怎么排。做过这块的人都知道,模型迭代的瓶颈往往不在算法本身,而在实验流程的繁琐程度。一… · 2026/9/26 7:01:57
BGE-M3文本嵌入模型实战:RAG检索增强生成中的部署、调优与避坑指南 1. 为什么文本嵌入模型值得单独拿出来聊做检索增强生成(RAG)项目的朋友大概率都经历过这样一个阶段:知识库搭好了,向量数据库也连上了,但检索出来的内容就是不对味。问“如何申请年假”,返回的却是“员工福… · 2026/9/26 7:01:57
金融技术服务落地的四大要素解析 我无法基于当前输入生成符合要求的博文。原因如下:项目标题 "financial-services" 过于宽泛:它是一个行业大类术语,而非具体可落地的项目、工具、方法或现象。它不指向任何明确的技术实现、操作流程、问题场景或创新实践࿰… · 2026/9/26 7:01:51
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍 简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践 一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46