☰
链表双指针经典题:返回倒数第k个节点全解析
2026/10/18 5:04:46 网站建设 项目流程

面试题这东西,题库里一堆“剑指offer”原题,很多同学都是背答案过的。但面试官不傻,他问“返回倒数第k个节点”这道题,表面上是考链表操作,其实是在试探你有没有真正理解指针、边界和复杂度分析,是不是只会背模板。我当年面试也栽在这道题的变体上,后来自己带人时也常拿它当试金石。今天不背书,就纯从实战角度把这题掰开揉碎,讲讲它的考点、两种主流思路、边界坑、以及怎么在面试现场展示出老手的水平。

1. 题目拆解与考点定位:面试官到底在问你什么

1.1 题目本身的含义

“返回倒数第k个节点”,输入的是一个单链表头节点(或者叫头指针)和一个正整数k,输出的是从链表尾部往前数第k个节点。

单链表长什么样,我再啰嗦一句:每个节点有数据域(val)和指针域(next),只能从head开始,顺着next箭头一个一个往后走。和数组最大区别是——你不知道整条链有多长,也不知道某个位置怎么直接跳过去。这就决定了我们不能像数组那样用下标一步到位,必须靠遍历去“逼近”目标。

有个容易混淆的点:题目说的是“返回倒数第k个节点”,通常返回的是那个节点的指针/引用,而不是节点的值。只要你返回了节点,拿.val取数值只是调用方的事情。面试时建议直接问一句“返回值类型是什么”,这既是澄清需求,也是展示你习惯性做需求确认的好习惯。

1.2 这道题为什么是面试高频题

高频是有道理的,因为它考点密度高,题面又短,十分钟之内就能看出一个人的基本功:

  • 数据结构基础:链表遍历、指针操作,懂不懂链表的物理结构。
  • 边界思维:空链表、k过大、k等于链表长度、k等于0,每一个都会出问题。
  • 复杂度意识:很多人第一反应是先遍历一遍求长度,再走一遍;能不能想到双指针一次遍历,就是一次优化思维的体现。
  • 鲁棒性:非法输入怎么处理,是抛异常、返回空、还是和面试官讨论约定?不同公司代码规范不一样,能主动提出来说明你考虑过生产环境里的健壮性问题。

1.3 一个容易被忽略的细节:单链表的天然限制

数组和向量能随机访问,是因为数据在内存里连续排布,加上下标偏移就能定位。单链表每个节点是分散分配的,通过指针串联,你想知道某个节点是倒数第几,唯一的办法就是从头走一遍。很多初学者会用“把链表转成数组”的暴力思路,实际上这就是抓住了链表“只能顺序访问”的本质,只不过代价大而已。理解这一点,后面双指针解法为什么“省一次遍历”你才能真正看明白——不是省代码量,是省了一次从头再走的开销。

2. 暴力解法先热身:两次遍历法的思路与局限

2.1 思路推导

最直观的思路:链表不知道长度,那先数一遍不就知道了吗。

第一步:从头遍历,用计数器count记录节点个数,比如链表是 1 -> 2 -> 3 -> 4 -> 5,那么count就是5。 第二步:想拿倒数第2个,也就是正数第4个节点(4)。公式很好推:index = count - k + 1,从1开始计数的话,倒数第k个就是正数第count - k + 1个节点。注意有的语言或伪代码下标从0开始,那公式变成count - k,这个坑我后面专门说。 第三步:从头再走count - k步(如果从0计数),就能停在第k个节点上。

伪代码长这样:

def find_kth_from_end(head, k): if head is None: return None count = 0 cur = head while cur is not None: count += 1 cur = cur.next # 倒数第k个,是正数第 count - k + 1 个(1-based) # 因此从head走 count - k 步(0-based步数) steps = count - k if steps < 0: return None # k太大 cur = head for _ in range(steps): cur = cur.next return cur

2.2 复杂度分析

