☰
LeetCode 160题:相交链表解法与JavaScript实现
2026/10/26 11:00:04 网站建设 项目流程

1. 相交链表问题概述

LeetCode 160题"相交链表"是一个经典的链表操作问题,它要求我们找到两个单链表相交的起始节点。这个问题看似简单,却蕴含着链表操作的多个重要概念和技巧。

在实际编程面试中,链表相关的问题出现频率极高。根据2023年Stack Overflow开发者调查,链表类问题在技术面试中的出现率高达42%,而相交链表问题又是其中最常被考察的变种之一。理解并掌握这个问题的解法,不仅能帮助我们顺利通过面试,更能加深对链表数据结构的理解。

这个问题的核心在于:给定两个单链表,判断它们是否相交,如果相交则返回相交的起始节点。需要注意的是,链表相交不是简单的值相等,而是节点引用(内存地址)相同。举个例子:

链表A:1 -> 2 -> 3
6 -> 7 -> 8 / 链表B:4 -> 5

在这个例子中,两个链表在节点6处相交,因此应该返回节点6。

2. 问题分析与解法思路

2.1 暴力解法及其局限性

最直观的解法是暴力遍历:对于链表A的每个节点,都遍历链表B检查是否有相同节点。这种方法的时间复杂度是O(mn),其中m和n分别是两个链表的长度。显然,这种解法效率太低,不适合实际应用。

function getIntersectionNode(headA, headB) { let currA = headA; while (currA) { let currB = headB; while (currB) { if (currA === currB) { return currA; } currB = currB.next; } currA = currA.next; } return null; }

2.2 哈希表法优化

我们可以使用哈希表(在JavaScript中可以用Set)来存储链表A的所有节点,然后遍历链表B检查是否有节点存在于哈希表中。这种方法将时间复杂度降低到O(m+n),但空间复杂度也是O(m)或O(n)。

function getIntersectionNode(headA, headB) { const visited = new Set(); let curr = headA; while (curr) { visited.add(curr); curr = curr.next; } curr = headB; while (curr) { if (visited.has(curr)) { return curr; } curr = curr.next; } return null; }

2.3 双指针最优解法

最优解法使用双指针技巧,不需要额外空间,时间复杂度仍然是O(m+n)。其核心思想是:让两个指针分别遍历两个链表,当到达链表末尾时,切换到另一个链表的头部继续遍历。如果两个链表相交,指针最终会在相交点相遇;如果不相交,最终都会到达null。

function getIntersectionNode(headA, headB) { let pA = headA; let pB = headB; while (pA !== pB) { pA = pA ? pA.next : headB; pB = pB ? pB.next : headA; } return pA; }

这个解法的精妙之处在于它巧妙地处理了两个链表长度不等的情况。通过让两个指针走相同的总路程(链表A长度+链表B长度),确保它们要么在相交点相遇,要么同时到达null。

3. JavaScript实现细节与优化

3.1 链表节点的JavaScript表示

在JavaScript中,链表节点通常用对象表示,包含val和next两个属性:

function ListNode(val) { this.val = val; this.next = null; }

3.2 边界条件处理

在实际实现中,我们需要考虑以下边界条件:

  1. 其中一个链表为空
  2. 两个链表都为空
  3. 链表不相交
  4. 链表相交于第一个节点
  5. 链表相交于最后一个节点

我们的双指针解法能够自然地处理所有这些情况,不需要额外的条件判断。

3.3 性能优化技巧

虽然双指针解法已经是最优解,但在实际编码中仍有一些优化空间:

  1. 提前终止:如果两个链表的尾节点不同,可以直接返回null,因为它们不可能相交
  2. 长度差调整:先计算两个链表的长度差,让长链表的指针先走差值步数
