Python 算法基础篇之链表

目标:理解链表的内存模型,掌握指针操作技巧,熟练运用虚拟头节点、快慢指针、反转等核心模式解决复杂问题。


一、为什么需要链表?

1.1 数组的结构性缺陷

数组要求内存连续,导致插入和删除的代价与数据规模成正比:

python 复制代码
arr = [1, 2, 3, 4, 5]

# 头部操作:O(n) ------ 所有元素移动
arr.insert(0, 0)   # [0, 1, 2, 3, 4, 5]
arr.pop(0)         # [1, 2, 3, 4, 5]

# 中间操作:O(n) ------ 后半部分元素移动
arr.insert(2, 99)  # [1, 2, 99, 3, 4, 5]

核心矛盾:数组的物理连续性带来了随机访问的优势(O(1) 索引),但也导致了动态修改的劣势。

1.2 链表的解决思路

链表通过指针连接离散节点,将物理连续性要求转化为逻辑连续性:

复制代码
数组(连续内存)                链表(离散内存)
┌───┬───┬───┬───┐            ┌─────┐     ┌─────┐     ┌─────┐
│ 1 │ 2 │ 3 │ 4 │            │ 1 │●│────→│ 2 │●│────→│ 3 │●│────→ None
└───┴───┴───┴───┘            └──┬──┘     └──┬──┘     └──┬──┘
  插入需批量移动                  插入只需修改指针
维度 数组 链表
内存布局 连续 离散
随机访问 O(1) O(n)
头部插入/删除 O(n) O(1)
中间插入/删除(已知前驱) O(n) O(1)
缓存友好性 (局部性) 低(指针跳转)

选择策略

  • 查询频繁、修改稀少 → 数组
  • 修改频繁、无需随机访问 → 链表
  • 需要兼顾 → 动态数组 (Python list)或 跳表

二、单向链表:从零实现

2.1 节点定义

链表的基本单元是节点,包含数据域和指针域:

python 复制代码
class ListNode:
    """链表节点"""
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next
    
    def __repr__(self):
        return str(self.val)

2.2 核心操作

链表操作的本质是指针的重新指向。以下是最小可用实现:

python 复制代码
class LinkedList:
    """单向链表(最小完整实现)"""
    
    def __init__(self):
        self.head = None
        self._size = 0
    
    def add_at_head(self, val) -> None:
        """头插:O(1)"""
        self.head = ListNode(val, self.head)
        self._size += 1
    
    def add_at_tail(self, val) -> None:
        """尾插:O(n) ------ 需遍历到最后"""
        new_node = ListNode(val)
        if not self.head:
            self.head = new_node
        else:
            curr = self.head
            while curr.next:
                curr = curr.next
            curr.next = new_node
        self._size += 1
    
    def delete_at_index(self, index: int) -> None:
        """删除第 index 个节点:O(n)"""
        if index < 0 or index >= self._size:
            raise IndexError("Index out of range")
        
        if index == 0:
            self.head = self.head.next
        else:
            prev = self.head
            for _ in range(index - 1):
                prev = prev.next
            prev.next = prev.next.next  # 跳过目标节点
        
        self._size -= 1
    
    def to_list(self) -> list:
        """转换为 Python 列表"""
        result = []
        curr = self.head
        while curr:
            result.append(curr.val)
            curr = curr.next
        return result


# 验证
ll = LinkedList()
ll.add_at_tail(1)
ll.add_at_tail(2)
ll.add_at_tail(3)
ll.add_at_head(0)
print(ll.to_list())  # [0, 1, 2, 3]
ll.delete_at_index(1)
print(ll.to_list())  # [0, 2, 3]

三、链表算法的六大核心技巧

3.1 技巧一:虚拟头节点(Dummy Head)

问题:头节点没有前驱,插入/删除时需要特殊处理。

解决:引入虚拟头节点,统一所有位置的操作逻辑。

python 复制代码
def remove_elements(head: ListNode, val: int) -> ListNode:
    """
    删除链表中所有值为 val 的节点
    使用虚拟头节点避免对头节点的特殊判断
    """
    dummy = ListNode(0, head)  # 虚拟头节点
    prev = dummy
    curr = head
    
    while curr:
        if curr.val == val:
            prev.next = curr.next  # 删除 curr
        else:
            prev = curr          # prev 后移
        curr = curr.next
    
    return dummy.next  # 返回真正的头节点

价值:将"头节点可能是目标"的边界情况转化为普通情况,代码更简洁,bug 更少。


3.2 技巧二:反转链表(三指针法)

迭代法:用三个指针依次后移,逐个反转指向。

python 复制代码
def reverse_list(head: ListNode) -> ListNode:
    """
    反转单向链表
    时间:O(n),空间:O(1)
    """
    prev = None
    curr = head
    
    while curr:
        next_temp = curr.next  # 暂存后继,防止断链
        curr.next = prev       # 反转指向
        prev = curr            # prev 前移
        curr = next_temp       # curr 前移
    
    return prev  # 新的头节点

执行过程1 → 2 → 3 → None):

