算法复杂度:从入门到面试必备
掌握算法复杂度,是通往高效编程的第一道门槛
前言
在数据结构与算法的学习道路上,算法复杂度是我们遇到的第一个重要概念。它不仅是理论分析的工具,更是校招笔试和面试中的必考内容。本文将带你从零开始理解时间复杂度和空间复杂度,掌握大O渐进表示法,并通过经典例题加深理解。
一、什么是数据结构和算法?
1.1 数据结构
数据结构(Data Structure)是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合。没有一种单一的数据结构对所有用途都有用,因此我们需要学习各式各样的数据结构,如:线性表、树、图、哈希等。
1.2 算法
算法(Algorithm)是定义良好的计算过程,它取一个或一组值作为输入,并产生一个或一组值作为输出。简单来说,算法就是一系列计算步骤,用来将输入数据转化为输出结果。
1.3 为什么重要?
- ✅ 校园招聘笔试必考
- ✅ 校园招聘面试必考
- ✅ 覆盖几乎所有技术岗位:Java工程师、C++工程师、前端工程师、算法工程师、测试开发工程师等
二、为什么要学算法复杂度?
2.1 面试中的高频考点
从各大厂的面试题来看,算法复杂度几乎是必考内容:
| 公司 | 岗位 | 复杂度相关考题 |
|---|---|---|
| 腾讯 | C++后台开发实习 | 快排堆排时间复杂度、推导过程 |
| 科大讯飞 | 研发类校招 | 多个算法题涉及复杂度分析 |
| 猎豹科技 | C++开发实习 | 数组和链表插入/删除/查找的时间复杂度、最小堆复杂度 |
2.2 衡量算法好坏的标准
衡量一个算法的好坏,主要从两个维度考量:
| 维度 | 含义 | 关注程度 |
|---|---|---|
| 时间复杂度 | 算法运行的快慢 | 目前主要关注 |
| 空间复杂度 | 算法运行所需额外空间 | 早期受限于存储容量,如今关注度降低 |
三、时间复杂度详解
3.1 为什么不能直接计算程序运行时间?
- 运行时间受编译环境 和机器配置影响
- 只能在程序写完后测试,无法提前评估
- 不同语句编译后的指令条数不同,精确计算困难
因此,我们使用时间复杂度函数 T(N) 来计算程序的执行次数,用执行次数代表时间效率的优劣。
3.2 核心案例:Func1 的执行次数分析
c
void Func1(int N)
{
int count = 0;
for (int i = 0; i < N; ++i)
{
for (int j = 0; j < N; ++j)
{
++count; // 执行 N² 次
}
}
for (int k = 0; k < 2 * N; ++k)
{
++count; // 执行 2N 次
}
int M = 10;
while (M--)
{
++count; // 执行 10 次
}
}
基本操作次数:T(N) = N² + 2N + 10
| N 的取值 | T(N) | N² 的占比 |
|---|---|---|
| 10 | 130 | 76.9% |
| 100 | 10210 | 97.9% |
| 1000 | 1002010 | 99.8% |
当 N 不断增大时,低阶项和常数项对结果的影响越来越小 ,因此我们只需要关注最高阶项。
四、大O渐进表示法
4.1 推导规则
| 规则 | 说明 |
|---|---|
| 规则1 | 只保留最高阶项,去掉低阶项 |
| 规则2 | 去除最高阶项的常数系数 |
| 规则3 | 若无 N 相关项,用常数 1 取代所有加法常数 |
4.2 示例汇总
| 函数 | T(N) | 时间复杂度 |
|---|---|---|
| Func1 | N² + 2N + 10 | O(N²) |
| Func2 | 2N + 10 | O(N) |
| Func3 | M + N | O(M+N) |
| Func4 | 100 | O(1) |
五、最好、最坏、平均情况
以 strchr(字符串查找字符)为例:
c
const char* strchr(const char* str, int character)
{
const char* p_begin = str;
while (*p_begin != character)
{
if (*p_begin == '\0')
return NULL;
p_begin++;
}
return p_begin;
}
| 情况 | 执行次数 | 时间复杂度 |
|---|---|---|
| 最好(字符在第一位) | 1 | O(1) |
| 平均(字符在中间) | N/2 | O(N) |
| 最坏(字符在最后/不存在) | N | O(N) |
⚠️ 大O渐进表示法一般关注最坏情况,即算法的上界。
六、经典时间复杂度计算
6.1 冒泡排序
c
void BubbleSort(int* a, int n)
{
for (size_t end = n; end > 0; --end)
{
int exchange = 0;
for (size_t i = 1; i < end; ++i)
{
if (a[i-1] > a[i])
{
Swap(&a[i-1], &a[i]);
exchange = 1;
}
}
if (exchange == 0)
break;
}
}
| 情况 | 执行次数 | 时间复杂度 |
|---|---|---|
| 最好(已有序) | N | O(N) |
| 最坏(降序) | N(N+1)/2 | O(N²) |
6.2 对数阶(二分查找思想)
c
void func5(int n)
{
int cnt = 1;
while (cnt < n)
{
cnt *= 2;
}
}
设执行次数为 x,则 2ˣ = n,因此 x = log₂n
时间复杂度:O(log n)
💡 底数大小对结果影响不大,一般省略不写,统一表示为 O(log n)
6.3 递归函数
c
long long Fac(size_t N)
{
if (0 == N)
return 1;
return Fac(N-1) * N;
}
每次调用 Fac 函数的时间复杂度为 O(1),共递归调用 N 次
时间复杂度:O(N)
七、空间复杂度
7.1 定义
空间复杂度是算法运行过程中额外临时开辟的空间 的数学表达式,计算的是变量的个数而非字节数。
7.2 示例1:冒泡排序
c
void BubbleSort(int* a, int n)
{
// 仅申请了 exchange 等有限个局部变量
// 使用了常数个额外空间
}
空间复杂度:O(1)
7.3 示例2:阶乘递归
c
long long Fac(size_t N)
{
if (N == 0)
return 1;
return Fac(N-1) * N;
}
递归调用了 N 次,额外开辟了 N 个函数栈帧,每个栈帧使用常数个空间
空间复杂度:O(N)
八、常见复杂度对比
| 函数 | 增长速度 | 评价 |
|---|---|---|
| O(1) | 常数阶 | ⭐⭐⭐⭐⭐ 最佳 |
| O(log n) | 对数阶 | ⭐⭐⭐⭐ 优秀 |
| O(n) | 线性阶 | ⭐⭐⭐ 良好 |
| O(n log n) | 线性对数阶 | ⭐⭐⭐ 一般 |
| O(n²) | 平方阶 | ⭐⭐ 较差 |
| O(n³) | 立方阶 | ⭐ 很差 |
| O(2ⁿ) | 指数阶 | ⭐ 极差 |
| O(n!) | 阶乘阶 | 💀 灾难 |
九、学习建议与资源
9.1 两大秘诀
| 秘诀 | 说明 |
|---|---|
| 死磕代码 | 光看不练假把式,多写代码才能形成肌肉记忆 |
| 画图思考 | 数据结构的学习离不开画图,把抽象的逻辑用图形具象化 |
9.2 书籍推荐
| 书籍 | 作者 | 推荐理由 |
|---|---|---|
| 《数据结构》 | 严蔚敏 | C语言版本,各大院校指定教材 |
| 《数据结构与算法分析》 | Weiss | C++版本,内容详尽,代码规整 |
| 《算法导论》 | Thomas H.Cormen | 叙述严谨,内容全面,深入讨论各类算法 |
| 《大话数据结构》 | 程杰 | 趣味易读,算法讲解细致深刻 |
结语
算法复杂度是数据结构与算法的基石,理解它不仅能帮助你在面试中脱颖而出,更能让你在实际开发中做出更优的技术决策。
掌握了大O渐进表示法后,你就可以开始分析各种数据结构和算法的时间/空间效率了。
下一站,线性表------顺序表与链表,我们不见不散!
📌 本文首发于个人技术博客,欢迎交流讨论。如有错误,敬请指正。
写作不易,如果觉得有帮助,欢迎点赞、收藏、转发支持!