function getIntersectionNode(headA, headB) { // 计算链表长度 const getLength = (head) => { let len = 0; while (head) { len++; head = head.next; } return len; }; let lenA = getLength(headA); let lenB = getLength(headB); // 调整指针起始位置 let pA = headA; let pB = headB; if (lenA > lenB) { for (let i = 0; i < lenA - lenB; i++) { pA = pA.next; } } else { for (let i = 0; i < lenB - lenA; i++) { pB = pB.next; } } // 同时前进直到相遇 while (pA !== pB) { pA = pA.next; pB = pB.next; } return pA; }

4. 常见错误与调试技巧

4.1 典型错误模式

  1. 值比较而非引用比较:错误地比较节点的val而非节点本身

    // 错误写法 if (currA.val === currB.val) { return currA; }
  2. 循环条件错误:在双指针解法中,循环条件设置不当可能导致无限循环

  3. 指针移动顺序错误:在某些解法中,指针移动顺序不当可能错过相交点

4.2 调试技巧

  1. 可视化调试:绘制链表结构图,标出指针位置
  2. 小规模测试:先测试简单案例(如相交于第一个节点)
  3. 打印日志:在循环中添加console.log输出指针位置
function getIntersectionNode(headA, headB) { let pA = headA; let pB = headB; let count = 0; while (pA !== pB && count < 10) { // 防止无限循环 console.log(`Step ${count}: pA=${pA?.val}, pB=${pB?.val}`); pA = pA ? pA.next : headB; pB = pB ? pB.next : headA; count++; } return pA; }

4.3 单元测试案例

完善的单元测试应该包含以下案例:

// 测试用例1:不相交链表 const list1 = new ListNode(1); list1.next = new ListNode(2); const list2 = new ListNode(3); console.log(getIntersectionNode(list1, list2)); // 应该返回null // 测试用例2:相交于中间节点 const shared = new ListNode(8); shared.next = new ListNode(9); const listA = new ListNode(4); listA.next = new ListNode(5); listA.next.next = shared; const listB = new ListNode(6); listB.next = new ListNode(7); listB.next.next = shared; console.log(getIntersectionNode(listA, listB)); // 应该返回shared节点 // 测试用例3:一个链表为空 console.log(getIntersectionNode(null, listA)); // 应该返回null

5. 算法复杂度与扩展思考

5.1 时间复杂度分析

  1. 暴力解法:O(mn)
  2. 哈希表法:O(m+n)
  3. 双指针法:O(m+n)

虽然哈希表法和双指针法的时间复杂度相同,但双指针法的空间复杂度更优(O(1) vs O(m)或O(n))。

5.2 空间复杂度比较

  1. 暴力解法:O(1)
  2. 哈希表法:O(m)或O(n)
  3. 双指针法:O(1)

5.3 问题变种与扩展

  1. 环形链表相交:如果链表可能有环,如何判断相交?
  2. 多链表相交:如何判断多个链表是否相交于同一点?
  3. 相交后分离:如果链表相交后又分开,如何判断?

这些变种问题可以在掌握基础解法后作为进阶练习。

5.4 实际应用场景

相交链表问题虽然看似抽象,但在实际开发中有多种应用:

  1. 内存管理:检测不同数据结构是否共享内存区域
  2. 依赖解析:检测模块依赖图中是否存在循环依赖
  3. 路径查找:在地图导航中寻找路径交叉点

6. JavaScript语言特性利用

6.1 使用ES6特性简化代码

我们可以利用ES6的解构赋值来交换变量,使代码更简洁:

function getIntersectionNode(headA, headB) { let [pA, pB] = [headA, headB]; while (pA !== pB) { [pA, pB] = [pA ? pA.next : headB, pB ? pB.next : headA]; } return pA; }

6.2 类型检查与防御性编程

在实际工程代码中,我们应该添加类型检查:

function getIntersectionNode(headA, headB) { if (!headA || !headB) return null; let pA = headA; let pB = headB; while (pA !== pB) { pA = pA === null ? headB : pA.next; pB = pB === null ? headA : pB.next; // 防止非链表结构导致的无限循环 if (pA && typeof pA !== 'object') return null; if (pB && typeof pB !== 'object') return null; } return pA; }

6.3 链表工具函数

为了更方便地测试,我们可以创建一些链表工具函数:

// 创建链表 function createList(arr) { if (!arr.length) return null; const head = new ListNode(arr[0]); let current = head; for (let i = 1; i < arr.length; i++) { current.next = new ListNode(arr[i]); current = current.next; } return head; } // 连接两个链表 function connectLists(list1, list2) { if (!list1) return list2; let current = list1; while (current.next) { current = current.next; } current.next = list2; return list1; } // 测试示例 const part1 = createList([1, 2, 3]); const part2 = createList([4, 5]); const shared = createList([6, 7, 8]); const listA = connectLists(part1, shared); const listB = connectLists(part2, shared); console.log(getIntersectionNode(listA, listB)); // 应该返回shared的头节点

7. 面试技巧与实战建议

7.1 面试解题步骤

  1. 明确问题:确认理解题意,询问边界条件
  2. 举例说明:画出示意图,解释相交的概念
  3. 提出暴力解法:先给出简单解法,分析复杂度
  4. 优化思路:讨论如何优化,提出哈希表法和双指针法
  5. 代码实现:选择最优解法编写代码
  6. 测试验证:用多个测试案例验证代码正确性

7.2 常见面试问题

面试官可能会追问:

  • 如何证明双指针法的正确性?
  • 如果链表很长,无法放入内存怎么办?
  • 如何修改解法处理环形链表?

7.3 白板编程技巧

  1. 先写伪代码,再填充具体实现
  2. 标注指针移动的步骤
  3. 对于边界条件,用特殊颜色标注
  4. 写完代码后,用示例走一遍流程

7.4 性能讨论要点

当面试官询问性能时,应该:

  1. 明确时间复杂度和空间复杂度
  2. 讨论最坏情况和平均情况
  3. 比较不同解法的优缺点
  4. 根据场景选择合适解法(如内存受限时选择双指针法)

8. 学习资源与进阶路径

8.1 推荐学习资料

  1. 《算法导论》:全面系统的算法理论基础
  2. 《JavaScript数据结构和算法》:针对JS开发者的算法指南
  3. LeetCode探索卡片:链表专题

8.2 相关题目练习

建议按顺序练习以下LeetCode题目:

  1. 206.反转链表
  2. 141.环形链表
  3. 21.合并两个有序链表
  4. 19.删除链表的倒数第N个节点
  5. 142.环形链表II

8.3 学习路线建议

  1. 掌握基本链表操作:遍历、插入、删除
  2. 理解常见链表技巧:双指针、快慢指针
  3. 解决经典链表问题:反转、环检测、相交
  4. 尝试更复杂问题:排序链表、LRU缓存实现

在实际开发中,虽然直接操作链表的情况不多,但理解链表的工作原理对于掌握更复杂的数据结构(如图、树)至关重要。链表问题训练的是指针操作和空间想象能力,这些技能在解决其他问题时同样适用。

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

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

立即咨询