news 2026/9/26 12:36:18

C语言链表操作精讲:从相交链表到双指针的O(1)解法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C语言链表操作精讲:从相交链表到双指针的O(1)解法
## 1. 这道题为什么值得写:面试高频与链表操作的试金石 相交链表(Intersection of Two Linked Lists)在 LeetCode 上是编号 160 的经典题,在《剑指 Offer》里对应第 52 题。我见过不少面试官拿它当热身题,也见过它作为二面手写题的升级版出现——比如要求不用哈希表、空间复杂度压到 O(1)。这道题表面上看只是"找两个链表有没有交点",实际上考察的是**链表指针操作的基本功、边界条件的敏感度、以及用数学思维化简问题的能力**。 先说清楚题目本身,用一句人话概括就是:给你两个单链表的头节点 headA 和 headB,判断这两个链表是否在某个节点处汇合,如果汇合,返回那个相交节点的指针;如果不相交,返回 NULL。注意一个关键前提——**两个链表一旦相交,从相交节点开始,后面的所有节点都是共享的**,因为它们都是单链表,每个节点只有一个 next 指针,不可能出现相交之后又分叉的情况。这一点是整个题目所有解法成立的基础。 链表的相交结构长什么样?我画个文字版的示意图,方便你对着理解:

A: a1 -> a2
c1 -> c2 -> c3 / B: b1 -> b2 -> b3

在这个结构里,A 链表从 a1 出发,走到 a2 之后进入 c1;B 链表从 b1 出发,经过 b1、b2、b3 之后也进入 c1。c1 就是相交节点,c1、c2、c3 是两个链表共享的部分。注意,A 的长度是 4(a1、a2、c1、c2、c3一共5个?我数错了,重来:a1、a2、c1、c2、c3是5个节点),B 的长度是 5(b1、b2、b3、c1、c2、c3),但公共部分只有 3 个节点。 为什么说这道题是"链表操作的试金石"?因为链表是 C 语言里最讲究"指针的语义"的数据结构。写数组题,你操作的是一段连续内存、下标直接可算;写链表题,你手里只有一个指向头节点的指针,所有后续节点都要靠 next 一步步走。相交链表这个题目涉及的指针移动、长度差计算、循环退出条件,稍有疏忽就会踩空指针的坑。而且 C 语言没有现成的链表库,一切都要自己定义结构体、自己管理内存,这对指针理解的考察比其他语言更深入。 这篇博文适合正在刷题的在校生、准备面试的求职者,也适合工作中偶尔需要手写数据结构的 C 语言开发者。我会从最朴素的做法开始,逐步讲到最优解,再附上我实际调试过程中踩过的坑和排查经验。 ## 2. 先搞定最容易想到的方案:哈希表与暴力双循环 ### 2.1 哈希表方案:思路最直,但空间换时间 最简单的思路是这样:既然相交链表从交点开始共享节点,那我只要把一个链表的所有节点地址记下来,然后遍历另一个链表,逐个检查当前节点的地址是否在刚才的记录里。如果在,那这个节点就是交点;如果走完都没找到,说明两个链表不相交。 C 语言实现这个方案,可以用哈希表,也可以用"借用数组当标记"的办法——但注意,这里存的是**指针地址**,不是整数值,所以直接用数组下标做哈希的话,你得把指针转换为整数再哈希。我这里的示例代码用了一个简单的哈希结构,你也可以直接用带哨兵的双重循环先跑通功能。 ```c #include <stdio.h> #include <stdlib.h> struct ListNode { int val; struct ListNode *next; }; #define HASH_SIZE 1024 struct HashNode { struct ListNode *addr; struct ListNode *next; // 链地址法解决冲突 }; struct HashNode *hash_table[HASH_SIZE]; unsigned int hash_ptr(struct ListNode *p) { return ((unsigned long)p >> 4) % HASH_SIZE; } void hash_insert(struct ListNode *p) { unsigned int idx = hash_ptr(p); struct HashNode *node = (struct HashNode *)malloc(sizeof(struct HashNode)); node->addr = p; node->next = hash_table[idx]; hash_table[idx] = node; } int hash_find(struct ListNode *p) { unsigned int idx = hash_ptr(p); struct HashNode *cur = hash_table[idx]; while (cur) { if (cur->addr == p) return 1; cur = cur->next; } return 0; } struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) { for (int i = 0; i < HASH_SIZE; i++) hash_table[i] = NULL; // 每次调用前清空 struct ListNode *p = headA; while (p) { hash_insert(p); p = p->next; } p = headB; while (p) { if (hash_find(p)) return p; p = p->next; } return NULL; }

