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

在数据结构与算法的世界里,链表(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++代码实现:

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++代码实现:

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 三个指针,严密地维护了链表在断裂与重组过程中的状态完整性。

### 关于链表的基础知识 链表是一种常见的数据结构,由一系列节点组成,每个节点包含两部分:**数据域**和**指针域**。其中,数据域用于存储实际的数据内容,而指针域则保存下一个节点的地址[^4]。 对于初学者来说,在学习链表时可以关注以下几个方面: #### 1. 单链表的基本概念 单链表是最简单的链表形式,其特点是每个节点只有一个指向下一节点的指针。最后一个节点的指针为空(`None`),表示链表结束。 #### 2. 判断两个单链表是否相交并找到第一个交点 要判断两个单链表是否相交,并找出它们的第一个公共节点,可以在不使用额外空间的情况下实现这一目标。具体方法如下: - 计算两条链表的长度差 `d`。 - 将较长的链表先移动 `d` 步,使得两者剩余的部分长度相同。 - 同步遍历两条链表,直到发现相同的节点即为交点[^1]。 以下是 Python 实现代码示例: ```python class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def getIntersectionNode(headA: ListNode, headB: ListNode) -> ListNode: if not headA or not headB: return None pA, pB = headA, headB while pA != pB: pA = pA.next if pA else headB pB = pB.next if pB else headA return pA ``` 此算法的时间复杂度为 \(O(m+n)\),空间复杂度为 \(O(1)\)。 #### 3. 学习资源推荐 如果你希望通过在线课程来深入理解链表的知识,可以选择一些优质的教学平台。例如,“头歌教育”提供了丰富的编程实践项目,适合初学者通过动手操作掌握链表的相关技能。此外,《Python3入门教程》也是一本不错的参考资料,它涵盖了基本的数据结构及其应用[^2]。 如果更倾向于 Java 的视角,则《Java基础小白入门教程》会是一个很好的起点。该系列讲解了从零开始构建程序逻辑的过程,其中包括数组与链表的区别等内容[^3]。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值