时间复杂度与空间复杂度
一、时间复杂度(Time Complexity)
时间复杂度是衡量算法执行效率的核心指标,它描述的是算法执行时间随输入规模增长的变化趋势,而非具体执行时间(不受硬件、编程语言影响)。通常用大O符号(O())表示,只保留对增长趋势起主导作用的最高阶项,忽略常数系数和低阶项。
1. 核心分析原则
- 只看最高阶项:例如
3n² + 5n + 2
简化为O(n²)
- 忽略常数系数:例如
5n
简化为O(n)
- 与输入数据分布无关:只关注最坏情况或平均情况(默认分析最坏情况)
- 操作单元定义:通常将基本操作(如赋值、比较、算术运算)视为O(1)时间单元
2. 常见时间复杂度及实例
(1)O(1) - 常数时间
- 特点:执行时间不随输入规模变化,始终为固定值。
- 场景:基本运算(加减乘除)、数组随机访问、哈希表查找等。
- 详细分析:即使存在多个O(1)操作,只要不随n变化,整体仍为O(1)
// 示例1:数组随机访问(底层是连续内存地址计算)
int getElement(int arr[], int index) {return arr[index]; // 计算地址偏移量是固定时间操作
}// 示例2:哈希表查找(理想情况下通过哈希函数直接定位)
unordered_map<string, int> dict = {{"apple", 1}, {"banana", 2}};
int val = dict["apple"]; // 哈希计算+桶访问是O(1)
(2)O(n) - 线性时间
- 特点:执行时间与输入规模n成正比,n翻倍时时间也翻倍。
- 场景:单循环遍历、线性查找等。
- 边界情况:循环中可能包含break提前终止,但最坏情况仍按O(n)计算
// 示例1:查找最大值(必须完整遍历)
int findMax(vector<int>& arr) {int max_val = INT_MIN;for (int num : arr) { // 必须比较所有n个元素if (num > max_val) max_val = num;}return max_val;
}// 示例2:线性搜索(可能提前终止但仍计为O(n))
int linearSearch(vector<int>& arr, int target) {for (int i = 0; i < arr.size(); i++) {if (arr[i] == target) return i; // 最好情况O(1),但最坏情况O(n)}return -1;
}
(3)O(logn) - 对数时间
- 特点:执行时间随n的对数增长(通常以2为底),n翻倍时时间只增加1。
- 核心逻辑:每次操作将问题规模缩小一半(如二分法)。
- 数学原理:对数复杂度源于递归式T(n) = T(n/2) + O(1)
// 示例1:二分查找(有序数组)
int binarySearch(vector<int>& arr, int target) {int left = 0, right = arr.size() - 1;while (left <= right) {int mid = left + (right - left) / 2; // 防止溢出if (arr[mid] == target) return mid;else if (arr[mid] < target) left = mid + 1; // 舍弃左半else right = mid - 1; // 舍弃右半}return -1; // 最多执行log₂n次循环
}// 示例2:求幂运算(快速幂算法)
double power(double x, int n) {if (n == 0) return 1;double half = power(x, n/2); // 递归规模减半return (n%2 == 0) ? half*half : half*half*x;
}
(4)O(nlogn) - 线性对数时间
- 特点:执行时间是n乘以logn,比O(n)慢,但比O(n²)快。
- 场景:高效排序算法(归并排序、快速排序、堆排序等)。
- 数学推导:常见于分治算法,递归树有logn层,每层处理O(n)操作
// 示例1:快速排序(平均情况)
void quickSort(vector<int>& arr, int low, int high) {if (low < high) {int pivot = partition(arr, low, high); // O(n)分区操作quickSort(arr, low, pivot-1); // 递归处理左半部分quickSort(arr, pivot+1, high); // 递归处理右半部分// 递归深度平均为logn,每层总操作O(n)}
}// 示例2:堆排序(建堆O(n),每次pop O(logn))
void heapSort(vector<int>& arr) {priority_queue<int> max_heap(arr.begin(), arr.end());for (int i = arr.size()-1; i >= 0; i--) {arr[i] = max_heap.top(); // 每次取最大O(logn)max_heap.pop(); // 共执行n次}
}
(5)O(n²) - 平方时间
- 特点:执行时间与n的平方成正比,n翻倍时时间变为4倍。
- 场景:双层嵌套循环(如冒泡排序、插入排序)。
- 优化方向:可通过剪枝或记忆化减少实际计算次数
// 示例1:选择排序(每次选择最小值)
void selectionSort(vector<int>& arr) {for (int i = 0; i < arr.size(); i++) { // n次int min_idx = i;for (int j = i+1; j < arr.size(); j++) { // 每次n-i次if (arr[j] < arr[min_idx]) min_idx = j;}swap(arr[i], arr[min_idx]); // 总计约n²/2次比较}
}// 示例2:矩阵乘法(三重循环)
vector<vector<int>> matrixMultiply(vector<vector<int>>& A, vector<vector<int>>& B) {int n = A.size();vector<vector<int>> C(n, vector<int>(n));for (int i = 0; i < n; i++) { // n次for (int j = 0; j < n; j++) { // n次for (int k = 0; k < n; k++) { // n次C[i][j] += A[i][k] * B[k][j]; // 总计n³次运算}}}return C;
}
3. 复杂度对比(增长速度)
O(1) < O(logn) < O(n) < O(nlogn) < O(n²) < O(n³) < O(2ⁿ) < O(n!)
- 前4种:适用于大规模数据处理(如排序算法、搜索算法)
- 中间2种:适用于中小规模数据(如动态规划问题)
- 后2种:仅适用于极小规模(如全排列问题)
二、空间复杂度(Space Complexity)
空间复杂度是衡量算法内存消耗的指标,描述算法所需存储空间随输入规模增长的变化趋势,同样用大O符号表示,关注额外空间(不包括输入数据本身)。
1. 常见空间复杂度及实例
(1)O(1) - 常数空间
- 特点:额外空间不随输入规模变化,始终为固定值。
- 注意事项:递归算法的调用栈不计入此类别
// 示例1:斐波那契数列迭代计算
int fib(int n) {if (n <= 1) return n;int a = 0, b = 1, c; // 固定3个变量for (int i = 2; i <= n; i++) {c = a + b;a = b;b = c;}return b; // 空间始终为O(1)
}// 示例2:原地数组操作
void squareArray(vector<int>& arr) {for (int& num : arr) {num = num * num; // 不创建新数组}
}
(2)O(n) - 线性空间
- 特点:额外空间与输入规模n成正比。
- 常见场景:需要复制输入数据或维护线性数据结构
// 示例1:递归斐波那契(调用栈深度n)
int fibRecursive(int n) {if (n <= 1) return n;return fibRecursive(n-1) + fibRecursive(n-2); // 递归树深度n
} // 实际空间O(n)而非O(2ⁿ),因为同时只有一条路径在内存中// 示例2:广度优先搜索(队列存储节点)
void BFS(Node* root) {queue<Node*> q; // 最坏存储所有节点q.push(root);while (!q.empty()) {Node* cur = q.front();q.pop();for (Node* child : cur->children) {q.push(child); // 队列最大可能存储O(n)节点}}
}
(3)O(logn) - 对数空间
- 特点:额外空间随n的对数增长,常见于递归调用。
- 数学原理:递归深度与问题规模的缩小速度相关
// 示例1:快速排序递归栈(理想划分)
void quickSort(vector<int>& arr, int low, int high) {if (low < high) {int pivot = partition(arr, low, high);quickSort(arr, low, pivot-1); // 递归左半quickSort(arr, pivot+1, high); // 递归右半} // 递归深度平均O(logn),最坏O(n)(当划分不平衡时)
}// 示例2:树的遍历(平衡二叉树)
void inorder(TreeNode* root) {if (!root) return;inorder(root->left); // 递归左子树cout << root->val; // 当前节点处理inorder(root->right); // 递归右子树
} // 栈深度=树高度,平衡树时为O(logn)
(4)O(n²) - 平方空间
- 特点:额外空间与n的平方成正比,常见于二维数据结构。
- 优化策略:考虑稀疏矩阵存储或动态计算
// 示例1:动态规划表(如最长公共子序列)
int LCS(string& s1, string& s2) {vector<vector<int>> dp(s1.size()+1, vector<int>(s2.size()+1)); // (m+1)×(n+1)表格// ...填充dp表...return dp.back().back();
}// 示例2:图的邻接矩阵
class Graph {vector<vector<int>> adjMatrix; // |V|×|V|矩阵
public:Graph(int n) : adjMatrix(n, vector<int>(n)) {}void addEdge(int u, int v) { adjMatrix[u][v] = 1; }
};
三、总结
维度 | 核心意义 | 关注重点 | 典型场景 | 优化策略 |
---|---|---|---|---|
时间复杂度 | 算法执行效率随n的增长趋势 | 循环次数、递归深度 | 遍历(O(n))、二分(O(logn)) | 分治法、剪枝、记忆化 |
空间复杂度 | 算法内存消耗随n的增长趋势 | 额外变量、数组、递归栈 | 数组复制(O(n))、原地操作(O(1)) | 复用空间、惰性计算 |
工程实践建议:
- 空间换时间:使用哈希表(O(n)空间)将查找时间从O(n)降到O(1)
- 时间换空间:流式处理大数据时,用多次扫描(O(n)时间)避免存储全部数据(O(1)空间)
- 递归改进:将O(n)空间递归(如斐波那契)改为迭代实现(O(1)空间)
- 数据结构选择:根据操作频率选择,如频繁查询用哈希表,频繁插入删除用链表