这个方案的时间复杂度是 O(m + n),其中 m 和 n 分别是两个链表的长度,因为遍历一次 A 链表做插入,遍历一次 B 链表做查找。空间复杂度是 O(m),因为需要把 A 链表的所有节点地址存下来。

这个方案在面试时可以作为"我第一时间想到的解法"说出来,但通常面试官会接着问一句:"能不能把空间复杂度降到 O(1)?"这就是下面两个方案的出场时机了。

2.2 暴力双循环:最笨但最容易验证思路

如果不想引入哈希表,另一个直观方案是双循环:对 A 链表的每个节点,都完整遍历一遍 B 链表,检查有没有指针相等的节点。伪代码如下:

struct ListNode *pA = headA; while (pA) { struct ListNode *pB = headB; while (pB) { if (pA == pB) return pA; pB = pB->next; } pA = pA->next; } return NULL;

这个写法最直白,时间复杂度是 O(m * n),空间复杂度是 O(1)。它的优点是不容易写错,尤其适合你在本地快速验证"两个链表到底相不相交、交点在哪"这个事实。但它的缺点很明显,一旦链表长度上万,性能就会急剧下降。

我个人的建议是:如果你在面试中先说哈希表方案,再优化到双指针方案,完全没有必要再提暴力双循环。但如果你是在自己学习调试阶段,暴力双循环是很好的辅助验证工具——你可以用它跑一遍测试用例,确认自己的预期结果,再去改写成更优的方案。

2.3 为什么哈希表方案在 C 语言里要特别小心

这里必须提醒一个 C 语言特有的坑:哈希表的键是指针值,而不是节点里的 val。我见过不少人刚上手这道题时,想用节点值 val 作为比较依据,结果两个链表里有两个值相同的节点,但它们的地址完全不同,就被误判成相交了。链表相交判断的本质是判断节点是否同一个,不是判断值是否相等。

另外,哈希表方案里如果你用(unsigned long)p >> 4做哈希,是把指针右移 4 位,因为典型的链表节点通过 malloc 分配时,地址通常按 16 字节对齐,低位基本为 0,右移后哈希分布更均匀。这个技巧在嵌入式或者教学场景下能提升一点性能,但不必过度优化。真正在工程里,我更倾向于直接用双指针方案,省去所有哈希表内存管理的麻烦。

3. 高效解法一:长度差法,先对齐再同步走

3.1 核心思想:把"长度差"这个变量消掉

哈希表和暴力法的本质问题在于:两个链表长度不等时,你没法简单地让两个指针"同步"往前走。但如果我们换个角度看:如果两个链表相交,那么从交点到尾部的公共部分长度是相同的。因此,两个链表的长度差,必然完全来自交点之前的"独有部分"。

这么说可能有点绕,我举一个例子。假设 A 链表长度为 7,B 链表长度为 5,交点位于 A 的第 3 个节点、B 的第 1 个节点。那么:

  • A 的独有部分长度 = 3 - 1 = 2(a1、a2)
  • B 的独有部分长度 = 1 - 1 = 0
  • 公共部分长度 = 5 - 1 + 1?不对,我重新算一下。

公共部分是从交点开始到尾部,这个长度对两个链表是一样的。如果 A 总长 7,交点前有 2 个独有节点,那交点后的公共长度就是 7 - 2 - 1(交点头本身)?其实不用这么纠结,关键结论只有一个:两个链表的长度差,就等于两个指针各自走到交点所需步数的差。如果能让两个指针从"距离交点相同步数"的位置出发,那么它们同步前进,必然会在交点相遇。

长度差法的具体操作分三步:

  1. 分别遍历两个链表,得到长度 lenA 和 lenB。
  2. 让较长的链表先走abs(lenA - lenB)步,这样两个指针就处在"距离链表末尾相同距离"的位置。
  3. 然后两个指针同步前进,每走一步比较一次,相等则返回该节点;走到 NULL 都没相遇,说明不相交。

3.2 C 语言完整实现与测试

