时间复杂度评估算法运行需要多少时间;空间复杂度评估算法运行需要占用多少内存。
两者都只关心【数据规模变大时】的增长趋势,不统计精确耗时 / 内存大小。
- 时间复杂度:算循环多少次
- 空间复杂度:算新开多少内存
一、时间复杂度 O (f (n))
n:输入数据的规模(比如数组长度) \(O()\):大 O 记号,表示最坏情况下,算法执行步数随 n 的增长量级,忽略常数、低次项。
常见复杂度(从快到慢)
- (O(1)) 常数阶:执行次数固定,和 n 无关
java
int a = arr[0]; // 不管数组多大,只取第一个元素
- (O(log n)) 对数阶:每次处理把问题规模减半,二分查找
- (O(n)) 线性阶:循环遍历一遍数据
java
for(int i=0; i<n; i++){}
-
(O(nlog n)):线性对数阶,快速排序、归并排序
-
(O(n^2)) 平方阶:两层嵌套循环
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){}
} -
(O(n^3)) 立方阶:三层循环
-
(O(2^n)) 指数阶、(O(n!)) 阶乘阶:数据稍微大一点就直接算不完,基本不可用
✅ 重点:
- 只看最高阶项,去掉系数。例:(2n^2+3n+5) → (O(n^2))
- 默认分析最坏时间复杂度
二、空间复杂度 O (f (n))
衡量算法额外申请的内存空间 随数据规模 n 的增长趋势。 ⚠️ 输入本身占用的空间不算,只算算法执行过程中新开辟的变量、数组、递归栈等。
例子
-
(O(1)) 常数空间:只定义几个临时变量,没有开辟随 n 变大的容器
int sum = 0;
for(int i=0;i<n;i++) sum += arr[i]; -
(O(n)):新创建长度为 n 的数组
int[] newArr = new int[n];
-
递归:递归深度决定空间复杂度。比如递归深度为 n,则空间复杂度 \(O(n)\)
常见概念:原地算法(in-place),一般指空间复杂度 \(O(1)\),不需要额外大量存储空间。
三、举个直观对比例子
给定长度为 n 的数组,求数组最大值:
int max = arr[0];
for(int i=1; i<n; i++){
if(arr[i] > max) max = arr[i];
}
- 时间复杂度:\(O(n)\),遍历一次数组
- 空间复杂度:\(O(1)\),只额外开了 max 变量
四、面试常问要点
- 为什么不用毫秒统计运行时间?
代码运行时间受 CPU、语言、环境影响。复杂度是算法本身的理论特性,和机器无关,用来比较算法优劣。
- 时间和空间经常可以取舍:空间换时间(缓存,哈希表)。
- 大 O 只看增长趋势:\(n=10\) 的时候 \(O(n^2)\) 可能比 \(O(n)\) 还快;只有 n 很大的时候,复杂度差异才会体现。
Java 集合常用操作时间 & 空间复杂度
说明:
- 均为平均情况 ,标注最坏;
n代表集合内元素数量- 只看核心操作:增、删、查
- 空间复杂度:指集合本身存储元素的空间,新增元素时底层扩容会有额外开销;临时操作一般是 O (1)
一、List 列表
ArrayList(数组实现,随机访问强)
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| get(index) | O(1) | 直接数组下标访问 |
| set(index) | O(1) | 修改指定下标 |
| add (末尾) | O (1) 平均,O (n) 最坏 | 扩容时需要复制整个数组 |
| add (中间 / 头部) | O(n) | 后面元素全部后移 |
| remove(index) | O(n) | 删除点之后元素全部前移 |
| contains() | O(n) | 需要遍历查找 |
空间:底层数组容量 ≥ 元素个数;空间复杂度 O (n)
LinkedList(双向链表)
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| get(index) | O(n) | 需要从头 / 尾遍历到下标 |
| add (首尾) | O(1) | 链表节点修改指针 |
| add (中间) | O(n) | 先要遍历找到位置 |
| remove (首尾) | O(1) | |
| remove (中间) | O(n) | 先遍历定位 |
| contains() | O(n) | 遍历查找 |
⚠️ 很多人误区:LinkedList 不是所有 add/remove 都是 O (1),只有头尾操作才是 O (1)
空间:O (n),每个节点额外存前后指针,内存开销比 ArrayList 大
二、Set 集合(不重复)
HashSet(底层 HashMap)
| 操作 | 时间复杂度 |
|---|---|
| add | O (1) 平均,O (n) 最坏(哈希大量冲突) |
| remove | O (1) 平均,O (n) 最坏 |
| contains | O (1) 平均,O (n) 最坏 |
TreeSet(底层 TreeMap,红黑树,有序)
| 操作 | 时间复杂度 |
|---|---|
| add / remove / contains | \(O(\log n)\) |
红黑树,元素自动排序;支持范围查找
LinkedHashSet:继承 HashSet,额外维护插入顺序;增删查平均 O (1)
空间:HashSet / TreeSet 都是 O (n)
三、Map 键值对
HashMap(面试重点!数组 + 链表 / 红黑树,JDK1.8)
| 操作 | 时间复杂度 |
|---|---|
| get(key) | O (1) 平均;最坏 O (n)(哈希冲突严重,链表) |
| put(key,val) | O (1) 平均;最坏 O (n);扩容时 O (n) |
| remove(key) | O (1) 平均;最坏 O (n) |
JDK1.8:链表长度≥8 转红黑树,此时最坏退化成 \(O(\log n)\)
TreeMap(红黑树,key 有序)
| 操作 | 时间复杂度 |
|---|---|
| put / get / remove | \(O(\log n)\) |
LinkedHashMap
底层 HashMap + 双向链表,保留插入顺序;增删查平均 O (1)
空间:HashMap / TreeMap O (n)
四、Queue / Deque
- ArrayDeque (数组双端队列,推荐替代 Stack、LinkedList 做队列)
- addFirst/addLast、pollFirst/pollLast:平均 O (1)
- 扩容时 O (n)
- PriorityQueue 优先队列(最小堆)
- offer / poll:\(O(\log n)\)
- peek:O(1)
- contains:O(n)
✅ 总结
- ArrayList:随机访问 O (1);中间插入删除 O (n)
- LinkedList:头尾增删 O (1);按下标查找 O (n)
- HashMap:增删查平均 O (1);TreeMap 固定 \(O(\log n)\)
- HashSet 等价 HashMap;TreeSet 等价 TreeMap
- PriorityQueue 入队出队都是 \(O(\log n)\)