这个解法时间复杂度是O(n):第一次遍历数长度是O(n),第二次遍历走到目标节点,最多也是O(n),加起来还是O(n)。空间复杂度O(1),没有额外数组或栈。

有同学说,这不挺好?O(n)已经不错了。

问题是:你遍历了两遍。如果这是一个超长的链表(比如几百万个节点的日志链),或者链表节点是从磁盘/网络流式加载的,第一遍数完,第二遍还没法重头再来(数据流没法回退)。双指针解法就能在这种场景下拿到优势。所以面试官往往会在你写完暴力解后追问一句“能不能只遍历一次”,这就是亮出双指针的时机。

2.3 暴力解法存在的坑

第一,如果k大于链表长度,steps会变成负数。上面代码里我做了防护,但很多人写的时候会忘记,访问到None.next就抛空指针异常。

第二,边界上k等于链表长度时,倒数第k个就是头节点本身,steps等于0,循环直接跳过,返回head。这个没问题,但你要确保自己的循环起点没写错。

第三,链表只有一个节点时,k等于1要能正确返回该节点。表层逻辑没问题,但如果你把公式记反了,定位错一个位置就会返回Null。

暴力解法不是错误答案,它是一个“正确但可优化”的答案。面试中先给暴力解,再主动优化,反而显得你思路有层次。关键是不能停在暴力解上。

3. 双指针一次遍历:经典解法背后的原理

3.1 核心思想用一句话说透

让两个指针保持k-1个节点的间距,从头同步往后走,当快指针(前后那个)走到链表尾部时,慢指针正好停在倒数第k个节点上。

为什么间距是k-1而不是k?动手推一下:链表长度为n,倒数第k个节点离尾部有k-1个next(因为最后一个节点到末尾不用再走,所以倒数第1个节点到null是0步,倒数第k个到null是k-1步)。让front先走k步,front会到达正数第k+1个节点(假设k不越界)。此时front到链表尾null还剩n - k步。让front和back同步走,都走n - k步后,front到达null,back走了n - k步后停下的位置是正数第(n - k + 1)个节点?等等,我重新数一下。

更稳妥的推导:back初始在head(正数第1个),front初始也在head。front先走k步,到达正数第k+1个节点(front现在距null还有n - (k+1) + 1 = n - k步)。然后front和back每次各走一步,当front走到null时,一共又走了n - k步。back从head出发走n - k步,到达正数第1 + (n - k)个节点,即倒数第n - (n - k + 1) + 1 = k个节点。推导成立。

如果你嫌数学烦,记住操作口诀:front先跑k步,然后两个指针手拉手一起跑,front到终点时back就是答案。但面试时最好能把推导说一遍,这是加分项。

3.2 代码实现(C++和Python两个版本)

C++版本:

