谈谈数组和链表的时间复杂度
一、数组(Array)
数组是一块 连续的内存空间,元素紧挨着存放。
随机访问快 O(1)
因为数组的地址是连续的,第
i
个元素的地址可以通过公式直接算出来:地址 = 起始地址 + i × 元素大小
所以访问任意下标(比如
arr[1000]
)不需要遍历,直接跳到内存位置即可,耗时固定。
插入/删除慢 O(n)
如果要在数组中间插入一个元素,比如在
arr[2]
插入 99:[10, 20, 30, 40, 50]↑ 插入 99
那么
30, 40, 50
都要往后挪一位,才能空出位置。删除同理,要把后面的元素全部往前移,避免空洞。
所以插入/删除需要移动 平均一半元素,最坏情况移动 n 个,复杂度 O(n)。
二、链表(Linked List)
链表的元素(节点)分散在内存中,每个节点都保存了 指向下一个节点的指针。
插入/删除快 O(1)
插入只需要改几个指针,不需要整体移动。
例如在 20 和 30 之间插入 25:[20 | next] -> [30 | next]插入后: [20 | next] -> [25 | next] -> [30 | next]
删除也类似,只要把前一个节点的指针指向下一个节点即可。
这些操作和链表长度无关,所以是 O(1)。
随机访问慢 O(n)
因为链表的节点不连续,不能直接算出第 i 个元素的地址。
要找到第
i
个元素,必须从头节点开始,一个个沿着指针走,直到走到目标位置。所以平均需要访问 n/2 个节点,复杂度 O(n)。
三、总结对比
数据结构 | 随机访问 | 插入/删除 |
---|---|---|
数组 | O(1) ✅ | O(n) ❌ |
链表 | O(n) ❌ | O(1) ✅ |
这里提出一个问题:链表插入一定比数组快吗?
答案是否定的:
之前说的 插入 O(1) 指的是:当你已经拿到插入位置的节点指针时,修改指针的操作本身是 O(1)。
详细拆分:
假设链表是:
head -> [10] -> [20] -> [30] -> [40] -> [50] -> [60]
你要在 index=5 和 index=6 之间插入新节点 [55]
。
1. 定位位置 (O(n))
你需要先遍历链表,找到第 5 个节点
[50]
。这一过程需要从头走一遍,最坏 O(n)。
2. 修改指针 (O(1))
假设你已经有了第 5 个节点
[50]
的引用。插入操作只要改两条指针:
原来:
[50] -> [60]插入后:
[50] -> [55] -> [60]
这一步和链表长度无关,固定时间,所以是 O(1)。
那么为什么说链表插入是 O(1)?
这是一个 前提条件不同的问题:
如果你只知道 index(比如第 5 个位置),那必须先遍历链表,整体复杂度 O(n)。
如果你已经持有 要插入位置的节点引用(比如
[50]
这个节点对象),那插入动作就是 O(1)。
所以在算法书里常见的说法:
数组:查找 O(1),插入 O(n)
链表:查找 O(n),插入 O(1)
其实是各自强调了它们的优势场景。
类比理解:
数组像一排教室座位,要插人得全体后移,很麻烦。
链表像一串散开的手牵手的人,只要你站在第 5 个人旁边,就能很快把新朋友插进来。
但如果你要找到第 5 个人,就得从头数过去,时间复杂度其实还是O(n)。
既然这样,那链表有什么优势呢?
链表真正的优势是在以下场景:
已知节点引用时的插入/删除
例如有个指针
p
指向第 5 个节点,要在它后面插入/删除节点,只需改指针,O(1)。这在某些算法中很有用(比如 LRU 缓存,频繁在链表头尾插入/删除)。
动态扩展时更灵活
数组扩容需要搬移到更大的一块连续内存,链表不需要。
频繁在头部/尾部插入删除
单链表头部插入:O(1)
双向链表尾部插入删除:O(1)
对比数组头部插入/删除需要整体移动,效率差。