干货版《算法导论》03:动态数组 × 链表的极致平衡艺术

干货版《算法导论》03:动态数组 × 链表的极致平衡艺术

  • [Bilibili 同步视频](#Bilibili 同步视频)
  • [🔗 链表 vs 动态数组:天生的矛盾与互补](#🔗 链表 vs 动态数组:天生的矛盾与互补)
    • [✅ 链表(Linked List)](#✅ 链表(Linked List))
    • [✅ 动态数组(Dynamic Array)](#✅ 动态数组(Dynamic Array))
  • [📌 关键概念:什么是均摊时间复杂度(Amortized)?](#📌 关键概念:什么是均摊时间复杂度(Amortized)?)
  • [🚀 方案一:双向预留空间・动态双端数组](#🚀 方案一:双向预留空间・动态双端数组)
    • [🔧 实现逻辑](#🔧 实现逻辑)
    • [⚠️ 必须注意:缩容策略(防抖动!)](#⚠️ 必须注意:缩容策略(防抖动!))
  • [🧩 方案二:双动态数组拼接・零重新造轮子](#🧩 方案二:双动态数组拼接・零重新造轮子)
    • 设计思想
    • [⚠️ 边界处理(满分关键)](#⚠️ 边界处理(满分关键))
  • [🧠 实战硬核题:单链表原地反转后半段](#🧠 实战硬核题:单链表原地反转后半段)
    • [✅ 三步绝杀思路](#✅ 三步绝杀思路)
    • [💻 满分代码实现(Python)](#💻 满分代码实现(Python))
    • [📊 性能分析](#📊 性能分析)
  • [🎯 总结:结构设计的终极美学](#🎯 总结:结构设计的终极美学)

Bilibili 同步视频

干货版《算法导论》03:动态数组 × 链表的极致平衡艺术

在算法与数据结构的世界里,链表动态数组 像是一对性格迥异的双子星✨------ 一个灵动善变,一个沉稳高效。我们总在两端取舍:要么要O (1) 两端操作 ,要么要O (1) 随机访问 ,却很少思考:能不能二者兼得?

今天就带你拆解:如何用极简思路,打造「最坏 O (1) 查找 + 均摊 O (1) 两端动态操作」的究极结构,再手把手搞定单链表原地反转后半段的硬核代码~


🔗 链表 vs 动态数组:天生的矛盾与互补

先回顾两种基础结构的核心宿命

✅ 链表(Linked List)

  • 优势:头部 / 两端增删 O (1),只改指针,不挪数据

  • 致命伤:查找 O (n)!内存离散,必须从头遍历到目标位置

✅ 动态数组(Dynamic Array)

  • 优势:随机访问 O (1),地址偏移一步到位

  • 短板:头部增删 O (n),要批量搬移元素,成本极高

🌪 灵魂拷问:

有没有一种结构,既能像数组一样秒查 ,又能像链表一样两头随便插 / 删

答案:当然有! 就是我们要实现的「双向动态序列」------Dynamic Deque


📌 关键概念:什么是均摊时间复杂度(Amortized)?

很多同学对「均摊」一头雾水,这里用最通俗的话讲透👇

均摊复杂度 ≠ 平均复杂度

  • 平均:对所有输入求期望

  • 均摊:对同一数据结构的连续操作求总代价再平均

严谨定义

若一个操作均摊 O (k) ,则连续执行 n 次,总耗时 ≤ n×k

→ 哪怕单次偶尔很贵,长期看依然稳如常数

最经典例子:Python list append()

  • 偶尔扩容要 O (n)

  • 但 n 次插入总代价还是 O (n)

    均摊 O (1),稳得一批~


🚀 方案一:双向预留空间・动态双端数组

核心思路超简单:
给动态数组「前后都留空」,不再只在尾部留余量!

🔧 实现逻辑

  1. 初始化时,头部 & 尾部都预留线性空间

  2. 头插 / 尾插:直接填空位,不用搬数据

  3. 空间占满 / 太空:整体重分配,把元素挪到新数组中央

  4. 用「计费法」保证:每 O (n) 次廉价操作,才触发一次昂贵重分配

⚠️ 必须注意:缩容策略(防抖动!)

只扩容不缩容 = 内存爆炸 💥

频繁缩容扩容 = 性能抖动 📉

安全缩容规则

  • 元素数量降到容量的 1/4 左右再缩

  • 缩完依然保留前后缓冲

  • 保证:缩完后,至少再经过 O (n) 次操作才需要再次调整

这样就从根源避免「删一个就缩、插一个就扩」的震荡~


🧩 方案二:双动态数组拼接・零重新造轮子

如果你不想手写新结构,只想用现成动态数组(如 Python list) 实现双端高效?

思路更妙:用两个动态数组,一正一反拼起来!

设计思想

  • 前半数组:正常存,负责尾部操作

  • 后半数组:反向存 ,负责头部操作

  • 访问时做一点下标算术,即可模拟全局连续索引

⚠️ 边界处理(满分关键)

当其中一个数组变空时:

  • 把另一个数组劈成两半

  • 重新分配到两个数组中

  • 恢复「前后都有缓冲」的不变量

依旧满足:均摊 O (1) 两端操作 + 最坏 O (1) 随机访问


🧠 实战硬核题:单链表原地反转后半段

题目约束拉满,堪称面试高频杀招:

  • 给定长度为 2n 的单链表

  • 禁止新建节点

  • 禁止非 O (1) 额外空间(不能用数组 / 栈暂存)

  • 原地修改:把后 n 个节点逆序

✅ 三步绝杀思路

  1. 找中点:走到第 n 个节点,切分前后两半

  2. 反转后半段指针:只改 next 指向,不挪数据

  3. 修补首尾链接:让前半段尾 → 新后半段头,原后半段尾 → null

💻 满分代码实现(Python)

python 复制代码
class ListNode:
    def __init__(self, item):
        self.item = item
        self.next = None

class LinkedList:
    def __init__(self):
        self.head = None
        self.size = 0

def reorder_students(lst: LinkedList):
    """
    反转单链表后 n 个节点(总长度 2n),原地、O(1) 空间
    """
    n = lst.size // 2
    if n == 0:
        return

    # 1. 找到第 n 个节点 a
    a = lst.head
    for _ in range(n - 1):
        a = a.next

    # 2. 反转 a 之后的 n 个节点
    b = a.next
    x_prev, x = a, b

    for _ in range(n):
        next_node = x.next     # 先保存下家
        x.next = x_prev        # 反转指针
        x_prev, x = x, next_node

    # 3. 修补首尾:a 指向新头,旧头 b 指向 None
    c = x_prev
    a.next = c
    b.next = None

📊 性能分析

  • 时间:O(n),一趟遍历搞定

  • 空间:O(1),只用到常数个临时变量

  • 完全满足题目所有严苛约束,面试直接满分


🎯 总结:结构设计的终极美学

这一套内容,本质是在教你数据结构设计的核心哲学

  1. 空间换时间:预留缓冲,均摊昂贵操作

  2. 不变量维护:用简单规则保证结构稳定

  3. 原地修改:尽量复用已有节点 / 内存,不造新负担

最终我们得到:

最坏 O (1) 随机访问 (比链表强)

均摊 O (1) 两端增删 (比动态数组强)

原地、低空间、高性能

这就是算法世界里最迷人的地方 ------
不做取舍,直接全都要。


相关推荐
2301_766283441 小时前
如何在 Go 中使用 gocql 执行本地 CQL 脚本文件
jvm·数据库·python
dFObBIMmai1 小时前
MongoDB防注入攻击指南
jvm·数据库·python
li星野1 小时前
栈与队列通关八题:从括号匹配到接雨水,手撕LeetCode高频题(Python + C++)
c++·python·leetcode
彳亍1011 小时前
如何解决Oracle启动ORA-00119错误_网络服务名与listener相关性
jvm·数据库·python
SamDeepThinking1 小时前
IntelliJ IDEA 中有什么让你相见恨晚的技巧?
java·后端·程序员
weixin_459753941 小时前
c++怎么编写多线程安全的跨平台文件日志库_无锁队列与异步IO【附源码】
jvm·数据库·python
夏恪1 小时前
如何用 IDBKeyRange 范围匹配检索特定区间的本地数据
jvm·数据库·python
SamDeepThinking1 小时前
为什么选微服务而不是动态扩容单体
java·后端·架构
风筝在晴天搁浅1 小时前
字节 LeetCode CodeTop 912.排序数组
算法·leetcode