struct ListNode { int val; ListNode *next; ListNode(int x) : val(x), next(nullptr) {} }; ListNode* findKthFromEnd(ListNode* head, int k) { if (head == nullptr || k <= 0) { return nullptr; } ListNode* front = head; ListNode* back = head; // front 先走 k 步 for (int i = 0; i < k; ++i) { if (front == nullptr) { // k 大于链表长度 return nullptr; } front = front->next; } // 同步移动 while (front != nullptr) { front = front->next; back = back->next; } return back; }

Python版本:

class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def find_kth_from_end(head: ListNode, k: int) -> ListNode: if not head or k <= 0: return None front = head back = head # front 先走k步 for _ in range(k): if front is None: return None front = front.next # 同步走 while front is not None: front = front.next back = back.next return back

有同学会疑惑:为什么front先走k步,下一步先走到None,然后while条件就断了?因为while判断的是front不为空才继续走。如果链表长度正好等于k,front先走k步后会到达null,此时while直接不进入,back原地不动,返回head。检验一下:链表长度5,k等于5,倒数第5个就是第1个(head),正确。

k等于1时,front先走1步,之后front和back同步一格一格挪,直到front走到null,back正好停在最后一个节点上,也就是倒数第1个。检验:链表 1->2->3,front先走到2(第2个节点),然后同步:front到3,back到2;front到null,back到3。返回3,正确。

3.3 为什么这个解法更高明:思维层面的价值

两次遍历是“先获取长度再定位”,双指针是“用相对距离直接逼近目标”。后者避免了对链表长度的依赖——在某些场景里,你根本不知道也不能提前遍历整个链表(例如流式数据),双指针就变成了一种“在线算法”,边读边维护窗口。这也是为什么面试官对双指针解法会眼前一亮:你体现了抽象思维——把“长度”这个全局信息,转化成了“间距”这个局部信息。

再加上空间复杂度还是O(1),只用了两个指针。这个解法能覆盖的题目变体也不少,包括找单链表的中间节点(快慢指针速度比为2:1)、判断链表是否有环、以及删除倒数第k个节点。懂一道相当于懂了一类题。

4. 边界条件与异常处理:面试翻车重灾区

4.1 空链表

head为nullptr。什么都不用想,直接返回nullptr。有些面试官会要求抛异常,但大多数情况下返回空指针/空引用就行。我见过有人在这种输入上报错崩溃,当场印象分就没了。

4.2 k <= 0

题目一般说k是正整数,但你不确定就默认防护。k等于0时,“倒数第0个节点”本身没有定义,返回nullptr是最稳的。k是负数同理。别小看这一行,防御性编程的习惯就是从这里体现出来的。

4.3 k 大于链表长度

这是最隐蔽的坑。链表长度只有3,k给5,你front先走5步,第4步就踩到nullptr,再一访问next就会崩溃。所以我代码里在for循环内判断了front是否为空,为空直接返回nullptr。
面试时最好主动跟面试官说一句“这里我按照约定返回空指针,因为倒数第k个节点不存在”,既阐明了行为,又显得你考虑周全。

4.4 k 等于链表长度

前面验证过,此时返回头节点。这也算一个边界测试点,写单元测试时务必覆盖。

4.5 链表只有一个节点

k等于1的情况要返回该节点本身;k大于1则返回空。这一点代码里的判断自然就能覆盖,但有些人可能因为初始化指针的位置不对,导致返回了null。

4.6 边界问题速查表

场景期望行为常见坑
head为空返回空指针直接崩溃
k <= 0返回空指针未处理
k > 链表长度返回空指针访问空指针的next
k == 链表长度返回head无
k == 1返回尾节点无
链表单个节点,k==1返回该节点初始化指针位置不对

边界问题处理好了,哪怕你的主逻辑写得一般,面试官至少不会觉得你基本功差。实际编码中,边界问题占的代码量可能比主逻辑还多,这本身就是工程常态。

5. 代码落地与测试用例设计:从“能跑”到“靠谱”

5.1 测试驱动:先想清楚怎么验证

很多同学刷题时只写函数,不写测试,导致自己代码对不对全靠“感觉”。面试手写代码时,考官通常不会让你真的跑测试,但你心里要有测试样例。就这道题而言,建议准备以下这么几组用例:

  • 普通链表:1 -> 2 -> 3 -> 4 -> 5,k=2,期望返回节点4。
  • 边界k=5:同上链表,期望返回节点1。
  • 边界k=1:期望返回节点5。
  • 链长为1:head指向一个节点,k=1,期望返回该节点。
  • 非法输入:head为空,或者k=0,或者k=6,期望返回空。
  • 两个节点的链表:1 -> 2,k=2,期望返回头节点1;k=1,期望返回尾节点2。

如果是在本地IDE里自己验证,可以写一个简单的辅助函数用数组创建链表,再遍历打印结果。但面试时写辅助函数太耗时,你只需要在讲代码的时候口头说“这里如果链表是x,返回应该是y”就可以了。重点是让面试官知道你有意识地在做用例设计,而不是碰运气。

5.2 一个容易写错的细节:front先走的步数

有的版本会写成“front先走k-1步再来同步”,这是因为目标节点定义成“倒数第k个节点,包括最后一个节点在内往前数,第一个是最后一个节点”。我上面用的版本是front先走k步,然后同步走到front为null。两种写法都对,但你要确定自己没有把两种情况混在一起写——最常见的问题就是先走步数和同步移动的逻辑在边界上差了一个位置,导致结果错位。

我的习惯是记死一种:“front先走k步,然后front和back同步,front到null时,back指向结果”。这样不易混淆。

5.3 编码风格的加分项

写代码时,命名一定要清楚。front表示前面的指针,back表示后面的指针,这比a、b两个字母好一百万倍。面试官看代码时,会根据变量名判断你的工程素养。循环里能用for就用for,因为循环次数明确;同步阶段可以用while,因为结束条件是front为null。如果代码缩进混乱、变量名随手写、空行乱加,哪怕逻辑对,面试官也很难给你高分。

一个额外建议:写完代码后,自己带一组数据在脑中逐行走一遍,比如head -> 1 -> 2 -> 3 -> 4 -> 5,k=3。front走3步到节点4;同步阶段front走到5,back走到2;front再走到null,back走到3。返回3,即倒数第3个节点(从尾往前数:5是倒数第1,4是倒数第2,3是倒数第3),正确。这种“心中调试”在面试现场特别加分——比写完就停、等面试官发现问题强太多了。

6. 面试现场的表达技巧与变式拓展:让面试官记住你

6.1 表达思路要有“时间轴”

面试官问完题,你先别急着写代码。正确节奏是:

  • 先确认需求(节点返回还是值返回、k是否为正整数)。
  • 说明暴力思路和时间复杂度。
  • 询问/直接提出优化思路,说明双指针的核心思想。
  • 再动手写代码。
  • 写完主动说“我来检查一下边界条件”。

这个顺序表明你的思考方式是从需求到方案到实现到验证,这正是工程研发的标准流程。很多候选人上来就写,快是快,但面试官无法看到你的思维过程,等于白写。

说双指针思路的时候,我习惯用一句话比喻:“快指针先跑出k步的距离,然后两个人保持这个距离一起走,快指针到终点时,慢指针正好在倒数第k个位置。”这种表述很容易让面试官听懂你的意图,而且显得你表达能力强。

6.2 可能的面试追问

面试官不会只满足于一道主解,他会变着花样追问,大部分追问都在这几类里:

  • “如果链表特别长,双指针的空间优势体现在哪?”——答:不需要存整个链表或额外栈,O(1)空间即可。
  • “如果这是个双向链表呢?”——答:那就简单了,直接尾部往前回退k-1步,但也要处理越界。
  • “如果只能访问一次,且链表不能重新遍历呢?”——答:双指针天然适合,因为数据来了就能维护间距。
  • “如果改成删除倒数第k个节点呢?”——答:那个题要找到倒数第k+1个节点,然后改变next指向,需要增加一个哑节点处理删除头节点的情况。
  • “如果找链表中间节点呢?”——答:快慢指针,快指针每次走两步,慢指针每次走一步,快指针到尾时,慢指针在中间。

既然能追问这么多,说明这道题本身就是“一鱼多吃”的好题。你在准备这道题时,顺手把找中间节点、判断环形链表、删除倒数第N个节点一起过一遍,花半天时间就能把链表双指针题型串起来。之后再遇到任何双指针变体,脑子里就有了一张网,而不是一个个孤立的题。

6.3 现场手写时的三个小动作

