ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

链表判环算法深解:快慢指针原理、证明与工程实践

链表判环算法深解:快慢指针原理、证明与工程实践 1. 从一次面试翻车说起为什么简单的题也会栽这事儿得从两年前说起。当时我去面一家中厂前几轮都顺风顺水到了技术终面面试官笑眯眯地在白板上写了一道题给定一个链表判断是否有环。我一看EasyLeetCode 141 原题。脑子里的思路瞬间冒出来快慢指针嘛一个走一步一个走两步遇上就是有环。于是我三下五除二写完边界条件也处理了head为空、只有一个节点的情况都覆盖了。面试官点了点头又问了一句那你能证明一下为什么快指针走两步、慢指针走一步它们一定会在环里相遇吗我愣住了。我背过答案但我从来没想过证明这个。我当时的表情大概就像考试前只背了题目答案、却对定理推导一无所知的学生一样。面试官没有刁难我他耐心地把证明讲了一遍我听得懂但那种被问到底层就露馅的感觉至今记忆犹新。后来我把这道题彻底吃透了才发现它远不是背个快慢指针模板那么简单。这篇文章想把我重新学习 Linked List Cycle Detection 的过程完整记录下来包括几种解法之间的取舍、快慢指针能成立的数学原理、实际操作中容易踩的坑以及从这题延伸出去的一堆变体。如果你也像我一样会做但说不清为什么这篇文章应该能帮你把那层窗户纸捅破。先说结论链表判环这道题考察的不是你能不能背出 Floyds Cycle Detection 算法而是你有没有真正理解为什么它能用。理解了这个面试时不管怎么追问你都能接住理解不了换一个稍微偏门的问法你就会被打回原形。2. 三种主流解法从暴力到优雅的递进2.1 哈希表法最直觉但不够高级先看最简单、最不用动脑子的思路。遍历链表每经过一个节点就把它的引用存进一个 Set 里。如果发现某个节点已经存在了说明回到了之前访问过的节点那必然有环如果遍历到了nullptr说明链表到头了没环。bool hasCycle(ListNode *head) { std::unordered_setListNode* visited; while (head ! nullptr) { if (visited.count(head)) { return true; } visited.insert(head); head head-next; } return false; }这个方案的时间复杂度是 O(n)空间复杂度是 O(n)。逻辑上没有任何问题而且特别容易理解——我走过的地方做了记号走回记号处就是见鬼了。但面试官让你做这题通常不会满足于 O(n) 空间。你注意看题目描述LeetCode 141 下面通常有一行小字Could you use O(1) memory?这就是在暗示你别用哈希表。不是哈希表不对而是这道题的考点在更巧妙的地方。哈希表法的价值在于它给了我们一个正确性基准。后面无论用什么优化方案都可以拿它的结果做对照验证。我自己在写测试脚本时经常先用哈希表版本跑一遍确定答案再用快慢指针版本去比对省得 Debug 半天发现是测试数据出了问题。2.2 破坏链表法想一想就好千万别在生产代码里用还有一个思路比较野既然判断有没有环那我把每个访问过的节点的next指针指回一个固定的哑节点不就行了如果又碰到了那个哑节点说明回到了被标记过的路径上。ListNode* dummy new ListNode(0); while (head ! nullptr) { if (head-next dummy) { return true; } ListNode* next head-next; head-next dummy; head next; } return false;说实话这个思路很聪明我第一次看到时甚至觉得有点惊艳——它同样只用 O(1) 空间而且比快慢指针还好理解。但它有一个致命问题它破坏了原始链表结构。在现实工程里链表数据往往是有主、需要复用的你跑一次判断链表废了这就是妥妥的事故。不过这个思路让我想到一个很重要的点很多巧妙解法的本质往往是我们是否允许修改原始数据。允许修改你能玩出花来不允许修改你就得靠更聪明的游走策略。这也是为什么面试官通常会加一句不能修改链表结构——加了这句话快慢指针基本就成了唯一候选。2.3 快慢指针法两头跑迟早会碰头终于到主角了。快慢指针Floyds Cycle Detection的关键就一句话让两个指针从head出发慢指针每次走一步快指针每次走两步。如果链表有环快指针终会在环里追上慢指针如果没环快指针会先到达链表末尾跳出。bool hasCycle(ListNode *head) { if (head nullptr || head-next nullptr) { return false; } ListNode *slow head; ListNode *fast head-next; // 常见变体先让 fast 领先一步 while (slow ! fast) { if (fast nullptr || fast-next nullptr) { return false; } slow slow-next; fast fast-next-next; } return true; }注意我故意把fast初始化成了head-next而不是head。这两种写法在 LeetCode 上都能过但细节上有些微妙差别后面初始化陷阱那一节我会专门展开。这段代码看起来简单但它的正确性远不是多跑几步总能追上这么一句能糊弄过去的。我当时被面试官问为什么整个脑袋是空的下面这一节就是我当时缺的那块拼图。3. 快慢指针为什么一定相遇把证明掰开揉碎3.1 关键前提快指针先进环慢指针后进环要证明快慢指针一定相遇第一步是明确它们的位置关系。假设链表的直线段长度是 L也就是从head到环入口的距离环的长度是 C。慢指针走了 L 步后第一次踏进环口。此时快指针已经走了 2L 步。由于快指针每个周期比慢指针多走一步它在环里已经比慢指针多跑了 L 步的超额距离。由于 L 不一定小于 C快指针可能已经在环里绕了好几圈了。但它一定在环里这一点是确定的。这个先后进环的顺序非常重要。它保证了追及问题发生在环内而不是在直线的某个位置——因为在直线上快指针永远追不到慢指针因为它们的起点相同快指针只是无意义地重复走。3.2 追及的本质相对速度是 1接下来是核心。慢指针在环内以速度 1 前进快指针以速度 2 前进。如果以慢指针为参照物快指针相对慢指针的速度是 1——也就是说快指针每走两步相对慢指针就走近一步。用追及问题的语言来说当慢指针刚进环时快指针已经处在环的某个位置 P。快指针要在环内追上慢指针需要追赶的距离是从 P 到慢指针当前所在位置也就是环入口的弧长。我们把这个距离记作 D。那么追及需要的时间就是 D / 1 D 步。由于 D 最大不会超过 C - 1同一时刻两指针不重合如果 D 0 那就已经相遇了所以最坏情况下慢指针在环内走 C - 1 步之内快指针一定能追上它。这也是为什么用快慢指针不需要担心会不会永远追不上——因为相对速度是常数 1追及时间只取决于初始间距 D而 D 是有限且小于环长的。3.3 一个直观的类比操场套圈如果你觉得上面的公式太干可以换个生活化的类比。想象学校操场的环形跑道你和同学从同一扇门进去你走得慢速度 1同学跑得快速度 2。你先在门口等着同学从门口先出发跑。当你踏入跑道时同学可能已经在你前面绕了大半圈。但因为你们速度不同你在环形跑道上相对转圈来看同学每分钟比你多跑一段距离。他每跑过一个完整的周长就会比原来更逼近你一些——套圈就是这么发生的。区别只在于这道题里套圈发生在环内部快指针追上慢指针时就是有环的直接证据。我想强调一遍快指针不是走一圈正好碰到慢指针而是多走的路程差填平了它们之间的初始间距。只要相对速度为正追上是必然如果快指针速度和慢指针相同那就永远不会相遇。这也是环内驱逐巡航问题的最关键区别。3.4 反直觉结论快指针走几步不是关键关键是不能和慢指针同速这里有个很反直觉的结论快指针不一定要走两步走三步、走四步理论上都能追到吗答案是不保证。走三步时相对速度是 2追及一定能进行。但是步长带来的一个额外问题是快指针可能跳过慢指针所在的节点吗在走两步的情况下快指针每步跨两个节点在环内它的轨迹和慢指针的轨迹只在特定的奇偶性条件下重叠。如果环的长度是偶数、而初始间距 D 是奇数那么快指针和慢指针会不会交错而过而永远不相遇这里需要小心。走两步的经典场景下数学上可以证明不会出现永远交错的情况因为快指针的轨迹是连续的偶/奇节点切换但最终总会在那个环内某一节点碰头。严格证明可以借助模运算令环长为 C慢指针位置为 i快指针位置为 2i dd 是初始差距两者相同的条件是 i ≡ 2i d (mod C)即 i ≡ -d (mod C)。因为 d 是固定常数这个同余方程一定有解所以相遇必然发生。走三步的情况方程变成 i ≡ 3i d (mod C)即 2i ≡ -d (mod C)当 C 为偶数且 d 为奇数时无解——所以真的可能永远碰不上。这个推导是我吃了大亏才弄明白的。我当时的想法是快指针走快一点不就能更快追上吗——完全不是这么回事。面试题里设定走两步不是随便选的是经过数学验证的最稳选择。4. 求环入口的位置141 题背后的隐藏考点4.1 为什么追上了还不够还得算出入口LeetCode 141 只要求返回 bool但面试官几乎一定会追加一道变体找到环的入口节点对应 LeetCode 142。如果你只背了判断有没有环的模板这一问就直接卡死。我第一次遇到 142 时真的懵了。我当时的困惑是都找到相遇点了可这个点不在环入口啊怎么办正确答案是一个优雅的数学推导而它恰好解释了为什么快慢指针方案如此强大。4.2 核心等式从 head 到入口等于相遇点到入口再加若干整环设链表的直线段长度为 L环入口为 E环长为 C。当慢指针和快指针在环内某点 M 相遇时设 M 到环入口 E 的距离按前进方向为 X。慢指针走过的总距离L mC X其中 m 是慢指针进环后绕的圈数通常是 0如果环很长的话。 快指针走过的总距离L nC X其中 n 是快指针绕的圈数。由于快指针比慢指针多走一倍的距离有L nC X 2 * (L mC X)化简一下L (n - 2m)*C - X注意 (n - 2m)C 是环长的整数倍记作 kC。于是 L k*C - X。这意味着从 head 走到环入口的距离 L等于从相遇点 M 继续往前走到环入口的距离C - X加上 k 个整圈。换句话说如果让一个新的指针从 head 出发同时让那个相遇点的指针继续以相同速度前进它们会在环入口处相遇。因为前者走了 L后者走了 (C - X) (k-1)*C L。这是一条真正通向入口的路径。4.3 代码实现同步走的双指针具体实现不复杂ListNode *detectCycle(ListNode *head) { ListNode *slow head, *fast head; // 第一阶段找到相遇点 while (fast fast-next) { slow slow-next; fast fast-next-next; if (slow fast) { break; } } if (!fast || !fast-next) { return nullptr; } // 第二阶段从头出发和从相遇点出发速度相同相遇处即入口 slow head; while (slow ! fast) { slow slow-next; fast fast-next; } return slow; }这个代码的巧妙之处在于第一阶段结束后fast停在相遇点第二阶段把slow重置回head然后两边都以步长 1 前进。由于等式保证了它们会在入口对齐所以循环退出时两个指针指向的就是入口。我有一个自己验证过的小技巧怕出错的话可以把第二阶段看成两个人在跑同一条环形跑道一个人从跑道外入口处开始跑另一个人在环里某处开始回头跑他们会在入口碰头。虽然这个类比不太严格但用来记忆代码顺序足够了。4.4 计算过程示例手工跑一遍链表为了彻底搞懂我曾经自己构造了一个链表1 - 2 - 3 - 4 - 5 - 6 - 3也就是 3、4、5、6 形成环入口是 3直线部分长度 L21 和 2环长 C4。慢指针走了 2 步到 3此时快指针在 5。追及过程慢指针 4、快指针 3慢指针 5、快指针 5。它们在 5 相遇。X 就是从 5 到环入口 3 的距离按前进方向5 - 6 - 3所以 X 2。用公式验证L k*C - X取 k1则 L 4 - 2 2正好等于 2。第二阶段从 head 出发的指针走 2 步到 3从相遇点 5 出发的指针走 2 步也是到 3。二者在 3 相遇得到入口。这就是为什么求入口的代码正确性有数学背书而不是某种魔法。我当时亲手算了三组不同参数的链表全部能对上之后才敢在面试里自信地说出接下来我们来找入口。5. 实战中的坑初始化、快指针边界和其他脏事5.1 快慢指针初始化的不同洗法网上关于快慢指针的代码五花八门有的fast head有的fast head-next有的一开始就判断head-next有的把判空放在循环里。这些写法各有各的适用场景但它们之间微妙的差异我以前完全没注意。关键区别在于执行流程fast head第一次循环里slow走一步到head-nextfast走两步到head-next-next。这种写法适合 while 循环条件为slow ! fast的情况且初始时二者相等所以必须用 do-while 或者先移动再比较。fast head-next第一次循环前slow和fast已经不同可以直接进入 while 条件判断。但这种写法在第一阶段结束后相遇点和对齐公式中的起点定义会有细微偏移。实际写代码的时候我建议认准一种风格并理解它的行为而不是随手抄。我最常用的是fast head加上 do-while 风格ListNode *slow head, *fast head; do { if (!fast || !fast-next) return false; slow slow-next; fast fast-next-next; } while (slow ! fast); return true;这种写法在第一步就处理了空指针逻辑上走到哪查到哪我自己调试的时候更好跟踪。5.2 环的特殊成员一个节点自环一个最容易让人着道的 case 是head指向的节点只有一个且它的next指向自己。这种情况在题例里偶尔出现在现实工程里也可能发生——数据异常导致自环。fast head的写法第一次迭代时fast-next存在指向自己所以fast走到自己slow也走到自己二者相等返回 true没问题。fast head-next的写法如果head就一个节点那么head-next nullptr你会直接走进判空分支返回 false——这是错的。当时我第一次用这种写法时就在这个 case 上错了。我后来专门写了个测试单节点自环、双节点互指环、直线末尾接环全跑一遍才放心。5.3 链表判空与单节点无环并列处理还有一个容易忽略的边界空链表和单节点无环链表。前者head nullptr后者head-next nullptr。这两个 case 都不能返回有环。我在写代码时养成了一个习惯先把这两种情况单独拎出来再进入主循环这样逻辑更清晰也省得在主循环里反复加判空导致代码支离破碎。if (head nullptr || head-next nullptr) { return false; }这句话几乎可以出现在任何判环代码开头而且面试时先写它也向面试官传递了我考虑过边界条件的信号。5.4 快指针的 next 为空检测位置另一种经典的 bug 来源是快指针走到链表末尾你试图访问fast-next-next但fast-next可能已经是nullptr这时候直接对nullptr取next会导致崩溃。所以每次需要跳两步之前必须先确认fast-next不为空。我把这个检测放在循环体开头while (fast ! nullptr fast-next ! nullptr) { // 在这里安全地移动快指针 }这种写法我强烈推荐因为它把能不能走两步的判断集中在一个条件里不会漏。如果你把判断分散在两个地方很容易在某个分支里忘记检查。6. 从判环到更多变体这道题能扯出来的内容远比想象多6.1 变体一求环节的长度既然能找入口自然也能求环的长度。思路很简单找到相遇点后让一个指针停在原地另一个指针以步长 1 在环里绕圈数它走回出发点需要多少步。因为你在环里一定能走回来。代码大致长这样ListNode *meet ...; // 相遇点 ListNode *cur meet-next; int length 1; while (cur ! meet) { cur cur-next; length; }这个变体的价值在于它比直接写在纸上求环长更能考人对环内游走的理解。我遇到过面试官在面完 141 和 142 后随手抛出这个问题的——很多候选人前面答得很好但到这里会突然短路。6.2 变体二链表交点的变种还有一个更广义的扩展如果两个链表可能相交如何找到第一个公共节点经典解法是把两个链表首尾相接制造出一个环然后用上面的找环入口方法求解。LeetCode 160 就是这道题。这种把新问题转化成旧问题的思路是我刷题过程中最大的收获之一。你不需要背太多题如果你把判环的根本逻辑吃透面对交点问题时只需要一步化归就能把新问题变成老问题。我第一次自己想出这个转化时那个成就感远超背会十道题。6.3 变体三有环情况下的链表倒数第 k 个节点链表有环后很多常规操作的性质都变了。比如找倒数第 k 个节点如果链表有环那么倒数第 k 个没有明确定义。但如果你先求环入口和环长就可以把链表的线性部分和环路部分分开处理。这类题目在现实里面试官不一定会问但它展示了判环算法的工具箱属性——你不只是在做一道题而是在掌握一套可以组合使用的基础能力。我后来在写一些自定义数据结构时就用过类似的技巧来判断配置依赖图是否成环否则死锁排查会非常痛苦。6.4 工程案例配置依赖图的死锁检测说到工程案例我想分享一个自己实际遇到的问题。当时我在维护一个插件系统每个插件可以依赖其他插件形成一个依赖图。我需要确保用户不会配制出循环依赖否则启动时插件会无限循环加载。这个场景和链表判环思想一致不过节点从单链表节点变成了可以有多个出边的图节点。当时的解决方案是做一个 DFS 版本的三色标记法白未访问、灰访问中、黑访问完成如果在 DFS 过程中又碰到灰节点就说明有环。但我的第一反应其实是把它简化成每个节点只有一个依赖的特例用快慢指针思路写了个粗糙版本——因为插件依赖通常是单依赖所以我把它当成链表处理没问题。后来遇到多依赖插件才升级成三色标记。这让我体会到链表的快慢指针虽然只能解决单链结构但它的思想双指针、游走、追及是可以迁移到图论的。如果你理解了这套思想你在应对更复杂问题时会有一种武器库里有不止一件工具的底气。7. 测试是检验理解的唯一标准我如何验证自己的方案7.1 构造测试链表最简单但最可靠的方法纸上推演再清楚不跑代码心里还是不踏实。我写了一套简单的测试框架用来构造各种形式的链表并验证判环算法。核心是写个函数把数组转成链表ListNode* buildLinkedList(const std::vectorint vals, int cyclePos) { if (vals.empty()) return nullptr; ListNode* head new ListNode(vals[0]); ListNode* cur head; std::vectorListNode* nodes; nodes.push_back(cur); for (int i 1; i vals.size(); i) { cur-next new ListNode(vals[i]); cur cur-next; nodes.push_back(cur); } if (cyclePos 0) { cur-next nodes[cyclePos]; } return head; }cyclePos -1时表示无环否则就是环入口在数组中的下标。这套工具帮我跑了大量测试用例包括前面说的单节点自环、双节点互指环等极端情况。7.2 暴力对照把哈希表结果当作标准答案我一开始心里有个疑问我的快慢指针代码如果写错了我怎么知道它错了测试链表是我自己构造的我算得出正确答案。但如果节点很多、环很长手算实在太慢。所以我用哈希表版本当参照实现让它输出正确答案再和快慢指针比对。bool hasCycleHash(ListNode* head) { /* 用 unordered_set 实现 */ } bool hasCycleFloyd(ListNode* head) { /* 用快慢指针实现 */ } for (int n 1; n 10; n) { for (int pos -1; pos n; pos) { auto* list buildLinkedList(std::vectorint(n, 0), pos); assert(hasCycleHash(list) hasCycleFloyd(list)); } }这段小脚本我把节点的数量和环的位置都遍历了一遍从 1 个节点到 10 个节点全部匹配。这种差分测试的方法后来被我用到很多算法题里算是意外收获。7.3 用随机大链表压测确认不会死循环边界都测过之后我又担心一个问题万一代码在某种情况下死循环测试会卡住。所以我写了一个随机测试每次生成 1000 个节点、随机决定是否有环、随机决定环的位置然后跑判环函数限制 1 秒内必须返回结果。因为主循环里快指针每一步都走两步慢指针走一步它们进环后追及时间有上限理论上复杂度是 O(n)。实测下来也是毫秒级返回没有出现死循环。如果你担心自己的代码死循环可以用同样方法压一下如果卡住多半是快指针边界条件写错了。8. 面试时如何应对追问从背答案到真正会讲8.1 面试官最爱问的 5 个追问这道题在面试中出现的频率非常高所以我把常见追问整理了一个清单方便自己复盘追问考察点为什么快慢指针一定会相遇是否理解相对速度和环内追及空间复杂度能不能做到 O(1)是否知道哈希表的劣势怎么找环的入口是否能推导 L kC - X环的长度怎么求是否掌握相遇点之后的游走如果快指针走三步还能行吗是否理解步长奇偶对相遇性的影响前三问几乎是必问第四问看情况第五问是压轴加分题。我第一次准备时对第五问完全没概念后来推导明白了才敢说走三步不一定行走两步一定行。8.2 讲题的正确姿势先讲朴素解法再优化面试时不要一上来就写快慢指针。我现在的策略是先说暴力解法——用哈希表记录访问过的节点空间 O(n)然后主动提出我能不能优化到 O(1) 空间再引出快慢指针。这样既展示了思维的递进过程也自然而然把面试官的注意力带到了你准备好的知识点上。如果直接甩出快慢指针代码面试官很容易追问你怎么想到的为什么不用更简单的方法。换成先朴素、后优化的叙述节奏面试官通常会顺着你的思路走追问也在你准备过的范围内。8.3 主动证明不要等对方问如果说我那次翻车教会了我什么那就是主动证明正确性。在写完代码后主动说一句让我花 30 秒解释为什么这个算法一定能终止并得出正确结果然后用相对速度的追及逻辑讲一遍。面试官一般会点头认可心里给你加分。这比被动等追问要主动得多也更能展现你是在做工程而非背题库。我在后续的面试里每次讲解这道题都用这个策略效果稳定。有一位面试官甚至听完后说大多数人来都是写代码然后等我问你是第一个自己把证明讲了的人。后来那个 offer 给了虽然不全因为这道题但我确信这道关没拖后腿。9. 重新回看那次翻车我学到的做题方法论9.1 会写代码不代表会讲道理那次面试对我最大的冲击是让我意识到我刷题的方式有问题。我把大量时间花在了看过答案就以为自己会了上忽略了底层推理。代码可以背但面试不是考试面试官更看重你对一个问题理解的深度。从那以后我给自己定了个规矩每道题刷完之后必须能口述出三个问题——为什么这个解法有效、边界条件在哪里、如果参数变了还成立吗。这一套三问反思法效果出奇地好不仅帮我巩固了对题目的理解还让我形成了一整套知识网络。9.2 构建知识网络从一道题到一簇题当我深入研究链表的环检测时发现它和许多看似不相关的问题都有联系。比如求链表中点快慢指针快指针到末尾时慢指针在中点。判断回文链表先找中点再反转后半段逐一比较。相交链表把尾接到头转成环入口问题。环形数组数组索引跳转判断是否有重复访问。这些题的核心都是游走双指针。你掌握的是一类指针游戏的规则而不是一道题的死模板。面试时哪怕遇到新题只要它符合这种结构你也能识别出来。9.3 不只是面试算法思维在工程中的迁移我曾经觉得刷题和工作没关系直到做了插件系统那次。你会发现很多判断会不会死循环判断依赖是否有环检测一个流程是否会无限迭代的问题本质都是在某种图结构上做游走与状态判断。链表的判环是最简单的那个原型理解了它再往图上走就不那么害怕了。比如在一个配置解析器里我曾经遇到过一条规则可以间接引用自身的情况。当时我第一反应就是用三色标记法检查依赖图而不是去写一个脆弱的深度计数器。那道解法直接照搬了链表的标记已访问节点思想只不过从 Set 升级成了颜色数组。这种迁移能力就是靠大量为什么的积累沉淀出来的。10. 关于这道题我最后想多说的三件事10.1 别跳过证明哪怕你已是老手我知道有些读完这篇文章的朋友会觉得这题太简单不需要看证明。但我想说我最初也这样想直到被问到当场卡壳。证明不仅能帮你应付面试追问更重要的是它能让你在代码出错时快速定位原因。如果你只看结果不看过程一旦遇到题目变形你连为什么走不通都说不清。我现在的习惯是每道算法题至少推一遍核心不变式每次推完都对这道题多一分成体系的理解。判环的追及方程是我推得最熟的几个之一因为它的结论可以延伸出环入口、环长等一堆衍生结论性价比极高。10.2 动手写一套自己的测试用例我强烈建议你别只把 LeetCode 的提交通过当终点。把那几种边界 case——空链表、单节点自环、双节点互指、长直线末尾接环、入口在 head——全部亲手构造一遍跑通一遍。这种自己在家里搭测试台的经验比刷十道题更锻炼工程手感。我在日常工作中发现很多 bug 不是算法逻辑不对而是边界条件没覆盖。判环这种看似简单的题恰是训练边界意识的最好素材。你把它的边界吃透了以后写链表相关代码都会更细心。10.3 把一道题变成一类题最后一点想说的是不要停在这道题本身。花半小时想想如果链表变成有向图怎么判环如果快指针走三步怎么调整证明如果可以修改链表结构但要求恢复原状怎么办这些延伸问题每一个都能帮你把这道题的价值放大数倍。我的做法是每道题建立一个小笔记把变体、证明、测试策略都记下来日后复习的时候效率极高。Linked List Cycle Detection 这道题表面上是 LeetCode 的 Easy实际是一颗能长出很多知识分支的种子。从那次面试翻车到现在我每次遇到两个指针在环里赛跑的问题都会想起那天白板前的沉默。那段沉默让我学会了不背答案不轻看简单题也让我养成了先证明后编码再测试的做题习惯。希望这篇包含了我个人踩坑经历的复盘也能让你在下次面对这道题时不只有答案更有底气和思路。
返回列表