直接上代码:

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) { int lenA = 0, lenB = 0; struct ListNode *pA = headA, *pB = headB; // 第一遍:统计两个链表的长度 while (pA) { lenA++; pA = pA->next; } while (pB) { lenB++; pB = pB->next; } // 重新指向头节点 pA = headA; pB = headB; // 让较长的链表先走长度差 int diff = lenA - lenB; if (diff > 0) { while (diff--) pA = pA->next; } else { diff = -diff; while (diff--) pB = pB->next; } // 同步前进,比较指针地址 while (pA && pB) { if (pA == pB) return pA; pA = pA->next; pB = pB->next; } return NULL; }

我在本地跑了几组测试:

第一组,两个链表不相交:

headA: 1 -> 2 -> 3 headB: 4 -> 5 -> 6 期望结果:NULL

运行过程:lenA=3,lenB=3,长度差为 0,两个指针从头同步走,走到 NULL 都没有相等,返回 NULL。正确。

第二组,两个链表相交:

headA: 1 -> 2 -> 3 -> 4 -> 5 headB: 6 -> 3 -> 4 -> 5 (交点值 3,假设地址相同) 期望结果:返回值为 3 的节点指针

运行过程:lenA=5,lenB=4,diff=1,pA 先走一步,来到指向 2 的节点。此时 pA 距离交点(节点 3)还有 1 步,pB 距离交点(节点 6 的下一个,也就是节点 3)也是 1 步。然后同步走,pA 到节点 3,pB 也到节点 3,地址相等,返回。正确。

3.3 这个方案的边界条件与常见错误

写这个代码时,最容易犯的错误有三个,我一个个说。

第一个错误:统计长度后忘记把指针重置回头节点。这几乎是每个初学者都会踩的坑。两个 while 循环走完之后,pA 和 pB 都已经指向 NULL 了,如果不重新赋值pA = headA; pB = headB;,后面的逻辑全部白写。

第二个错误:处理长度差时符号搞反。如果 lenA > lenB,你让 pA 先走 diff 步;反之让 pB 走。这个 if-else 分支本身不难,但如果你写到一半改动了变量的含义,很容易把diff = -diff的逻辑丢掉,导致指针越界。我建议先写一个int diff = lenA - lenB;然后统一处理,比如:

if (diff < 0) { while (diff++) pB = pB->next; } else { while (diff--) pA = pA->next; }

这里注意,diff为负时,diff++是将负数逐步增加直到 0,效果正好是让 pB 先走-diff步。这个写法在 C 语言里完全合法,但读代码的人需要想一下,注释写清楚比较好。

第三个错误:同步前进时只判断 val 相等而不是地址相等。我在前面已经强调过了,pA->val == pB->val只能说明两个节点的值相同,不能说明它们相交。必须判断pA == pB,这是指针比较,比较的才是地址。

长度差法的时间复杂度是 O(m + n),空间复杂度 O(1),已经能满足绝大多数面试要求了。但 LeetCode 的官方题解还给出了更巧妙的双指针法——两个指针走"对方的路",连长度差都不用先算。这就是下一节的内容。

4. 高效解法二:双指针交替法,最优雅的 O(1) 方案

4.1 为什么两个指针走完各自的路再走对方的路就能相遇

这个解法的代码极短,但理解起来需要绕一个弯。我先给出代码,再解释原理。

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) { if (!headA || !headB) return NULL; struct ListNode *pA = headA; struct ListNode *pB = headB; while (pA != pB) { pA = pA ? pA->next : headB; pB = pB ? pB->next : headA; } return pA; }

就这么几行。我第一次看到这个解法时愣了一下,因为它的逻辑太"绕"了——两个指针都不停地走,走到 NULL 就跳到另一个链表的头部,直到它们相遇。但仔细推演之后发现,这实际上是"长度差法"的另一种表达,而且连"算长度"这个步骤都省了。

假设链表 A 长度为 m,链表 B 长度为 n,交点前的独有部分分别为 a 和 b,公共部分长度为 c。那么:

  • m = a + c
  • n = b + c

当 pA 走完 A 链表(走了 m 步)时,它会跳到 headB 继续走;当 pB 走完 B 链表(走了 n 步)时,它会跳到 headA 继续走。如果两个链表相交,pA 一共走m + b步后,pB 一共走n + a步后,它们都会到达交点。我们来验证这个"步数一致性":

