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 秒内确定使用哪种技巧模式,并能在纸上准确画出指针变化------链表才真正成为你的本能。

相关推荐
曲幽1 小时前
FastAPI 少有人提的实用技巧:把 Depends 依赖提到路由层,代码少写60%
python·fastapi·web·routes·depends·prefix·apiroute
科研前沿2 小时前
纯视觉无感解算 + 动态数字孪生:室内外无感定位技术全新升级
大数据·人工智能·算法·重构·空间计算
qiaozhangchi2 小时前
求解器学习笔记
笔记·python·学习
kexnjdcncnxjs2 小时前
Redis如何记录每一次写操作_开启AOF持久化机制实现命令级追加记录
jvm·数据库·python
Wadli2 小时前
26.单调栈
算法
程序媛徐师姐2 小时前
Python基于Django的小区果蔬预定系统【附源码、文档说明】
python·django·小区果蔬预定系统·果蔬预定·python小区果蔬预定系统·小区果蔬预定·python果蔬预定系统
晨曦夜月2 小时前
进程的五大状态及特殊进程解析
linux·服务器·算法
吟安安安安2 小时前
适合短期冲刺的学习工作流(针对算法)
学习·算法