从零开始写算法——链表篇:相交链表 + 反转链表

在数据结构与算法的世界里,链表(Linked List)往往是我们遇到的第一个"非线性"挑战。不同于数组的连续内存存储,链表依靠指针串联起零散的内存块。这种特性决定了链表操作的核心在于:如何优雅且安全地控制指针

今天我们通过两道经典的 LeetCode 题目------"相交链表"和"反转链表",来深入探讨双指针技巧背后的数学原理与状态流转。

第一题:相交链表 (Intersection of Two Linked Lists)

题目背景

给你两个单链表的头节点 headAheadB,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null

底层原理解析:消除"时差"的数学魔法

这道题最直观的解法是用哈希表(HashSet)存储链表 A 的所有节点,然后遍历链表 B 查看是否存在相同节点。但这需要 O(N) 的空间复杂度。如果题目要求 O(1) 空间复杂度,我们就必须利用逻辑上的技巧。

为什么双指针走完 A 再走 B 就能相遇?

假设链表 A 的非公共部分长度为 a,链表 B 的非公共部分长度为 b,它们公共部分的长度为 c

  • 链表 A 的总长度 = a + c

  • 链表 B 的总长度 = b + c

如果是两个人在跑步,一个人跑完 A 还需要跑 B,另一个人跑完 B 还需要跑 A:

  1. 指针 p 的路径 :先走 A,再走 B。总路程 = (a + c) + b

  2. 指针 q 的路径 :先走 B,再走 A。总路程 = (b + c) + a

根据加法交换律:a + c + b = b + c + a

这意味着什么? 这意味着,只要两个指针按照这个规则走,它们走过的总路程一定是相等的

  • 如果两个链表相交(c > 0):它们会在走完 a+b 步后,同时到达公共部分的起点(也就是剩下的 c 部分的开始)。

  • 如果两个链表不相交(c = 0):它们会同时走完 a+b 步,然后同时指向 nullptr(也就是链表的尽头)。

这个算法的本质,就是通过拼接路径,消除了两个链表长度不同带来的"时差",强行让两个指针在终点(或交点)同步。

代码实现

C++代码实现:

cpp 复制代码
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        // 定义两个漫游指针
        ListNode* p = headA;
        ListNode* q = headB;
        
        // 当 p 和 q 没有相遇时,继续循环
        // 如果相交,会在交点相遇;如果不相交,会在 nullptr 相遇(此时 p==q==null)
        while(p != q) {
            // 走完自己的路,就去走别人的路
            // 这种三元运算符的写法非常凝练
            p = p ? p->next : headB;
            q = q ? q->next : headA;
        }
        return q;
    }
};

时空复杂度分析

  • 时间复杂度 :O(M + N)。其中 M 和 N 分别是两个链表的长度。最坏情况下,两个指针都需要遍历完两个链表(即走过 M+N 个节点)才能相遇或确认不相交。

  • 空间复杂度 :O(1)。我们只使用了 pq 两个指针变量,没有申请额外的存储空间。


第二题:反转链表 (Reverse Linked List)

题目背景

给你单链表的头节点 head,请你反转链表,并返回反转后的链表。

底层原理解析:三指针的"状态流转"

反转链表看起来简单,但在写代码时很容易出现"断链"的情况(即丢失了下一个节点的地址)。要解决这个问题,我们需要理解这是一个局部状态不断推进的过程。

在反转的过程中,我们需要维护三个视角(变量):

  1. 过去 (pre):已经反转好的链表的头部。

  2. 现在 (cur):当前正在处理的节点,我要把它的指针指向"过去"。

  3. 未来 (nxt):当前节点的下一个节点。在我改变"现在"的指向前,必须先记下"未来"在哪里,否则链条就断了。

操作核心步骤(循环不变量): 每一次循环,我们实际上是把 cur 这个节点,从"未反转"的队伍里拆下来,拼接到"已反转"的队伍头上去。

  • nxt = cur->next:先拿个小本本记下下一家是谁(保存未来)。

  • cur->next = pre关键一步,斩断与未来的联系,回首指向过去(反转指向)。

  • pre = cur:更新"过去"的定义,当前的节点变成了新的表头(推进状态)。

  • cur = nxt:脚踏实地,走向原本的下一家(继续处理)。

代码实现

C++代码实现:

cpp 复制代码
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        // cur 代表当前待处理节点
        // pre 代表已经反转好的链表的头节点(初始化为 null)
        ListNode* cur = head;
        ListNode* pre = nullptr;
        
        while (cur) {
            // 1. 暂存未来:记录下一个节点,防止断链
            ListNode* nxt = cur->next;
            
            // 2. 回首过去:修改指针指向
            cur->next = pre;
            
            // 3. 推进状态:pre 和 cur 整体向前移动
            pre = cur;
            cur = nxt;
        }
        
        // 循环结束时,cur 指向 null,pre 指向原链表的最后一个节点
        // 也就是新链表的头节点
        return pre;
    }
};

时空复杂度分析

  • 时间复杂度:O(N)。其中 N 是链表的长度。我们需要遍历链表中的每一个节点一次,进行指针修改操作。

  • 空间复杂度 :O(1)。这是迭代法的优势,我们只使用了 cur, pre, nxt 三个指针变量。如果使用递归法,虽然代码更短,但会消耗 O(N) 的栈空间。


总结

这两道题目虽然简单,但蕴含了链表操作的两个核心哲学:

  1. 视角的转换:在"相交链表"中,我们通过拼接路径,将"不同步"的问题转化为了"同步"问题。

  2. 状态的维护 :在"反转链表"中,我们通过 precurnxt 三个指针,严密地维护了链表在断裂与重组过程中的状态完整性。

相关推荐
DuHz7 分钟前
242-267 GHz双基地超外差雷达系统:面向精密太赫兹传感与成像的65nm CMOS实现——论文阅读
论文阅读·物联网·算法·信息与通信·毫米波雷达
报错小能手24 分钟前
数据结构 字典树
开发语言·数据结构
XLYcmy33 分钟前
高级密码生成器程序详解:专门设计用于生成基于用户个人信息的密码猜测组合
开发语言·数据结构·python·网络安全·数据安全·源代码·口令安全
AI科技星40 分钟前
时空的固有脉动:波动方程 ∇²L = (1/c²) ∂²L/∂t² 的第一性原理推导、诠释与验证
数据结构·人工智能·算法·机器学习·重构
2401_841495641 小时前
【LeetCode刷题】寻找重复数
数据结构·python·算法·leetcode·链表·数组·重复数
罗技1231 小时前
Easysearch 集群监控实战(下):线程池、索引、查询、段合并性能指标详解
前端·javascript·算法
一路往蓝-Anbo1 小时前
C语言从句柄到对象 (七) —— 给对象加把锁:RTOS 环境下的并发安全
java·c语言·开发语言·stm32·单片机·嵌入式硬件·算法
中國龍在廣州1 小时前
谈谈2025年人工智能现状及发展趋势分析
人工智能·深度学习·算法·自然语言处理·chatgpt·机器人·机器人学习
Joe_Blue_022 小时前
Matlab入门案例介绍—常用的运算符及优先级
开发语言·数据结构·matlab·matlab基础入门案例介绍
C雨后彩虹2 小时前
二维伞的雨滴效应
java·数据结构·算法·华为·面试