步骤 prev curr next_temp 链表状态
初始 None 1 2 1 → 2 → 3
第1轮 1 2 3 None ← 1 2 → 3
第2轮 2 3 None None ← 1 ← 2 3
第3轮 3 None --- None ← 1 ← 2 ← 3

递归法(理解即可,面试推荐迭代法):

python 复制代码
def reverse_list_recursive(head: ListNode) -> ListNode:
    """递归反转"""
    if not head or not head.next:
        return head
    
    new_head = reverse_list_recursive(head.next)
    head.next.next = head   # 下一个节点指向自己
    head.next = None        # 断开原指向,防止环
    
    return new_head

3.3 技巧三:快慢指针

检测环(Floyd 判圈算法)
python 复制代码
def has_cycle(head: ListNode) -> bool:
    """
    判断链表是否有环
    时间:O(n),空间:O(1)
    """
    if not head or not head.next:
        return False
    
    slow = head
    fast = head.next  # 快指针先走一步,避免初始相遇
    
    while fast and fast.next:
        if slow == fast:
            return True
        slow = slow.next        # 慢:1步
        fast = fast.next.next   # 快:2步
    
    return False

原理:有环时,快指针相对慢指针的速度为 1 步/轮,必然追上;无环时,快指针先到尾。

找环入口
python 复制代码
def detect_cycle(head: ListNode) -> ListNode:
    """找到环的入口节点,无环返回 None"""
    if not head or not head.next:
        return None
    
    # 阶段1:找相遇点
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow == fast:
            break
    else:
        return None  # 无环
    
    # 阶段2:找入口
    # 数学证明:头到入口距离 = 相遇点到入口距离
    ptr1 = head
    ptr2 = slow
    while ptr1 != ptr2:
        ptr1 = ptr1.next
        ptr2 = ptr2.next
    
    return ptr1
找中点
python 复制代码
def find_middle(head: ListNode) -> ListNode:
    """
    找到链表中间节点
    偶数长度时返回第二个中间节点
    """
    slow = fast = head
    
    while fast and fast.next:
        slow = slow.next        # 1步
        fast = fast.next.next   # 2步
    
    return slow  # fast 到尾时,slow 在中点

应用:归并排序的链表版、重排链表、判断回文链表。


3.4 技巧四:删除倒数第 N 个节点

双指针(一次遍历)

python 复制代码
def remove_nth_from_end(head: ListNode, n: int) -> ListNode:
    """
    删除倒数第 n 个节点
    时间:O(n),空间:O(1)
    """
    dummy = ListNode(0, head)
    fast = slow = dummy
    
    # fast 先走 n+1 步,保持 slow 在目标前驱
    for _ in range(n + 1):
        fast = fast.next
    
    while fast:
        fast = fast.next
        slow = slow.next
    
    slow.next = slow.next.next  # 删除
    return dummy.next

原理fastslow 的距离恒为 n+1,当 fast 到尾(None)时,slow 恰好在倒数第 n+1 个节点(目标的前驱)。


3.5 技巧五:合并有序链表

python 复制代码
def merge_two_lists(l1: ListNode, l2: ListNode) -> ListNode:
    """
    合并两个升序链表
    时间:O(n+m),空间:O(1)
    """
    dummy = ListNode(0)
    curr = dummy
    
    while l1 and l2:
        if l1.val <= l2.val:
            curr.next = l1
            l1 = l1.next
        else:
            curr.next = l2
            l2 = l2.next
        curr = curr.next
    
    curr.next = l1 if l1 else l2  # 连接剩余
    return dummy.next

3.6 技巧六:重排链表(综合应用)

LeetCode 143 :将链表重新排列为 L0 → Ln → L1 → Ln-1 → L2 → Ln-2 → ...

python 复制代码
def reorder_list(head: ListNode) -> None:
    """
    重排链表
    1. 找中点(快慢指针)
    2. 反转后半部分
    3. 交替合并
    """
    if not head or not head.next:
        return
    
    # 步骤1:找中点
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
    
    # 步骤2:反转后半部分
    prev = None
    curr = slow.next
    slow.next = None  # 断开前后两部分
    while curr:
        next_temp = curr.next
        curr.next = prev
        prev = curr
        curr = next_temp
    
    # 步骤3:交替合并
    first, second = head, prev
    while second:
        tmp1 = first.next
        tmp2 = second.next
        first.next = second
        second.next = tmp1
        first = tmp1
        second = tmp2

四、双向链表与 LRU 缓存

4.1 双向链表实现

双向链表每个节点有前驱和后继指针,支持 O(1) 的任意位置删除:

python 复制代码
class DListNode:
    """双向链表节点"""
    def __init__(self, key=0, val=0):
        self.key = key
        self.val = val
        self.prev = None
        self.next = None


