时间复杂度是衡量算法运行时间随输入规模增长而变化的一个指标。它不是具体的运行时间,而是一个增长趋势的表示。
1. 什么是时间复杂度?
- 定义:时间复杂度表示算法执行时间随输入规模(通常用 nn 表示)增长的趋势。
- 表示方法:用大 OO 符号(Big-O Notation)表示,例如 O(n)O(n)、O(n2)O(n2) 等。
- 意义:帮助我们分析算法的效率,尤其是在输入规模很大时。
2. 如何求时间复杂度?
步骤 1:找出算法的基本操作
- 基本操作是算法中执行次数最多的操作,通常是循环、递归或条件判断中的操作。
步骤 2:计算基本操作的执行次数
- 根据输入规模 nn,计算基本操作执行的次数。
步骤 3:用大 OO 表示法表示
- 忽略常数项和低阶项,只保留最高阶的项。
3. 常见的时间复杂度
以下是常见的时间复杂度,按效率从高到低排序:
| 时间复杂度 | 名称 | 例子 |
|---|---|---|
| O(1)O(1) | 常数时间 | 访问数组中的元素 |
| O(logn)O(logn) | 对数时间 | 二分查找 |
| O(n)O(n) | 线性时间 | 遍历数组或链表 |
| O(nlogn)O(nlogn) | 线性对数时间 | 快速排序、归并排序 |
| O(n2)O(n2) | 平方时间 | 冒泡排序、选择排序 |
| O(2n)O(2n) | 指数时间 | 递归求解斐波那契数列 |
| O(n!)O(n!) | 阶乘时间 | 旅行商问题(穷举所有可能的路径) |
4. 如何计算时间复杂度?
例子 1:常数时间 O(1)
int a = 10; // 1次
int b = 20; // 1次
int c = a + b; // 1次
- 基本操作执行次数是固定的,与输入规模 n 无关。
- 时间复杂度为 O(1)。
例子 2:线性时间 O(n)
for (int i = 0; i < n; i++) { // 循环 n 次
System.out.println(i); // 1次
}
- 基本操作
System.out.println(i)执行了 n 次。 - 时间复杂度为 O(n)。
例子 3:平方时间 O(n2)
for (int i = 0; i < n; i++) { // 循环 n 次
for (int j = 0; j < n; j++) { // 循环 n 次
System.out.println(i + "," + j); // 1次
}
}
- 基本操作
System.out.println(i + "," + j)执行了n×n=n2次。 - 时间复杂度为O(n2)。
例子 4:对数时间 O(logn)
int i = 1;
while (i < n) {
i = i * 2; // 每次 i 乘以 2
}
- 每次循环i乘以 2,直到i大于或等于n。
- 循环次数是 log2n(因为2k=n,所以 k=log2n)
例子 5:线性对数时间 O(nlogn)
for (int i = 0; i < n; i++) { // 循环 n 次
int j = 1;
while (j < n) { // 循环 log n 次
j = j * 2;
}
}
- 外层循环执行 n 次,内层循环执行 logn 次。
- 总操作次数为 n×logn。
- 时间复杂度为 O(nlogn)。
5. 时间复杂度的计算规则
-
忽略常数项:
- O(2n) 简化为 O(n)。
- O(100) 简化为 O(1)。
-
忽略低阶项:
- O(n2+n) 简化为 O(n2)。
- O(n+logn) 简化为 O(n)。
-
嵌套循环:
- 外层循环执行 n 次,内层循环执行 m 次,时间复杂度为 O(n×m)。
-
递归算法:
- 递归的时间复杂度通常需要分析递归的深度和每次递归的操作次数。
6. 递归算法的时间复杂度
递归的时间复杂度通常用递归树或主定理来计算。
例子:斐波那契数列
int fib(int n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
- 每次递归调用会分解为两个子问题,时间复杂度为 O(2n)。
7. 总结
-
时间复杂度是描述算法运行时间与输入规模关系的指标。
-
计算方法:
- 找出基本操作。
- 计算基本操作的执行次数。
- 用大 OO 表示法表示。
-
常见时间复杂度:O(1)、O(logn)、O(n)、O(nlogn)、O(n2)、O(2n)。