🌟 时间复杂度和空间复杂度的概念
- 时间复杂度: 表示算法执行时间与输入数据量之间的关系。
- 空间复杂度: 表示算法所需内存空间与输入数据量之间的关系。
📝 特点
1️⃣ 时间复杂度:
- 描述算法执行速度与数据规模之间的增长关系。
- 常用表示法: (O(1)), (O(\log n)), (O(n)), (O(n^2)), (O(n^3)), (O(2^n)), (O(n!)) 等。
2️⃣ 空间复杂度:
- 描述算法所需内存与数据规模之间的增长关系。
- 常用表示法: (O(1)), (O(\log n)), (O(n)), (O(n^2)) 等。
🧭 如何判定时间复杂度
- 若算法中含有顺序结构,时间复杂度按加法规则计算。
- 若算法中含有循环结构,时间复杂度按乘法规则计算。
- 若算法中含有分支结构,时间复杂度取所有分支中最大值。
- 常数、低阶项和高阶项系数在时间复杂度中可以省略。
- 对于递归算法,时间复杂度等于递归次数乘以每次递归的时间复杂度。
🧭 如何判定空间复杂度
- 基本数据类型的变量和常量,视为 (O(1))。
- 通常情况下,算法所需的临时工作空间随着问题规模 ( n ) 的增长而增长。因此,数组和矩阵通常是 (O(n)) 或 (O(n^2))。
- 字符串,通常视为 (O(n)),其中 ( n ) 是字符串的长度。
- 若算法递归调用自身,根据调用次数和每次调用所需的临时空间,递归空间复杂度为调用次数乘以每次调用所需的空间。
- 对于并发执行的算法或程序,总空间需求是各部分最大值的总和,因为它们并发执行,所以需要累加每个部分的空间需求。
⚠️ 注意: 与时间复杂度类似,我们通常只关心空间复杂度的增长率。因此,常数和低阶项可以被忽略。
⚡️ 时间复杂度示例分析
1️⃣ (O(1)) - 常数时间复杂度:
无论输入数据的大小如何,算法都执行固定数量的操作。
示例:
int a = 10;
int b = 20;
int sum = a + b;
2️⃣ (O(\log n)) - 对数时间复杂度:
常见于二分搜索或平衡二叉搜索树的查找操作。
示例:
int binarySearch(int arr[], int x) {
int l = 0, r = arr.length - 1;
while (l <= r) {
int m = l + (r - l) / 2;
if (arr[m] == x)
return m;
if (arr[m] < x)
l = m + 1;
else
r = m - 1;
}
return -1;
}
3️⃣ (O(n)) - 线性时间复杂度:
算法的执行时间与输入数据的大小成正比。
示例:
void printArray(int arr[]) {
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
4️⃣ (O(n^2)) - 平方时间复杂度:
常见于简单排序算法,如冒泡排序。
示例:
void bubbleSort(int arr[]) {
int n = arr.length;
for (int i = 0; i < n-1; i++)
for (int j = 0; j < n-i-1; j++)
if (arr[j] > arr[j+1]) {
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
5️⃣ (O(n^3)) - 立方时间复杂度:
较少见,可能出现在三重嵌套循环中。
示例:
void someAlgorithm(int n) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
for (int k = 0; k < n; k++) {
// Some constant-time operations
}
}
}
}
6️⃣ (O(2^n)) - 指数时间复杂度:
常见于某些递归算法,如计算斐波那契数。
示例:
int fibonacci(int n) {
if (n <= 1)
return n;
return fibonacci(n-1) + fibonacci(n-2);
}
7️⃣ (O(n!)) - 阶乘时间复杂度:
常见于旅行商问题的求解。
示例:
一个简单的旅行商问题的求解可能会尝试所有可能的路径来找到最短的路径,这导致了 (O(n!)) 的时间复杂度。
⚠️ 注意: 这些只是各个复杂度级别的简单示例。在实际应用中,可能会遇到更复杂的情况和算法。
⚡️ 空间复杂度示例分析
1️⃣ (O(1)) - 常数空间复杂度:
无论输入数据的大小如何,算法都使用固定大小的额外空间。
示例:
int findMax(int arr[]) {
int max = Integer.MIN_VALUE;
for (int i = 0; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
return max;
}
分析: 无论数组 arr
有多大,我们都只使用了一个额外的 max
变量来存储最大值。
2️⃣ (O(\log n)) - 对数空间复杂度:
常见于一些递归算法,其中递归的深度是对数的。
示例:
int binarySearch(int arr[], int x) {
int l = 0, r = arr.length - 1;
while (l <= r) {
int m = l + (r - l) / 2;
if (arr[m] == x)
return m;
if (arr[m] < x)
l = m + 1;
else
r = m - 1;
}
return -1;
}
分析: 二分搜索的递归调用(或迭代深度)是对数的。
3️⃣ (O(n)) - 线性空间复杂度:
空间需求与输入数据大小成正比。
示例:
int[] copyArray(int arr[]) {
int[] copy = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
copy[i] = arr[i];
}
return copy;
}
分析: 我们创建了一个新的数组 copy
,其大小与输入数组 arr
相同。
4️⃣ (O(n^2)) - 平方空间复杂度:
在二维数据结构中较为常见,例如,创建一个二维数组。
示例:
int[][] generateMatrix(int n) {
int[][] matrix = new int[n][n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
matrix[i][j] = i * j;
}
}
return matrix;
}
分析: 我们创建了一个 n x n
大小的二维数组 matrix
。
⚠️ 注意: 空间复杂度主要考虑额外的空间使用,不包括输入和输出数据本身所占用的空间。