【力扣-24. 两两交换链表中的节点 ✨】Python笔记

0 阅读7分钟

链表中的“舞伴交换”:两两交换链表中的节点

摘要:本文详解LeetCode 24题“两两交换链表中的节点”。通过“虚拟头节点”与“三指针移动法”,在不修改节点值的前提下,仅通过调整指针指向实现节点两两交换,助你掌握链表操作的精妙逻辑。


📚 核心知识点:指针重连的艺术

在处理链表节点交换问题时,我们最大的挑战在于:链表是单向的,一旦改变了指向,后面的节点可能会“丢失”

为了优雅地解决这个问题,我们需要两个核心技巧:

  1. 虚拟头节点(Dummy Node)

    • 由于头节点 head 可能会被交换(变成第二个节点),我们需要一个永远不动的“哨兵” dummy 指向头部,最后返回 dummy.next 即可。
    • 它还能统一操作逻辑,避免对第一个节点进行特殊处理。
  2. 三指针/临时变量法

    • 交换两个相邻节点(比如 1->2 变成 2->1),我们需要操作三个位置的指针:

      1. 前驱节点(prev) :指向交换对之前的那个节点。
      2. 节点1(first) :交换对的第一个。
      3. 节点2(second) :交换对的第二个。
    • 为了防止链表断开,我们需要借助临时变量保存后续节点。


📝 题目解析:LeetCode 24. 两两交换链表中的节点

题目描述
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(只能进行节点交换)。

示例
输入:head = [1,2,3,4]
输出:[2,1,4,3]


💡 解题思路:断链重连四步走

想象一下,我们要把 1 -> 2 -> 3 变成 2 -> 1 -> 3
我们需要一个指针 prev 站在 1 的前面(假设是 dummy)。

交换步骤图解

  1. 定位prev 指向 dummyfirst 指向 1second 指向 2

  2. 第一步(断后路)prev.next 指向 second(2)。

    • 链表变成:dummy -> 2,后面断了。
  3. 第二步(存后路) :我们需要记住 2 后面的 3,所以用临时变量 temp = second.next

  4. 第三步(内部反转)second.next 指向 first(1)。

    • 链表变成:2 -> 1
  5. 第四步(接后路)first.next 指向 temp(3)。

    • 链表变成:dummy -> 2 -> 1 -> 3
  6. 移动指针prev 移动到 first(也就是现在的 1),准备处理下一对。


💻 代码实现(Python)

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next

class Solution:
    def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
        # 1. 创建虚拟头节点,防止头节点被交换后丢失,也方便统一逻辑
        dummy = ListNode(0, head)
        # prev 指针始终指向“待交换对”的前一个节点
        prev = dummy

        # 2. 循环条件:必须保证后面至少有两个节点才能交换
        # 如果 prev.next 为空,说明链表结束了
        # 如果 prev.next.next 为空,说明只剩一个节点,无法交换
        while prev.next and prev.next.next:
            # 标记待交换的两个节点
            first = prev.next       # 节点 1
            second = prev.next.next # 节点 2

            # --- 核心交换逻辑 (4步变3步写法) ---

            # 第一步:prev 指向 second (2)
            prev.next = second

            # 第二步:first (1) 指向 second 的后面 (3),注意这里要用 temp 或者 second.next
            # 这里为了代码简洁,直接利用 second.next 指向 first
            first.next = second.next

            # 第三步:second (2) 指向 first (1)
            second.next = first

            # --- 交换完成,移动 prev 指针 ---
            # prev 移动到这一对的末尾,也就是 first (1)
            prev = first

        # 返回真正的头节点
        return dummy.next

📊 复杂度分析

维度分析
时间复杂度O(N),其中 N 是链表的节点数量。我们只需要遍历一次链表。
空间复杂度O(1),只需要常数空间存储若干变量(dummy, prev, first, second)。

📌 迭代法总结

这道题是链表操作的基本功

  1. 不要只交换值:题目明确要求交换节点,面试时如果直接交换 val 会被扣分。
  2. 画图是关键:在纸上画出指针指向的变化,避免逻辑混乱。
  3. Dummy Node 永远的神:只要涉及头节点可能变动的操作,加上 dummy 能让代码健壮性提升一个档次。

递归写法:换个维度的“乾坤大挪移”

