一、大O表示法:只看趋势
大O表示法描述的是算法执行时间随数据规模增长的变化趋势。推导时只需记住:保留最高阶项,去掉所有系数。
例如:2N + 3logN + 10 最终记为 O(N)。
二、O(log N):折半就是对数
对数复杂度的核心特征只有一个:每次循环或递归,问题的规模都缩小一半(或按固定比例缩小)。
2.1 循环折半
#include <stdio.h>
/*
* 功能:演示对数复杂度 O(log N)
* 关键特征:循环变量 i 每次乘以 2(翻倍)
*
* 推导:设循环执行 x 次后结束
* 第1次:i = 2
* 第2次:i = 4
* 第3次:i = 8
* ...
* 第x次:i = 2^x
*
* 结束条件:2^x >= N => x >= log₂N
* 因此时间复杂度为 O(log N)
*/
void demoLogN(int N) {
int count = 0;
int i = 1;
while (i < N) {
i = i * 2; // 关键:每次翻倍
count++;
}
printf("N=%d, 循环次数=%d\n", N, count);
}
int main() {
demoLogN(16); // N=16, 循环4次 (log₂16 = 4)
demoLogN(1024); // N=1024, 循环10次
return 0;
}
快速判断 :看到循环变量 i *= 2 或 i /= 2,立刻想到 O(log N)。底数是 2 还是 3 不影响大O表示。
2.2 二分查找
#include <stdio.h>
/*
* 功能:二分查找,在有序数组中查找目标值
* 时间复杂度:O(log N)
*
* 核心思想:每次比较后,搜索范围缩小一半
* 无论目标在哪,最多只需 log₂N 次比较
*/
int binarySearch(int arr[], int n, int target) {
int left = 0;
int right = n - 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; // 未找到
}
int main() {
int arr[] = {3, 8, 12, 25, 31, 47, 58, 69, 81, 95};
int n = 10;
int result = binarySearch(arr, n, 47);
printf("查找结果下标:%d\n", result); // 输出 5
return 0;
}
2.3 递归折半
#include <stdio.h>
/*
* 功能:递归方式演示对数复杂度
* 时间复杂度:O(log N)
*
* 分析:递归深度决定了执行次数
* 每次递归 N 变为 N/2,深度为 log₂N
* 每层只执行常数操作(打印、判断)
*/
void recursiveHalve(int N) {
if (N <= 1) {
return; // 递归终止条件
}
printf("当前 N = %d\n", N);
recursiveHalve(N / 2); // 关键:每次规模减半
}
int main() {
recursiveHalve(16); // 会打印 16, 8, 4, 2,共 log₂16 = 4 层
return 0;
}
三、O(N log N):线性遍历套折半操作
O(N log N) 是高效排序算法(如归并排序、堆排序)的典型复杂度。它的结构很固定:外层是 N 次的线性遍历,内层是 O(log N) 的折半操作。
#include <stdio.h>
/*
* 功能:演示线性对数复杂度 O(N log N)
*
* 外层循环:i 从 1 到 N,执行 N 次
* 内层循环:j 每次翻倍,执行 log N 次
* 总操作次数 = N * log N
*/
void demoNLogN(int N) {
int total = 0;
for (int i = 1; i <= N; i++) { // 外层:线性 N 次
int j = 1;
while (j < N) { // 内层:对数 log N 次
j = j * 2;
total++;
}
}
printf("N=%d, 总操作次数=%d\n", N, total);
}
int main() {
demoNLogN(8); // 8 * 3 = 24
demoNLogN(16); // 16 * 4 = 64
return 0;
}
实际场景 :对 N 个元素分别进行一次二分查找,整体就是 O(N log N)。
四、快速判断口诀
表格
| 代码特征 | 时间复杂度 | 判断要点 |
|---|---|---|
| 循环变量每次乘2或除2 | O(log N) | 折半、翻倍就是对数 |
| 外层 N 次 + 内层折半 | O(N log N) | 线性套对数 |
| 递归每次规模减半 | O(log N) | 递归深度为 log N |
| 递归先遍历 N 再折半递归(两次) | O(N log N) | 类似归并排序的递归树 |
五、练习题
题目1:
void funcA(int N) {
int i = N;
while (i > 1) {
i = i / 3; // 每次变为原来的 1/3
}
}
时间复杂度是多少?
题目2:
void funcB(int N) {
for (int i = 0; i < N; i++) {
int j = 1;
while (j < N) {
j = j * 2;
}
}
}
时间复杂度是多少?
题目3(递归):
void funcC(int N) {
if (N <= 1) return;
funcC(N / 2);
}
时间复杂度是多少?
题目4(递归):
void funcD(int N) {
if (N <= 1) return;
for (int i = 0; i < N; i++) {
printf("%d ", i);
}
printf("\n");
funcD(N / 2);
funcD(N / 2);
}
时间复杂度是多少?
题目5(双参数):
void funcE(int N, int M) {
for (int i = 0; i < N; i++) printf("1");
for (int j = 0; j < M; j++) printf("2");
}
时间复杂度是多少?(提示:O(M+N) 与 O(max(M,N)) 在渐进意义下等价,因为 max(M,N) ≤ M+N ≤ 2×max(M,N),两者只相差常数倍。通常写 O(M+N) 更直观。)
答案与解析
表格
| 题号 | 时间复杂度 | 解析 |
|---|---|---|
| 1 | O(log N) | 循环变量每次除以3,底数不影响大O |
| 2 | O(N log N) | 外层N次 × 内层log N次 |
| 3 | O(log N) | 递归深度为 log N,每层常数操作 |
| 4 | O(N log N) | 递归树深度为 log N,每层总工作量约为 N(N + N/2 + N/2 = 2N),总复杂度为 O(N log N) |
| 5 | O(M+N) | 两个独立循环,复杂度相加。也可写作 O(max(M,N)) |