  • 先写成员变量还是先写方法?我一般先写节点结构定义,再写核心方法。
  • 写完一处return后马上检查会不会漏掉其他路径。比如front走k步中途发现null返回null;主逻辑返回back;还要确保函数签名返回的是节点而不是值。
  • 字迹或排版要能让自己读下去。面试现场压力大,如果代码写到一半自己都找不到变量定义在哪,心态很容易崩。

7. 常见问题与避坑速查:那些你容易“莫名其妙”挂掉的点

7.1 为什么我返回的是“倒数第k+1个”节点

这是最普遍的问题,多半是因为front先走了k-1步。前面说了,两种约定差一步,你只要对照“间距k-1个next”这个标准来检查即可。建议写完后拿一个长度为5的链表,k=2,心算一遍结果。

心算过程:链表[1,2,3,4,5],k=2,期望节点4。如果front先走1步到节点2,back在节点1,同步走:front到3,back到2;front到4,back到3;front到5,back到4;front到null,back到5。返回5,不对。也就是说front先走k-1步+同步,back会在front到null时指向倒数第k-1个节点?也不是,重新推导:back到节点5,即倒数第1,而不是倒数第2。这说明front先走k-1步的情况下,back落在倒数第k-1个节点?这里很容易绕晕。所以更简单可靠的办法是死记“front先走k步,front到null,back是答案”。别临场推导,容易乱。

我修正一下上面的推演:front先走k-1步,间距是k-2,所以back会落在倒数第k-1个节点的位置,差一位。这就是为什么大家总差一个位置的原因——初始间距必须等于目标位置离结尾的步数,即k-1个next,换算成“先走k步”而不是“先走k-1步”。记住这个结论就行。

7.2 为什么有人说“快指针先走k步”,有人又说“先走k-1步”

因为链表是否带头节点、节点的下标定义(从0还是从1)、while循环的结束条件不同,会影响步数。你不需要彻底搞明白所有写法,选定一套笔记并固化下来就够了。

7.3 手写代码时忘了处理k大于链表长度

在本地IDE里,C++和Java直接崩溃,Python会报AttributeError,都很丢脸。我建议把“循环里判空”这个习惯练成肌肉记忆。不只是这题,链表题里只要涉及多次next,就要警惕next是否为null。

7.4 把返回节点误写成返回值

有次模拟面试我见过候选人返回了back->val,题目问“返回倒数第k个节点”,他返回了节点的值。面试官追问他才意识到。虽然不少平台判题时比对的是值,但工程上返回节点意味着你可以继续操作这个节点,语义完全不同。开写之前确认需求,不是多余动作。

7.5 双指针同步移动时不小心先移动了back

有人会把两行顺序写反:back = back->next; front = front->next;。单看逻辑,这两个指针没有依赖关系,顺序互换其实不影响结果。但如果中间加了某些判断或提前终止逻辑,就可能有影响了。保持代码里先front后back的顺序,读起来和推导过程一致,减少思维负担。

8. 扩展思考:双指针思想的迁移应用

链表上的双指针核心就一句话:用两个指针的相对位置关系,代替对“长度/位置”的预先计算。这个思想延伸到其他数据结构:

  • 数组上的左右双指针,可以用来解决“有序数组两数之和”、“反转数组”、“移动零”等问题。这里的指针间距不再是关键,而是相向逼近。
  • 字符串上的快慢指针可以判断回文串,快指针遍历,慢指针记录半边,后半段镜像比较。
  • 滑动窗口本质上也是双指针的一种,窗口左右边界就是两个指针,控制扩张与收缩。

所以这道“面试题.返回倒数第k个节点”别看它短,它是一整类算法的入口。你把它当成一个“题根”去学,收获远超一道题本身。

最后说点个人经验。我带过的实习生里,有人基础不错但每次链表题手写总是从边界开始出问题,原因就是心里没有一个固定的“链表演练场”。我建议你把这题的链表结构、三种边界、双指针步数口诀,花一个晚上写到自己的笔记里,第二天再默写一遍代码。默写不出来的地方,就是你理解还不到位的地方,回去翻推导,不要直接看答案。这道题你吃透了,链表类题目里至少有一半你都有了抓手。以后面试再遇到它,你不会慌,而是能在纸上边画图边把思路讲清楚——那才是面试官真正想看到的“会做”和“懂做”的区别。

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

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

立即咨询