一、 核心概念:大 O 记号(渐进时间复杂度)
在 408 中,我们主要使用大 O 表示法,它衡量的是算法在最坏情况下的执行效率界限。
-
计算三大铁律(抓大头,去零头):
-
只保留最高阶项:。
-
常数系数化为 1:,。
-
加法法则(串行代码):。谁大听谁的。
-
乘法法则(嵌套循环):。内外层相乘。
-
二、 时间复杂度(Time Complexity)
看什么? 找代码里执行次数最多的那条语句(通常在最内层循环),看它的执行次数和问题规模 的数学关系。
常见量级(必须背诵,按从快到慢排序):
-
常数阶
-
特征:没有循环,或者循环次数是常数。
-
例子:基础的加减乘除、数组随机访问
a[i]、常数次循环for(int i=0; i<100; i++)。
-
-
对数阶 (408 最爱考的拉分项)
-
特征:循环变量每次乘以/除以一个常数,呈指数级逼近终点。
-
例子:二分查找、二叉树的遍历(树高)。
-
代码:
while(i <= n) { i = i * 2; }
-
-
线性阶
-
特征:单层循环,步长为常数(通常为 1)。
-
例子:遍历一维数组、链表查找。
-
代码:
for(int i=0; i<n; i++) { ... }
-
-
线性对数阶
-
特征:外层是一个 的循环,内层是一个 的循环。
-
例子:快速排序、归并排序、堆排序的平均情况。这是比较排序算法的时间下限。
-
-
平方阶
-
特征:两层嵌套循环。
-
例子:冒泡排序、插入排序、选择排序、遍历二维矩阵。
-
-
/ 指数阶/阶乘阶
- 特征:通常见于暴力穷举。如果在 408 算法大题里你写出了这种复杂度,基本意味着方法全错,只能拿 1-2 分的代码卷面分。
三、 空间复杂度(Space Complexity)
看什么? 为了运行这个算法,除了题目原本给你的输入数据外,你额外申请了多少内存空间?
-
原地工作(In-place)
-
特征:只定义了几个额外的局部变量(如
int i, j, temp;)。无论 有多大,这几个变量占用的字节数不变。 -
408 要求:只要题目说“空间复杂度尽可能高效”,潜台词就是逼你写 。
-
-
线性空间
- 特征:为了处理 个数据,你
malloc(或者new)了一个大小为 的辅助数组。
- 特征:为了处理 个数据,你
-
平方空间
- 特征:开辟了大小为 的二维数组(如:图的邻接矩阵)。
-
【隐蔽考点】递归的函数调用栈空间
-
注意:递归代码虽然没有明显定义数组,但每次递归系统都要压栈(保存局部变量和返回地址)。
-
计算:空间复杂度 = 递归树的深度。如果递归深度是 ,空间复杂度就是 。
-
四、 408 算法题“骗分”兵法
既然你的策略是“先拿基础分”,那这几条原则你必须刻在骨子里:
-
时空互换原则:时间和空间往往不可兼得。408 大题如果卡住了,绝对不要死磕双优。立刻开一个新数组(牺牲空间,变成 ),用最简单直接的 循环把流程跑通。13 分你至少能拿 8-10 分!
-
暴力出奇迹:看到不懂的题,先写两层
for循环暴力匹配。 往往是所有题目的保底解法。 -
不要在考卷上写死循环:即使逻辑不完美,你的
while和for必须有明确的退出条件(比如i < n),否则直接 0 分。
当数组 A 被当作参数传进 FindMedian 函数的那一刻,它就不再是一个完整的数组了,它退化成了一个指向数组第一个元素的指针(本质上变成了 int *A)。 在常见的 64 位系统下,不管你这个数组里装了 5 个数还是 5 万个数,一个指针变量永远只占 8 个字节。那么 sizeof(A) 永远等于 8,sizeof(A[0]) 是一个整型占 4 个字节。算出来的 永远是 。你的程序跑两步直接就结束了。
408算法大题时间与空间复杂度计算通用指南
是什么
-
定义:算法的时间复杂度 是衡量算法运行时间随数据规模 增长的渐进趋势;空间复杂度 是衡量算法执行过程中占用的 额外内存空间 随 增长的渐进趋势。两者均使用大 记号(Big-O)表示。
-
大白话:时间复杂度看“代码执行了多少次基本操作”,空间复杂度看“除了原输入数据外,额外申请了多少变量或递归栈空间”。
核心内容
1. 时间复杂度 的通用计算方法
计算核心:找到代码中执行次数最多的 基本语句(通常是最内层循环),计算其随数据规模 增长的累计执行次数 ,取最高阶项并忽略常数系数。
常见代码结构的计算规律
① 单重循环与简单累加
C
int sum = 0;
for (int i = 0; i < n; i++) { // 循环执行 n 次
sum += A[i]; // 基本语句
}
-
次数:
-
复杂度:
② 多重嵌套循环 或
C
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) { // 外层 i=0 时内层 n-1 次,i=1 时 n-2 次...
// 基本语句
}
}
-
次数:
-
原则:忽略低阶项与常数系数,取最高阶 ,得到 。
③ 变量翻倍/减半循环
C
int i = 1;
while (i < n) {
i = i * 2; // 每次乘以 2
}
-
次数:设执行 次,则
-
复杂度:忽略对数底数,统一记为 。
④ 递归分治结构(Master 定理 / 递推树)
-
双侧分治(如完全快速排序、归并排序):
每次把规模为 的问题拆成 2 个规模为 的子问题,本层处理耗时 。
递归树共有 层,每层总工作量为 ,总时间为 ====。
-
单侧减半(如快速选择 QuickSelect、二分查找):
每次把规模为 的问题丢弃一半,只保留 1 个规模为 的子问题,本层处理耗时 。
计算等比数列累加: ====。
2. 空间复杂度 的通用计算方法
计算核心:只计算算法在运行过程中“额外申请的内存空间”,原输入数组/数据占用的内存空间不计入。
常见空间开销来源
① 局部简单变量
C
int low = 0, high = n - 1, temp; // 只使用了常数个局部变量
-
无论 是 10 还是 100 万,辅助变量个数恒定,空间复杂度为 。
② 辅助数组/哈希表
C
int *B = (int *)malloc(n * sizeof(int)); // 申请了与 n 等大的辅助数组
-
申请的额外内存空间与输入规模 成正比,空间复杂度为 。
③ 函数递归调用栈(最易遗漏点 ⚠️)
递归调用的本质是利用系统栈保存每一层的函数局部变量和返回地址。
-
==递归栈空间 = 递归树的最大深度 每层栈帧大小==。
-
例 1:单路线性递归(如递归求阶乘、单链表递归)
递归深度为 ,系统栈中同时保存 个函数帧 。
-
例 2:双侧分治递归(如快速排序、二叉树树高)
平均情况下,递归树高度为 ,系统栈中最多同时保存 个函数帧 ====。
(注:最坏情况下快排树退化为单链,深度为 )。
例题
-
题目:指出 2016 真题中“集合划分为 且 最大”的暴力全排序算法与最优快速选择算法(QuickSelect)的平均时间复杂度和空间复杂度,并说明依据。
-
分析:比较全排序算法(双侧递归)与单侧划分算法(QuickSelect)在步数规约和栈空间占用上的差异。
-
答案:
-
全排序方案:平均时间复杂度为 ====,依据为快速排序递归树深度为 且每层处理 次;空间复杂度为 ====,依据为系统递归调用栈的深度。
-
快速选择方案(最优):平均时间复杂度为 ====,依据为单侧划分比较次数收敛于等比数列 ;空间复杂度为 ====,依据为采用
while循环迭代实现,无递归栈开销,仅占用常数个辅助变量。
-
⚠️ 易错点
Warning
混淆输入空间与额外空间 → 题目给出的输入数组
int A[]本身占用的内存空间不计入空间复杂度,只有代码里额外malloc的内存或系统递归栈才算!忽视递归栈开销 → 凡是写了递归函数(如
QuickSort),哪怕一行malloc都没写,空间复杂度也绝不能写 ,至少是 == 的栈空间==!混淆单侧递归与双侧递归 → 完全快速排序是对左右两边都递归处理(),时间为 ;快速选择(QuickSelect)只对包含目标点的单侧递归/迭代(),时间复杂度收敛为 ====。
把最坏情况当成平均情况 → 快速排序在最坏情况(如完全有序)下时间复杂度会退化为 ,但在 408 答题中第 (3) 问若无明确说明,默认书写并推导平均复杂度。