pA 走到交点需要的总步数 = A 链表独有部分 a + 公共部分 c?不对,这里要重新算。pA 从 headA 出发,走到 A 链表的交点需要 a + 1 步?不对,我不用步数精确到个位,只看相对关系。

更简洁的理解方式是这样:pA 总共走过的路径是"A 链表全部 + B 链表交点之前的独有部分",也就是m + b;pB 总共走过的路径是"B 链表全部 + A 链表交点之前的独有部分",也就是n + a。由于:

m + b = (a + c) + b = a + b + c n + a = (b + c) + a = a + b + c

所以当 pA 走完m + b步、pB 走完n + a步时,它们走过的总步数相同,而且此时它们都正好站在交点上。这就是"殊途同归"。

如果两个链表不相交,那么 pA 走完m + n步、pB 也走完n + m步后,它们会同时走到 NULL,即pA == pB == NULL,循环退出,返回 NULL。

4.2 逐步推演:用一个具体例子走一遍

纸上得来终觉浅,我手动推演一遍。

假设 A 链表:a1 -> a2 -> c1 -> c2 -> NULL,B 链表:b1 -> b2 -> b3 -> c1 -> c2 -> NULL。交点 c1。

初始化:pA 指向 a1,pB 指向 b1。

第 1 步循环:pA != pB,pA 指向 a2,pB 指向 b2。 第 2 步循环:pA != pB,pA 指向 c1,pB 指向 b3。 第 3 步循环:pA != pB,pA 指向 c2,pB 指向 c1。 第 4 步循环:pA != pB,pA 向后走,此时 pA 在 c2 的 next,也就是 NULL;pB 从 c1 走到 c2。根据代码逻辑,pA 为 NULL,所以 pA 被赋值为 headB,即 b1;pB 不为 NULL,所以 pB 指向 c2。

第 5 步循环:pA(现在指向 b1)!= pB(指向 c2),pA 走到 b2,pB 走到 NULL,于是 pB 被赋值为 headA,即 a1。 第 6 步循环:pA 指向 b2,pB 指向 a1,不相等,pA 走到 b3,pB 走到 a2。 第 7 步循环:pA 指向 b3,pB 指向 a2,不相等,pA 走到 c1,pB 走到 c1。 第 8 步循环:pA == pB,跳出循环,返回 c1。

从第 1 步到第 8 步,pA 实际遍历了 A 的全部 4 个节点,又遍历了 B 靠近交点之前的独有部分 b1、b2、b3;pB 遍历了 B 的全部 5 个节点,又遍历了 A 靠近交点之前的独有部分 a1、a2。两者最终在 c1 相遇。看起来很神奇,其实就是前面证明的恒等式在起作用。

4.3 这个解法与长度差法的本质联系

很多人会问:双指针交替法和长度差法到底哪个更好?我的答案是,它们在数学上是等价的,只是实现路径不同。

长度差法是显式地计算长度差,让长链表先走几步,消除初始偏移;双指针交替法是隐式地利用"走完自己的路再走别人的路",让两个指针的总路程趋于一致,最终必然在交点汇合。你可以把双指针法理解为"把长度差的处理延迟到了走完一条链表之后"——它同样利用了"两个链表相交则尾部对齐"这个性质。

从代码可读性来说,长度差法更直白,适合新手;双指针交替法更精炼,适合面试现场手写和追求极致简洁的场景。我个人在面试时会先讲长度差法,再主动提出"其实还有更简洁的双指针交替法",这样既展示了基础理解,又展示了优化能力。但在工程代码里,我会用带注释的长度差法,因为几个月后回头维护代码时,双指针那三行要反推一下才能想起来为什么正确。

另外,双指针交替法有一个隐藏的边界情况需要处理:如果 headA 或 headB 本身是 NULL,那么循环会直接走到pA ? pA->next : headB这样的逻辑吗?不会,因为while (pA != pB)在 pA 和 pB 都为 NULL 时才会退出,但如果一个为 NULL 另一个不为 NULL,循环会继续。所以在进入循环前,最好加一个判断:

if (!headA || !headB) return NULL;

这个判断不是必须的——因为如果 headA 为 NULL,pA 为 NULL,pB 指向 headB(如果非空),循环会继续走,最终 pB 走到 NULL 后跳到 headA(也是 NULL),两者相遇退出,返回 NULL。但加上这个判断会让逻辑更清晰,也更安全,避免任何潜在的野指针操作。我建议加上。