class DoublyLinkedList:
    """带头尾哨兵的双向链表"""
    
    def __init__(self):
        self.head = DListNode()   # 头哨兵
        self.tail = DListNode()   # 尾哨兵
        self.head.next = self.tail
        self.tail.prev = self.head
        self.size = 0
    
    def add_to_head(self, node: DListNode) -> None:
        """头部添加:O(1)"""
        node.prev = self.head
        node.next = self.head.next
        self.head.next.prev = node
        self.head.next = node
        self.size += 1
    
    def remove(self, node: DListNode) -> None:
        """删除指定节点:O(1)(已知节点地址)"""
        node.prev.next = node.next
        node.next.prev = node.prev
        self.size -= 1
    
    def move_to_head(self, node: DListNode) -> None:
        """移到头部:O(1)"""
        self.remove(node)
        self.add_to_head(node)
    
    def remove_tail(self) -> DListNode:
        """删除尾部:O(1)"""
        node = self.tail.prev
        self.remove(node)
        return node

4.2 LRU 缓存(LeetCode 146)

设计目标getput 操作均为 O(1)。

数据结构组合

  • 哈希表key → node,O(1) 查找
  • 双向链表:维护访问顺序,头部最近使用,尾部最久未使用
python 复制代码
class LRUCache:
    """
    LRU 缓存
    get/put 时间复杂度:O(1)
    """
    
    def __init__(self, capacity: int):
        self.capacity = capacity
        self.cache = {}              # key → node
        self.dll = DoublyLinkedList()
    
    def get(self, key: int) -> int:
        if key not in self.cache:
            return -1
        
        node = self.cache[key]
        self.dll.move_to_head(node)  # 更新为最近使用
        return node.val
    
    def put(self, key: int, value: int) -> None:
        if key in self.cache:
            node = self.cache[key]
            node.val = value
            self.dll.move_to_head(node)
        else:
            if self.dll.size >= self.capacity:
                tail = self.dll.remove_tail()
                del self.cache[tail.key]  # 淘汰最久未使用
            
            new_node = DListNode(key, value)
            self.cache[key] = new_node
            self.dll.add_to_head(new_node)


# 验证
cache = LRUCache(2)
cache.put(1, 1)
cache.put(2, 2)
print(cache.get(1))       # 1(1变为最近使用)
cache.put(3, 3)           # 淘汰 key=2
print(cache.get(2))       # -1(已被淘汰)
cache.put(4, 4)           # 淘汰 key=1
print(cache.get(1))       # -1
print(cache.get(3))       # 3
print(cache.get(4))       # 4

五、链表 vs 其他数据结构

操作 单向链表 双向链表(带尾指针) 动态数组
头部插入 O(1) O(1) O(n)
尾部插入 O(n) O(1) O(1) 均摊
中间插入(已知前驱) O(1) O(1) O(n)
头部删除 O(1) O(1) O(n)
尾部删除 O(n) O(1) O(1)
随机访问 O(n) O(n) O(1)
内存开销 高(指针) 更高(双指针) 低(连续)

六、链表问题决策树

复制代码
链表问题类型判断:
├── 需要修改头节点? → 使用虚拟头节点
├── 需要找位置/中点/环? → 快慢指针
├── 需要反转部分/全部? → 三指针迭代反转
├── 需要删除倒数第N个? → 双指针(间距n+1)
├── 需要合并多个有序? → 虚拟头节点 + 多路归并
└── 需要O(1)的get/put且按访问排序? → 哈希表 + 双向链表

七、核心心法

链表问题的难点不在于逻辑,而在于指针操作的精确性

1. 画图辅助

遇到复杂操作(如反转、合并),先在纸上画出指针变化,再写代码。

2. 边界检查清单

  • 头节点为空?
  • 单节点链表?
  • 操作后是否断链?
  • 返回值是否正确(dummy.next vs head)?

3. 虚拟头节点的条件反射

只要操作可能涉及头节点修改,立即使用 dummy head,省去 90% 的边界判断。

4. 快慢指针的条件反射

看到"找中点"、"判环"、"倒数第 K"等关键词,立即考虑快慢指针。

判断标准:当你能在看到链表题目时,10 秒内确定使用哪种技巧模式,并能在纸上准确画出指针变化------链表才真正成为你的本能。

相关推荐
吃好睡好便好21 小时前
用while循环语句求和
开发语言·学习·算法·matlab·信息可视化
TechWayfarer21 小时前
查询IP所在地的3种方案:从API到离线库,风控场景怎么选?
开发语言·网络·python·网络协议·tcp/ip
王璐WL21 小时前
【C语言入门级教学】函数的概念2
c语言·数据结构·算法
程序员榴莲21 小时前
Python 单例模式
开发语言·python·单例模式
hh.h.1 天前
昇腾CANN ops-transformer 仓的 MC2 算子:MoE 模型的全到全通信
python·深度学习·transformer·cann
不知名的忻1 天前
B 树与 B+ 树:面试完全指南
b树·算法·面试·b+树
运筹vivo@1 天前
2657. 找到两个数组的前缀公共数组 | 难度:中等
算法·leetcode·职场和发展·哈希表
索木木1 天前
NCCL SHARP 和 TREE算法
java·服务器·算法
NiceCloud喜云1 天前
Claude Files API 深入:从上传、复用到配额管理的工程化指南
android·java·数据库·人工智能·python·json·飞书