资讯详情

链式线性表倒数第K项:容器、栈与双指针三种解法全解析

📅 2026/10/18 1:41:27 | 华诺云谱 👁 阅读
链式线性表倒数第K项:容器、栈与双指针三种解法全解析
1. 这道20分题到底在考什么不止倒数这么简单链式线性表求倒数第K项几乎是每个学数据结构的人都会撞上的题目某OJ平台的7-19就是其中一道20分题。表面看它考的就是遍历链表倒数定位但如果你只在纸上画个链表、心里默念先数长度再走一遍就交上去大概率会踩中几个隐藏的坑。先说清楚题目到底让你做什么输入一串非负整数以某个负整数作为链表数据的结束标记紧接着再输入一个整数K要求输出这条链表倒数第K项的值如果取不到就输出NULL。这里有个典型的误区——很多同学以为必须先自己构造出完整的链式结构老老实实用指针去模拟一个单链表然后再实现倒数第K项的逻辑。但注意标题里给了三个提示词容器、链表、堆栈。这说明出题人并不限定你只能用指针模拟链表它更关注的是抽象能力。你完全可以用容器比如vector或数组存下数据也可以用栈把数据反向压出来甚至可以构造一个真正的单链表再用双指针完成。三种解法都能拿满分只是背后的思考路径完全不同。这道题真正的考察点也在这里第一你能否准确地建模出链式线性表的逻辑特性第二你能否用合适的工具在时间复杂度和空间复杂度之间做取舍第三你能否把所有边界情况都处理干净。K恰好等于链表长度、K大于链表长度、K等于0、链表为空——每一种情况在判题系统里都可能是一组专门构造的测试用例。所以这篇文章我打算把三种主流解法都拆开讲一遍从原理到代码再到互相比较最后把边界和调试经验也聊透争取让你一次把这道题吃干净。2. 解法一容器方案——用vector给节点编号最快的AC路径2.1 核心思路与完整代码容器法是我在OJ上最推荐的抢分写法。思路特别直白读数据时把每个元素原样push到vector里等K也读完之后倒数第K项就是数组里的第 size-K 个元素下标为 size-K。别小看这个思路它的正确性建立在数组顺序与链表顺序一致这一前提上。既然我的目标只是拿到倒数第K个值那我根本不需要关心节点的next指针指向哪只需要保证顺序不变就行。vector天然保存了顺序存取又是O(1)比老老实实构造链表再遍历两次要省事得多。#include cstdio #include vector int main() { std::vectorint arr; int x; while (scanf(%d, x) 1 x 0) { arr.push_back(x); } int k; scanf(%d, k); if (k 0 || k (int)arr.size()) { printf(NULL\n); return 0; } printf(%d\n, arr[arr.size() - k]); return 0; }不少同学第一次写会习惯用while (cin x x 0)这也能过但要注意判断一次只能读一个数别把结束标记和K一起吃掉。上面这份代码已经把结束标记排除在数据之外了当读到的x是负整数时它不会被push进vector循环也随之终止。紧接着读到的整数就是K整体逻辑很干净。2.2 为什么容器法在OJ上最划算从工程角度说vector方案的空间复杂度是O(n)时间复杂度是O(n)本质上是拿空间换实现速度。但判题系统通常不care小规模的空间占用测试数据一般也就几百到几千个节点完全在可接受范围内。更重要的是它的容错率。手写链表最容易出三类问题节点没初始化、尾指针没更新、循环条件写错导致段错误。而vector几乎把所有内存管理问题都屏蔽掉了你只需要关心下标运算。很多同学在OJ上遇到段错误后排查半天其实就是链表节点连接时某个指针没有处理好。用容器方案这种低级错误从源头就消失了。但容器方案也有它不够高级的地方空间复杂度线性。如果题目追加一个要求——只能用O(1)额外空间那vector思路就直接失效。这也是为什么我虽然推荐它作为AC首选但下面两种解法仍然值得好好掌握它们分别代表了借助栈特性解决问题和容器法在空间受限时的替代方案。3. 解法二堆栈方案——后进先出天然适配倒数3.1 栈解法的代码与原理倒数第K项换个角度看就是从链表的尾部往回数K个节点。而栈这种数据结构最擅长的就是倒着看——你把所有数据依次压栈再弹出K次最后一次弹出的就是倒数第K项。整个过程不需要记录链表长度也不需要下标运算逻辑上甚至比容器法更贴合倒数这个语义。#include cstdio #include stack int main() { std::stackint st; int x; while (scanf(%d, x) 1 x 0) { st.push(x); } int k; scanf(%d, k); if (k 0 || k (int)st.size()) { printf(NULL\n); return 0; } while (--k) { st.pop(); } printf(%d\n, st.top()); return 0; }这里有个细节值得留意while (--k)会先自减再判断所以当k2时循环只执行一次pop栈顶元素就是倒数第2项。这个写法比while (k--)少pop一次不用额外手动调整。我见过不少同学在这个循环条件上写错过稍微不留神就多弹了一个元素。3.2 栈解法的内存特征与适用场景栈方案的时间复杂度同样是O(n)空间复杂度也还是O(n)因为你需要用额外的栈容器保存所有数据。从这个角度看它和vector方案本质上是同一级别的开销只是换了一种顺序反转的手段。那栈方案的优势在哪里它更适合数据源本身是流式的、无法预知长度的场景。比如数据不是一次性给全而是一个个从网络接口或文件流里读入你不知道总共有多少项。vector在扩容时会频繁拷贝数据而栈的push操作在内存管理上更自然。当然在OJ上两者的实测性能差距几乎可以忽略选哪个纯粹看个人习惯。但我不建议你在面试或考试中只提容器法和栈法各一句就完事。面试官大概率会追问能不能把空间复杂度压到O(1)这时候如果你能说出第三种解法——双指针——才是真正的加分点。接下来就聊它。4. 解法三双指针方案——单次遍历的最优解4.1 快慢指针的运作原理双指针解法也叫快慢指针法是这道题在空间复杂度O(1)约束下的标准答案。思路相当巧妙先让快指针从头节点出发先走K步然后慢指针从头节点出发两指针同步前进。当快指针走到链表末尾的NULL时慢指针恰好停在倒数第K个节点上。为什么成立因为快指针比慢指针始终领先K个身位。快指针到达NULL时它走过的总长度是L那么慢指针走过的长度就是L-K这个位置正好是倒数第K项所在的下标。整个过程只遍历了一次链表没有额外开辟线性空间是三种解法里最优雅的。但如果题目不给一个已经构造好的链表你需要先自己建链表再用双指针处理代码量比前两种明显大。这也是它作为面试向解法的特性——在OJ抢分时不一定划算但在考察你链表基本功时很有价值。4.2 手工构造单链表并完成倒数查找的完整实现#include cstdio #include cstdlib struct Node { int data; Node* next; }; int main() { Node* head NULL; Node* tail NULL; int x; while (scanf(%d, x) 1 x 0) { Node* node (Node*)malloc(sizeof(Node)); node-data x; node-next NULL; if (head NULL) { head node; } else { tail-next node; } tail node; } int k; scanf(%d, k); if (k 0) { printf(NULL\n); return 0; } Node* fast head; Node* slow head; int steps 0; while (fast ! NULL steps k) { fast fast-next; steps; } if (steps k) { printf(NULL\n); return 0; } while (fast ! NULL) { fast fast-next; slow slow-next; } printf(%d\n, slow-data); return 0; }注意代码里的细节首先处理K0的情况防止先走K步时出现非法操作其次在让快指针先走K步时用一个steps变量记录实际走过的步数。如果链表长度小于K那么快指针在走完之前就会碰到NULL此时steps k直接输出NULL。这个判断非常重要少了它后面printf(%d\n, slow-data)会对空指针解引用导致段错误。还有一个容易忽略的细节如果K恰好等于链表长度那么快指针走完K步之后正好停在NULL上。此时进入第二个while循环的条件fast ! NULL不成立循环一次都不执行慢指针仍然停在头节点。头节点就是倒数第K项吗是的倒数第L项就是首元节点这个逻辑是自洽的。所以不需要为K等于链长单独加分支。当链表为空且K0时上面的while循环在第一步就因为fast NULL退出steps还是0steps k成立同样会输出NULL不会访问空指针。如果链表为空且K0则会先被k 0拦截输出NULL。这样四种异常场景空链表K0、空链表K0、非空K链长、K0都被覆盖了。5. 边界条件与隐藏扣分点K值异常时的完整处理清单5.1 完整边界清单与预期行为很多同学这道题拿不到满分不是因为主逻辑写不出来而是边界处理不到位。我把自己踩过的、以及帮别人排查过的边界情况汇总成一张表你写代码前对着过一遍基本就能保证提交后不再被边界用例打回。场景说明预期输出K为0或负数倒数第0项无意义NULL链表为空且K0没有节点可取NULL链表为空且K0先拦截K值非法NULLK等于链表长度倒数第L项为首元节点首元节点的值K大于链表长度快指针先走不完NULLK为1应该是最后一个节点尾节点的值链表只有一个节点且K1首尾节点重合该节点的值注意K等于链表长度和K大于链表长度这两种情况非常容易被混为一谈。K等于链长是合法的要输出头节点的值K大于链长才是非法的要输出NULL。区别就在于让快指针先走K步时快指针是否能在不越界的情况下走完K步。能走完则合法走不完则非法。5.2 边界处理的20分意义OJ判题通常会把测试点拆开一个测试点对应一组特殊输入。边界用例绝对不只是附加题——很多同学主逻辑对了却因为漏了K0或K大于链表长度直接被扣掉4到6分。20分的题因为一个小分支丢掉5分可能直接让你的总排名掉一个档次。双指针版本中的steps k判断尤其重要。假如你已经知道链表长度TeLength你也可以先算出len然后判断k len但题目输入没有给你链表长度你必须边构建边计数。最稳妥的做法是构建链表时用一个变量cnt记录节点总数然后判断k cnt。这和steps k是等价的但显式记录长度更容易读、更容易防止低级错误。我推荐在代码里同时做两层防护第一层是k 0第二层是构建完链表后用节点总数和K比较。如果你用的是容器方案那k arr.size()就是唯一的长度判断如果用了栈方案那就是k st.size()。总之K值的各种异常组合必须在输出前被全部拦截住绝不能让它带着非法值进入指针运算或下标运算。6. 复杂度账本与选型思路判题、面试、工程场景怎么选6.1 三种解法的复杂度对比三种解法的正确性都没问题它们之间的差异集中在空间开销、代码量、以及你对数据结构特性的理解深度上。下面这张表可以作为选型参考解法时间复杂度空间复杂度是否需手工构建链表实现难度适用场景容器法O(n)O(n)不需要低OJ快速AC堆栈法O(n)O(n)不需要低流式数据/反向输出双指针法O(n)O(1)需要中面试/空间受限容器法和堆栈法在复杂度上完全一致差异只是抽象工具不同。双指针法在空间上胜出但代价是需要维护一个真正的手工链表并且要考虑空指针解引用的问题。从纯算法角度看双指针法是最优解。但在OJ上我会毫不犹豫选择容器法或堆栈法。原因很实在OJ只关心你的答案对不对、跑得快不快不关心你用了多少额外内存。测试数据规模通常只有几百到几千个节点多开一个vector或stack根本不会触发内存限制。既然代码越短、越不容易出bug就越值得优先选。6.2 从面试官视角看这道题面试官如果问这道题意图往往也在第三层。他先看你能否用容器法快速给出正确解这是考察基础实现能力然后追问能不能不用额外存储空间这是考察你对链表结构的理解和对双指针技巧的掌握如果再用如果这是一个只知道头节点的单向链表来强调一遍那就是看你会不会做那个O(1)空间的优化。我在模拟面试时见过不少候选人卡在最后一步。思路是懂的说两个指针差K步但一写代码就忘记处理K大于链长的情况或者让慢指针在快指针还没走完K步时就提前进入第二步循环。所以你自己练习时建议三种写法都各实现一遍特别是双指针版本要反复跑边界用例形成肌肉记忆。工程场景里双指针思路也更常用。比如日志系统里统计倒数第K条记录、分页场景下定位倒数第K页的偏移这类问题往往不希望你一次性把所有数据加载到内存。维护一个头部指针和一个滞后K位的指针在数据流上边读边处理才是更可持续的做法。7. 提测自查与易错点复盘从编译错误到段错误7.1 常见错误清单哪怕思路完全正确提交时也可能被各种实现细节绊倒。我把容易出现的问题列在这里每一条都是我或周围同学真实踩过的坑误把结束标记当成数据结束标记是负整数应该只用来终止循环不能被push进容器或压入栈。如果读入逻辑写成了do...while很容易多存一个结束标记。K读取时机不对K必须在所有链表数据读完之后再读不能放在循环里。要是把K也当作链表数据的一部分结果会变成找倒数第K个负整数的位置完全偏离题意。忘了K0的情况双指针版本里如果K0fast先走的循环条件无法处理这属于非法输入必须单独拦截。空链表下的空指针解引用输出NULL前千万别访问任何指针。特别是双指针版本如果链表为空slow-data会直接触发段错误。在循环里移动tail时没考虑头节点构建链表时第一个节点要赋值给head后续节点才接到tail-next后面。漏掉第一个节点的特殊情况会导致链表开头多一个未被正确链接的节点。7.2 自测用例设计提交之前我建议你至少跑一遍下面这几组输入确认输出和预期一致输入: 1 2 3 4 5 -1 2 输出: 4 输入: 1 2 3 4 5 -1 5 输出: 1 输入: 1 2 3 4 5 -1 6 输出: NULL 输入: 1 -1 1 输出: 1 输入: -1 1 输出: NULL 输入: 5 -1 0 输出: NULL这六组覆盖了正常情况、K等于链长、K大于链长、单节点链表、空链表、K为0的非法值。视频演示可能看着直观但自己动手跑到输出全部正确后再提交才是稳妥的做法。还有一种自查思路是构造中间位置的大规模数据验证双指针移动过程中没有丢失节点。比如生成一万个连续整数K取5000输出应该是第5001个整数也就是5000。这类随机大样例可以帮你排除构建链表时指针错位的偶发问题。最后说一句个人习惯我在OJ上提交这道题时会优先用容器法AC之后再专门用双指针法写一遍作为练习。两种方式都跑通才算真正把这20分吃透了。毕竟考试和面试不会只考你一次倒数第K项它背后的链表操作、边界思维、空间换时间判断题才是任何时候都通用的东西。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