🏠 总目录📚 本教程 01 · 链表:指针到底在保住什么 ← →
📑 本页目录(点开跳转)

01 · 链表:指针到底在保住什么

⏱ 12 分钟 | 反转、合并、环入口:全是“先别把下一站弄丢”


🎯 一句话

链表题的核心不是指针语法,而是:改 next 之前,先把旧的下一站存下来。

🧩 题 1:反转链表

题目。 1 → 2 → 3 变成 3 → 2 → 1。

先画。 prev 是已经翻好的部分;cur 是还没处理的第一个节点;nxt 临时保住后路。

不变量。 每轮结束后,prev 指向已反转的前缀,cur 指向尚未处理的后缀。

class ListNode:
    def __init__(self, val, next=None):
        self.val, self.next = val, next


def reverse_list(head):
    prev, cur = None, head
    while cur:
        nxt = cur.next          # 先保住后路
        cur.next = prev         # 再掉头
        prev, cur = cur, nxt    # 两队伍一起向前
    return prev


def values(head):
    out = []
    while head:
        out.append(head.val)
        head = head.next
    return out


head = ListNode(1, ListNode(2, ListNode(3)))
assert values(reverse_list(head)) == [3, 2, 1]

⚠️ 最常见的错:先写 cur.next = prev,再想找原来的 cur.next。这时后路已经被覆盖,节点就丢了。

🧩 题 2:合并两个有序链表

题目。 把 1 → 3 → 5 和 2 → 4 合成 1 → 2 → 3 → 4 → 5。

不变量。 tail 后面永远是已经排好序的结果;a、b 各自指向还没被拿走的最小节点。

class ListNode:
    def __init__(self, val, next=None):
        self.val, self.next = val, next


def merge_sorted(a, b):
    dummy = ListNode(0)
    tail = dummy
    while a and b:
        if a.val <= b.val:
            tail.next, a = a, a.next
        else:
            tail.next, b = b, b.next
        tail = tail.next
    tail.next = a or b
    return dummy.next


def values(head):
    out = []
    while head:
        out.append(head.val)
        head = head.next
    return out


a = ListNode(1, ListNode(3, ListNode(5)))
b = ListNode(2, ListNode(4))
assert values(merge_sorted(a, b)) == [1, 2, 3, 4, 5]

💡 dummy 只是一个假头:这样第一个节点也和后面的节点一样接,不必写“第一次特殊处理”。

🧩 题 3:链表中环的入口

题目。 链表可能绕回去。返回绕回的第一个节点;没有环则返回 None。

先想两段路。 快慢指针相遇说明有环。相遇后,让一个指针回头;两者都每次走一步,再次相遇的地方就是入口。

class ListNode:
    def __init__(self, val, next=None):
        self.val, self.next = val, next


def cycle_entry(head):
    slow = fast = head
    while fast and fast.next:
        slow, fast = slow.next, fast.next.next
        if slow is fast:
            break
    else:
        return None

    seeker = head
    while seeker is not slow:
        seeker, slow = seeker.next, slow.next
    return seeker


n1, n2, n3, n4 = ListNode(1), ListNode(2), ListNode(3), ListNode(4)
n1.next, n2.next, n3.next, n4.next = n2, n3, n4, n2
assert cycle_entry(n1) is n2

这里不需要背距离公式。先记住使用条件:一个快、一个慢能确认“是否绕圈”;确认后,同速从头和相遇点出发能找到入口。


🔗 这一章连到哪里

去哪 为什么
Python 会咬你的地方 01 · 名字、对象和绑定 ⭐ 「先保住后路再掉头」为什么必须这么写:Python 里赋值从不复制对象,只是把名字重新贴一次。prev, cur = cur, nxt 这种多重赋值的求值顺序也在那一章

🛑 可以停在这里

链表所有“改指针”的题,先问:我改掉这条线之前,下一站放哪了?


✅ 检查点

反转链表时为什么一定先保存 nxt?

👀 看答案

因为下一步会覆盖 cur.next。不先保存,原来尚未处理的后缀就再也找不到了。


⚡ 走神救援

继续: 02 · 树:递归到底在替你保存什么

打卡记录保存在你的浏览器里,首页能看到总进度