干货版《算法导论》03:动态数组 × 链表的极致平衡艺术
- [Bilibili 同步视频](#Bilibili 同步视频)
- [🔗 链表 vs 动态数组:天生的矛盾与互补](#🔗 链表 vs 动态数组:天生的矛盾与互补)
-
- [✅ 链表(Linked List)](#✅ 链表(Linked List))
- [✅ 动态数组(Dynamic Array)](#✅ 动态数组(Dynamic Array))
- [📌 关键概念:什么是均摊时间复杂度(Amortized)?](#📌 关键概念:什么是均摊时间复杂度(Amortized)?)
- [🚀 方案一:双向预留空间・动态双端数组](#🚀 方案一:双向预留空间・动态双端数组)
-
- [🔧 实现逻辑](#🔧 实现逻辑)
- [⚠️ 必须注意:缩容策略(防抖动!)](#⚠️ 必须注意:缩容策略(防抖动!))
- [🧩 方案二:双动态数组拼接・零重新造轮子](#🧩 方案二:双动态数组拼接・零重新造轮子)
-
- 设计思想
- [⚠️ 边界处理(满分关键)](#⚠️ 边界处理(满分关键))
- [🧠 实战硬核题:单链表原地反转后半段](#🧠 实战硬核题:单链表原地反转后半段)
-
- [✅ 三步绝杀思路](#✅ 三步绝杀思路)
- [💻 满分代码实现(Python)](#💻 满分代码实现(Python))
- [📊 性能分析](#📊 性能分析)
- [🎯 总结:结构设计的终极美学](#🎯 总结:结构设计的终极美学)
Bilibili 同步视频
在算法与数据结构的世界里,链表 与动态数组 像是一对性格迥异的双子星✨------ 一个灵动善变,一个沉稳高效。我们总在两端取舍:要么要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),稳得一批~
🚀 方案一:双向预留空间・动态双端数组
核心思路超简单:
给动态数组「前后都留空」,不再只在尾部留余量!
🔧 实现逻辑
-
初始化时,头部 & 尾部都预留线性空间
-
头插 / 尾插:直接填空位,不用搬数据
-
空间占满 / 太空:整体重分配,把元素挪到新数组中央
-
用「计费法」保证:每 O (n) 次廉价操作,才触发一次昂贵重分配
⚠️ 必须注意:缩容策略(防抖动!)
只扩容不缩容 = 内存爆炸 💥
频繁缩容扩容 = 性能抖动 📉
安全缩容规则
-
元素数量降到容量的 1/4 左右再缩
-
缩完依然保留前后缓冲
-
保证:缩完后,至少再经过 O (n) 次操作才需要再次调整
这样就从根源避免「删一个就缩、插一个就扩」的震荡~
🧩 方案二:双动态数组拼接・零重新造轮子
如果你不想手写新结构,只想用现成动态数组(如 Python list) 实现双端高效?
思路更妙:用两个动态数组,一正一反拼起来!
设计思想
-
前半数组:正常存,负责尾部操作
-
后半数组:反向存 ,负责头部操作
-
访问时做一点下标算术,即可模拟全局连续索引
⚠️ 边界处理(满分关键)
当其中一个数组变空时:
-
把另一个数组劈成两半
-
重新分配到两个数组中
-
恢复「前后都有缓冲」的不变量
依旧满足:均摊 O (1) 两端操作 + 最坏 O (1) 随机访问
🧠 实战硬核题:单链表原地反转后半段
题目约束拉满,堪称面试高频杀招:
-
给定长度为 2n 的单链表
-
禁止新建节点
-
禁止非 O (1) 额外空间(不能用数组 / 栈暂存)
-
原地修改:把后 n 个节点逆序
✅ 三步绝杀思路
-
找中点:走到第 n 个节点,切分前后两半
-
反转后半段指针:只改 next 指向,不挪数据
-
修补首尾链接:让前半段尾 → 新后半段头,原后半段尾 → 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),只用到常数个临时变量
-
完全满足题目所有严苛约束,面试直接满分
🎯 总结:结构设计的终极美学
这一套内容,本质是在教你数据结构设计的核心哲学:
-
空间换时间:预留缓冲,均摊昂贵操作
-
不变量维护:用简单规则保证结构稳定
-
原地修改:尽量复用已有节点 / 内存,不造新负担
最终我们得到:
✅ 最坏 O (1) 随机访问 (比链表强)
✅ 均摊 O (1) 两端增删 (比动态数组强)
✅ 原地、低空间、高性能

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