5. 代码调试笔记:我在实际运行中遇到的两个典型问题

5.1 问题一:忘记重置指针导致"看似正确的错误结果"

有朋友问我,为什么自己写的长度差法代码在链表不相交时返回了某个节点地址,而不是 NULL。我让他把代码发给我看,结果发现他的代码长这样:

struct ListNode *pA = headA; struct ListNode *pB = headB; int lenA = 0, lenB = 0; while (pA) { lenA++; pA = pA->next; } while (pB) { lenB++; pB = pB->next; } // 然后直接开始用 pA 和 pB 比较,完全忘了把它们重新指向 headA 和 headB

问题就在这:统计完长度后,pA 和 pB 都已经指向 NULL 了。此时再让长的先走、短的同步走,实际上操作的是一堆 NULL 指针,不仅结果错误,还可能在pA->next上触发段错误。这类问题在本地调试时不会立刻崩溃,因为对 NULL 指针执行比较操作在语法上是允许的,但逻辑就完全错了。

我的排查经验是:长度统计完成后,立刻打印 pA 和 pB 的地址,确认它们是 NULL,然后再重新赋值。或者更干脆一点,用一个独立变量统计长度,不要用后续要用的指针变量:

struct ListNode *cur = headA; while (cur) { lenA++; cur = cur->next; } cur = headB; while (cur) { lenB++; cur = cur->next; }

这样 pA 和 pB 从头到尾都保留着初始指向,不会因为统计长度而污染。这个习惯不仅适用于这道题,所有涉及"先遍历一遍统计信息、再从头开始操作"的链表题(比如寻找中间节点、判断回文链表)都适用。

5.2 问题二:在 LeetCode 上跑出 Time Limit Exceeded 的元凶

另一个朋友在 LeetCode 上提交暴力双循环方案,遇到超出时间限制。他的链表长度是 10 万级别的,O(m * n) 的复杂度在极端情况下要跑 100 亿次比较,超时是必然的。这倒不奇怪,奇怪的是他在本地测试时完全正常——因为本地的两个链表长度都只有几十个节点。

这暴露了一个普遍问题:很多初学者在本地测试时喜欢用"两个短链表"来做验证,但这远远不够。链表长度为 1、长度为 0、两个链表完全不相交、两个链表完全重合(一个链表是另一个的子集)、交点恰好在第一个节点或最后一个节点——这些边界情况全部都要测到。

我整理了一个自查用的测试用例清单,你可以直接拿去用:

用例编号headAheadB期望结果
1NULLNULLNULL
21->2->3NULLNULL
31->2->31->2->3(相同地址的头节点)头节点
41->2->34->1->2->3(交点在第2个节点值1处)节点1
51->2->34->5(不相交)NULL
61->2->3->42->3->4(交点在第2个节点值2处)节点2

我自己每次写链表题都会准备这样一个 table 驱动的测试,而在 LeetCode 上则直接构造对应的测试函数:

void test() { // 构造链表节点 struct ListNode a1 = {1, NULL}, a2 = {2, NULL}, c1 = {3, NULL}; a1.next = &a2; a2.next = &c1; struct ListNode b1 = {4, NULL}; b1.next = &c1; struct ListNode *result = getIntersectionNode(&a1, &b1); printf("Expected c1 address: %p\n", (void*)&c1); printf("Actual result: %p\n", (void*)result); }

注意,测试用例里构造链表时,两个链表共用c1节点,这正是相交链表的特征。如果你用malloc分别申请两个独立的节点,哪怕 val 相同,地址也不同,测试结果就会偏离"相交"的定义。

5.3 关于内存管理的一个附加提醒

这道题在 LeetCode 上做的时候,链表节点一般由题目后台分配,你不需要自己释放内存。但如果你在本地把链表题改成"构造链表 -> 判断相交 -> 释放链表"的完整流程,就要格外小心:绝对不能对两个链表分别调用 free 来释放共享节点,否则同一块内存会被释放两次,触发 double free 崩溃。

正确做法是:先构造一个不含共享节点的两个独立链表做测试,或者用一个标志变量记录某个节点是否被释放。我自己在本地调试时,通常直接忽略释放这一步,只关注算法正确性,毕竟题目本身不要求管理内存生命周期。

6. 进阶视角:这道题延伸出来的链表考点

6.1 找到相交节点的变体:如果链表有环怎么办

相交链表的基础版本假设链表都是无环的。但面试官常会加问:如果两个链表可能有环,该怎么判断是否相交?这个变体其实把问题升级成了"判断链表是否有环"(经典快慢指针题)和"找环入口"(Floyd 判圈算法)的组合。

我的思路是这样:先分别检测两个链表是否有环。如果一个有环一个无环,那它们不可能相交;如果两个都有环,相交的情况会更复杂——公共节点可能在环上,也可能在环的入口处。处理方式也比较暴力:找到两个链表的环入口,然后分别走一遍,看环上面的节点是否被共享。这个展开讲能再写一篇长文,这里点到为止,提醒你如果遇到这个追问,核心考点依然是"指针相等即共享节点"这一条。

6.2 从 O(m*n) 到 O(m+n):复杂度分析是面试考察点

面试时,即使你写的是正确的长度差法,也一定会被要求解释时间复杂度和空间复杂度。这里我建议你把推导清晰地讲出来:每个链表最多被完整遍历一遍,所以总时间复杂度是 O(m + n);只用了常数额外空间,所以空间复杂度是 O(1)。如果你写哈希表方案,要主动承认它的空间复杂度是 O(m),从而引出后续优化。

很多候选人会卡在"为什么双指针交替法的空间复杂度是常数"这个问题上——因为代码里只有两个指针变量,没有额外的数组或哈希表,无论链表多长,额外内存都不增长。这个结论本身很简单,但在面试紧张时容易表述不清,建议提前组织好语言。

6.3 类似题目横向对比:环形链表、回文链表、合并有序链表

相交链表不是孤立的题目,它和下面几题共享很多底层技巧:

  • 环形链表(LeetCode 141):用快慢指针判断是否有环,核心是"快指针每次走两步,慢指针每次走一步,如果有环必定相遇"。这和相交链表的双指针交替法一样,都是"利用不同的速度/路线让指针最终汇合"的思想。
  • 回文链表(LeetCode 234):先用快慢指针找到中间节点,再反转后半部分,然后逐节点比较。找中间节点的过程,本质上也是"利用长度差/同步移动"的技巧。
  • 合并两个有序链表(LeetCode 21):这个更多的是考察递归或者迭代式节点拼接,但同样强调对 next 指针的精细控制。

如果你能把这几题放在一起刷,会比孤立刷题效果好得多。因为它们的底层都是一件事:理解链表节点的地址唯一性、next 指针的操作边界、以及双指针在链表上的各种应用模式。

7. 从"会做"到"做对":我的刷题心法总结

写到最后,分享一点我个人的体会。

相交链表这道题,我前前后后教过几十个同学写。最让我印象深刻的不是谁一次就写对,而是大家普遍会在"长度差法"和"双指针交替法"之间选择困难。其实这两者没有优劣之分,关键在于你是否能在 30 秒内讲清楚为什么你的代码是正确的。如果你选择了双指针交替法,却卡在"为什么要跳转到另一个链表头部"这个点上,面试时反而会露怯。

我的建议是:平时练习时两种方法都写一遍,用同一组测试用例验证。这样你不仅掌握了两种实现,还被迫理解了它们的内在一致性。真到了面试或竞赛场上,你就能做到手随心动,选最顺手的那个。

另外还想强调一件事,链表这类题目,"半小时没写出来"是正常的。不要灰心,不要急着看题解。给自己画图、举例子、推演指针走向,这个过程本身就是最大的收获。等你亲手画出那两条指针在链表之间交替跳跃的路线之后,你会发现这道题的美感——它用最简单的代码,体现了链表结构最本质的性质:节点共享、指针即地址、循环即遍历。这种美感,是看多少遍题解都体会不到的。

## 8. 追问与实践:一道题带来的三个自测问题 如果你看完上面这些内容,建议先别急着关掉页面。我出三个小问题,你能不依赖编译器,在心里推演出正确答案,才算真正掌握了这道题。 第一问:在双指针交替法中,如果两个链表长度完全相同且不相交,循环会执行多少次才退出?我的答案是 2n 次(n 为链表长度),因为每个指针要走完自己的 n 个节点,再走完对方的 n 个节点,总共 2n 步后同时到达 NULL。 第二问:在长度差法中,如果两个链表只有一个公共节点,而且这个节点就是 headA 和 headB 本身,会发生什么?答案是 lenA 和 lenB 相等,长度差为 0,两个指针从一开始就相等,直接返回头节点,不需要进入同步循环。 第三问:如果把双指针交替法中的 `pA = pA ? pA->next : headB;` 改成 `pA = pA->next ? pA->next : headB;`,会发生什么?答案是当 pA 指向最后一个非 NULL 节点时,`pA->next` 为 NULL,于是 pA 直接跳到 headB,跳过了 pA 自己的"NULL 状态",这个逻辑仍然能在相交链表的场景下工作,但在不相交的场景下可能提前退出并返回错误结果。这是一道经典的易错变体,建议你亲手推演一遍。 我每次给朋友讲这道题,都会把这三个问题抛出去。前两个问题大多数人都能答对,第三个问题能立刻答对的人不超过十分之一。为什么?因为它考察的不是背代码,而是对"指针在走完一条链表后的状态变化"的完全掌控——这是链表题真正的分水岭。 相交链表只是 C 语言链表大家族里的一个入门关卡。这道题背后藏着指针比较、边界条件、复杂度推导、以及"用数学消除不对称性"的通用思维。把这些吃透了,你再看环形链表、回文链表、合并链表,会突然觉得它们之间有千丝万缕的联系。到那时候,刷题就真的变成了一件有意思的事。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 12:35:55

基于YOLOv8的光伏电池EL图像缺陷检测实战指南

简介&#xff1a;一套基于YOLOv8的光伏电池缺陷检测项目&#xff0c;面向需要掌握目标检测算法落地与工业质检场景的开发者与学习者&#xff0c;覆盖模型训练、推理与部署全流程。项目共收录一千一百一十一个文件&#xff0c;其中包含一百五十九个Python训练/推理脚本、六十八个…

作者头像 李华
网站建设 2026/9/26 12:35:17

测量LMC662的输入电流

测量LMC662的输入偏置电流LMC662 Datasheet AD\Test\2026\September\TestLMC662InCurrent.SchDoc 01 【测量LMC662输入电流】 一、测试电路 这款LMC662功放已经在我的原题库盒子里放了很久&#xff0c; 可能之前购买它&#xff0c;是因为它具有极低的输入偏置电流以及低的失调…

作者头像 李华
网站建设 2026/9/26 12:32:40

2026机械行业标准更新速览:绿色低碳与智能制造全解析

做机械这一行的朋友都知道&#xff0c;每年开春最让人头疼的事儿就是“标准又变了”。图纸上标注的旧标准号还没捂热&#xff0c;新版本就发布了&#xff0c;供应商那边要重新确认&#xff0c;质检那边要更新检验依据&#xff0c;哪怕是写个设备操作规程&#xff0c;也得跟着标…

作者头像 李华
网站建设 2026/9/26 12:32:31

Agent开发者必学的SQL实战笔记:从表设计到工具封装

做 Agent 开发的朋友&#xff0c;十有八九都遇到过这种尴尬&#xff1a;模型对话、工具调用、记忆存储全都跑通了&#xff0c;结果一到需要读写数据库、查个用户信息、存个会话记录的时候&#xff0c;手边连个能用的 SQL 都挤不出来。更常见的是让 Agent 去调一个数据接口&…

作者头像 李华
网站建设 2026/9/26 12:32:19

托盘实例分割数据集实战:从解压到YOLO训练全流程

简介&#xff1a;本资源为托盘实例分割数据集&#xff0c;面向物流自动化、工业机器人视觉集成及制造业质量检测等场景&#xff0c;适合算法工程师与研究人员用于目标检测与实例分割模型的训练验证。数据共676张JPEG图片&#xff0c;按训练、验证、测试集划分&#xff0c;包含p…

作者头像 李华
网站建设 2026/9/26 12:32:05

Linux多核网卡流量分发:RSS/RPS/RFS/XPS协同调优实战

1. 这不是“调优玄学”&#xff0c;而是多核网卡流量分发的底层逻辑 你有没有遇到过这样的情况&#xff1a;一台配置了32核CPU、万兆网卡的Linux服务器&#xff0c;跑着高并发Web服务或实时数据采集&#xff0c;top里看CPU利用率却只有20%&#xff0c;但网络延迟飙升、连接堆积…

作者头像 李华