摘要:承接迭代法,继续详解 LeetCode 24 题的递归解法。通过“宏观语义”思维,将复杂链表拆解为“处理当前一对 + 递归处理剩余部分”,代码量大幅减少,助你掌握递归在链表中的优雅应用。


🤔 为什么要用递归?

刚才我们讲的“迭代法”(也就是画图一步步移动指针),虽然逻辑直观,但代码写起来确实有点繁琐,需要小心处理 prevnext 等各种指针的指向,稍不留神链表就断了。

这时候,递归就登场了。递归的魅力在于**“宏观语义”**——你不需要关心具体的每一步怎么走,只需要告诉计算机:

  1. 这一层我要做什么(把前两个节点交换)。
  2. 剩下的事交给谁做(交给下一层递归)。

🚀 递归解题思路:大事化小

核心逻辑
假设我们要交换 1 -> 2 -> 3 -> 4
我们可以把这个问题拆解为:

  1. 交换前两个节点:把 12 交换,变成 2 -> 1
  2. 处理剩下的节点:剩下的 3 -> 4 怎么处理?其实和上面是一样的逻辑!我们只需要把 1(现在的尾节点)指向“处理完剩下的链表后的新头节点”。

递归三部曲

  1. 终止条件(Base Case)

    • 如果链表为空(head == null),没法交换,直接返回 null
    • 如果链表只有一个节点(head.next == null),也没法交换,直接返回 head
  2. 当前层逻辑

    • 定义 firstNode 为当前头节点(比如 1)。
    • 定义 secondNode 为当前第二个节点(比如 2)。
    • 我们要让 secondNode 指向 firstNode,即 2 -> 1
  3. 递归调用(Drill Down)

    • firstNode 的后面应该接什么呢?应该接“从 secondNode.next 开始的链表交换后的结果”。
    • 即:firstNode.next = swapPairs(secondNode.next)

💻 代码实现(Python 递归版)

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next

class Solution:
    def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
        # 1. 终止条件:如果链表为空或只有一个节点,不需要交换,直接返回
        if not head or not head.next:
            return head
        
        # 2. 定义当前要交换的两个节点
        first_node = head          # 第一个节点 (例如 1)
        second_node = head.next    # 第二个节点 (例如 2)
        
        # 3. 递归调用:
        # 让第一个节点指向“剩余链表交换后的新头节点”
        # 比如:1 指向 swapPairs(3->4) 的结果
        first_node.next = self.swapPairs(second_node.next)
        
        # 4. 完成当前层的交换:
        # 让第二个节点指向第一个节点 (2 -> 1)
        second_node.next = first_node
        
        # 5. 返回新的头节点
        # 交换后,第二个节点变成了头节点,所以返回 second_node
        return second_node

📊 图解递归过程

假设输入 1 -> 2 -> 3 -> 4

  1. 第一层递归 (1, 2)

    • first 是 1,second 是 2。
    • 执行 1.next = swapPairs(3->4)。此时程序暂停,进入下一层。
  2. 第二层递归 (3, 4)

    • first 是 3,second 是 4。
    • 执行 3.next = swapPairs(null)。此时程序暂停,进入下一层。
  3. 第三层递归 (null)

    • 触发终止条件,返回 null
  4. 回溯(归的过程)

    • 回到第二层:3.next = null4.next = 3。返回节点 4(此时链表片段为 4->3)。
    • 回到第一层:1.next = 4(接上了刚才返回的片段),2.next = 1。返回节点 2

最终结果2 -> 1 -> 4 -> 3


📌 迭代 vs 递归:哪种更好?

维度迭代法 (Iterative)递归法 (Recursive)
代码复杂度较高,需要维护多个指针 (prev, curr)极低,逻辑清晰,代码简洁
空间复杂度O(1),只需要常数空间O(N),递归调用栈会占用空间
理解难度容易模拟,适合画图理解需要抽象思维,理解“宏观语义”
适用场景链表很长时推荐使用链表较短,追求代码优雅时使用

爱摸鱼的打工仔建议
面试时,建议先写出迭代法(展示你对指针操作的扎实功底),然后告诉面试官:“其实这道题用递归写会更简洁,我可以演示一下吗?” 这样能展示你思维的多维性,绝对是加分项!