1. 数据结构的介绍
一、核心基础概念
1.1 数据结构
定义 :计算机存储、组织数据的方式 ,是相互间存在特定关系的数据元素集合。常见类型:线性表、树、图、哈希表等。
1.2 算法
定义 :定义良好的计算过程,接收输入值,经过计算步骤 ,输出结果,是输入到输出的转化逻辑。
1.3 数据结构 & 算法的求职重要性
是校招笔试、面试必考核心内容,列举了多家大厂真实考察场景:
- 笔试:美团、科大讯飞等笔试包含编程题,直接考察算法实现;
- 面试:深信服、腾讯、猎豹科技等面试高频提问:数组与链表区别、时间复杂度、堆、归并排序、手写算法(全排列、最长公共子数组等)、底层原理实现等。
二、学习方法(2 个核心秘诀)
- 死磕代码:必须手写代码实现,不能只看理论;
- 画图 + 思考:用画图具象化数据结构逻辑,深度理解原理。
三、经典书籍推荐
| 书籍名称 | 版本 / 特点 | 推荐理由 |
|---|---|---|
| 《数据结构》严蔚敏 | C 语言版 | 内容详尽、代码规范,高校指定经典教材 |
| 《数据结构》殷人昆 | C++ 版 | 面向对象讲解,适配 C++ 开发,代码规整 |
| 《算法导论》托马斯・科尔曼 | 经典原版 | 叙述严谨,系统全面,深入讲解各类算法原理 |
| 《大话数据结构》程杰 | 通俗趣味版 | 语言易懂,算法讲解生动,适合入门 |
2. 算法效率
一、案例引入:旋转数组(LeetCode)
1. 基础代码(逐位后移)
void rotate(int* nums, int numsSize, int k) {
while(k--)
{
int end = nums[numsSize-1];
for(int i = numsSize - 1;i > 0 ;i--)
{
nums[i] = nums[i-1];
}
nums[0] = end;
}
}

2. 问题核心
代码局部运行可通过,但无法通过平台提交 ,本质是算法效率过低,因此需要用复杂度衡量算法好坏。
二、复杂度的概念
算法运行 会消耗时间资源和空间资源,因此从两个维度评判:
- 时间复杂度:衡量算法运行快慢
- 空间复杂度:衡量算法运行所需额外内存
💡 行业现状:早期硬件存储有限,空间复杂度很受重视;如今硬件性能提升,时间复杂度成为核心评判标准。
三、复杂度的重要性
复杂度是大厂校招笔试、面试必考知识点,图片给出真实面试案例:
- 腾讯 C++ 后台实习一面 :直接考察「快排、堆排序时间复杂度及最坏情况推导」
- 剑指 Offer 真题 :要求算法满足时间复杂度 O (n)、空间复杂度 O (1)
- 校招规律:笔试算法题、技术面试均会考察复杂度计算与原理理解
3. 时间复杂度基础定义
一、核心概念
- 核心概念 时间复杂度用函数式
T(N)定量描述算法运行效率,不直接统计真实运行时间,原因:
- 运行时间受编译器、机器配置影响,不通用
- 需提前理论评估,无法等代码写完测试
- 用代码执行次数代替真实时间,和运行时间成正比
- 程序执行的时间 = 二进制指令运行的时间 (cpu处理二进制指令非常快可以近似认为是一样的) * 执行次数
本质 :计算程序执行次数的函数,只对比增长量级,而非精确次数。
二、大 O 渐进表示法(核心推导规则)
用于简化时间复杂度,只关注数据量大时的最坏增长趋势 ,3 条规则:
- 只保留最高阶项,去掉低阶项 (N 极大时低阶影响可忽略)
- 去掉最高阶项 的常数系数
- 无 N 相关项、只有常数时 ,统一记为 O
(1)
三、时间复杂度计算示例(高频考点)
示例 1 Func1
#include <stdio.h>
// Func1 完整代码
void Func1(int N)
{
int count = 0;
// 第一层:双层嵌套循环 -> 执行 N*N 次
for (int i = 0; i < N; ++i)
{
for (int j = 0; j < N; ++j)
{
++count;
}
}
// 第二层:单层循环 -> 执行 2*N 次
for (int k = 0; k < 2 * N; ++k)
{
++count;
}
// 第三层:固定循环 -> 执行 10 次
int M = 10;
while (M--)
{
++count;
}
// 输出最终执行次数
printf("N = %d 时,总执行次数 count = %d\n", N, count);
}
// 主函数:测试运行
int main()
{
// 测试 N = 10
Func1(10);
return 0;
}
1. 双层嵌套循环
for (int i = 0; i < N; ++i)
for (int j = 0; j < N; ++j)
++count;
- 外层循环跑 N 次
- 内层循环跑 N 次
- 总次数:N × N = N²
2. 单层循环
for (int k = 0; k < 2 * N; ++k)
++count;
- 循环跑 2N 次
- 总次数:2N
3. 固定次数循环
int M = 10;
while (M--)
++count;
- 固定跑 10 次,和 N 无关
- 总次数:10
总执行次数公式
T(N) = N² + 2N + 10
时间复杂度为: O(N^2)
示例 2 Func2
void Func2(int N)
{
int count = 0;
for (int k = 0; k < 2 * N ; ++k)
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
}
- 循环 1:执行**
2N次** - 循环 2:固定执行**
10次** - 总次数:
T(N)=2N+10 - 时间复杂度:O (N) (去掉常数、系数,只保留最高阶)
示例 2 Func3
void Func3(int N, int M)
{
int count = 0;
for (int k = 0; k < M; ++k) ++count;
for (int k = 0; k < N ; ++k) ++count;
}
- 总次数:
T(N)=M+N - 时间复杂度:O (N+M) (两个未知量都保留)
示例 3 Func4
void Func4(int N)
{
int count = 0;
for (int k = 0; k < 100; ++k) ++count;
}
- 循环固定执行 100 次,和 N 无关
- 时间复杂度:O (1) (常数复杂度)
示例 4 strchr(查找字符)
cpp
#计算strchr的时间复杂度?
const char *strchr(const char* str,char character)
{
const char* p_begin =s;
while(*p_begin !=character)
{
if(*p_begin='\0')
return NULL;
p_begin++ ;
}
return p_begin;
}
- 最好情况:字符在第 1 位 →O
(1) - 最坏情况:字符在最后 / 不存在 →O
(N) - 平均情况:O
(N)
大 O 关注最坏情况 ,所以一般写 O (N)
示例 5 冒泡排序 BubbleSort
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]);
}
}
}

- 有序最好情况:O**
(N)** - 逆序最坏情况:
T(N)=n(n-1)/2 - 时间复杂度:O (N²)
示例 6 func5(对数复杂度)
void func5(int n)
{
int cnt = 1;
while (cnt < n)
{
cnt *= 2;
}
}
- 循环条件:
2^x = n→x = log₂n - 时间复杂度:O (logN) (底数省略)(当n趋于无穷大时 底数的大小对结果影响不大)
示例 7 阶乘递归 Fac
long long Fac(size_t N)
{
if(0 == N) return 1;
return Fac(N-1)*N;
}
- 递归调用 N 次
- 时间复杂度:O (N)
计算递归算法的时间复杂度 =单词递归的时间复杂度 * 递归次数
核心总结(必背)
- 只看最高阶项 ,去掉常数、系数
- 单层循环 →O (N) ;嵌套循环 →O (N²)
- 每次翻倍 / 折半 →O (logN)
- 固定次数 →O (1)
- 大 O 默认看最坏情况
4. 空间复杂度
一、核心概念📌
空间复杂度 用于衡量算法运行时额外临时开辟的存储空间 规模,采用大 O 渐进表示法 ,和时间复杂度计算规则类似。
注意:
空间复杂度不是程序占⽤了多少bytes的空间,因为常规情况每个对象⼤⼩差异不会很⼤,所以空间复杂度算的是变量的个数。
- 不计入:函数栈帧中编译期就确定的空间(参数、局部变量、寄存器信息)。
- 只计算:运行时显式申请 的额外空间(动态数组、递归调用栈等)。
二、计算示例解析
示例 1:冒泡排序 BubbleSort
void BubbleSort(int* a, int n)
{
assert(a);
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;
}
}
- 额外空间:仅**
exchange,end,i** 等常数个局部变量 ,空间不随n变化。(数组是传入的数据不算入) - 空间复杂度:O(1)(常数空间)
示例 2:递归阶乘 Fac
long long Fac(size_t N)
{
if(N == 0)
return 1;
return Fac(N-1)*N;
}
- 额外空间:递归调用
N次 ,产生N 个函数调用栈帧,栈帧数量随N线性增长。 - 空间复杂度:O(N)(线性空间)
5.常见复杂度的对比


时间复杂度从慢到快:
O(1) < O(logn) < O(n) < O(nlogn) < O(n²) < O(n³) < O(2ⁿ) < O(n!)
复杂度越大 程序运行越慢 运行效率越低 程序设计差
6. 旋转数组 3 种思路完整解析(回归算法效率的探究)
题目要求
给定数组,将数组元素向右旋转 k 步 (例:[1,2,3,4,5,6,7],k=3 → [5,6,7,1,2,3,4])
思路 1:暴力右移(每次移 1 位,循环 k 次)
复杂度太高 效率低下
-
时间:O(N^2 ),最坏 k=n 时,双层循环
-
空间:O(1)只有temp i 两个变量
-
缺点:数据量大时超时,LeetCode 无法通过
void rotate(int* nums, int numsSize, int k) {
while(k--)
{
int end = nums[numsSize-1];
for(int i = numsSize - 1;i > 0 ;i--)
{
nums[i] = nums[i-1];
}
nums[0] = end;
}
}
思路 2:额外数组(空间换时间)(将后k个数据放到前k个位置)
复杂度适当大 效率相当
-
时间:O(N) ,遍历 2 次数组
-
空间:O(N) ,需要新数组存储数据
-
优点:逻辑简单,不会超时;缺点占用额外内存
void rotate(int* nums, int numsSize, int k)
{
int newArr[numsSize];
for (int i = 0; i < numsSize; ++i)
{
newArr[(i + k) % numsSize] = nums[i];
}
for (int i = 0; i < numsSize; ++i)
{
nums[i] = newArr[i];
}
}
思路 3:三次反转(最优解法,原地操作)
复杂度低 效率高
核心逻辑(示例:1234567,k=3)
- 反转前**
n−k** 个:4321567 - 反转后**
k** 个:4321765 - 整体反转:
5671234(最终结果)
- 先取模:
k = k%numsSize,避免 k 大于数组长度
复杂度
-
时间:O(N)
-
空间:O(1),原地修改,最优解
void reverse(int* nums,int begin,int end)
{
while(begin<end){
int tmp = nums[begin];
nums[begin] = nums[end];
nums[end] = tmp;
begin++;
end--;
}
}
void rotate(int* nums, int numsSize, int k)
{
k = k%numsSize;
reverse(nums,0,numsSize-k-1);
reverse(nums,numsSize-k,numsSize-1);
reverse(nums,0,numsSize-1);
}
最优方案对比
| 思路 | 时间复杂度 | 空间复杂度 | 推荐度 |
|---|---|---|---|
| 暴力右移 | O(N^2) | O(1) | ⭐ |
| 额外数组 | O(N) | O(N) | ⭐⭐⭐ |
| 三次反转 | O(N) | O(1) | ⭐⭐⭐⭐⭐ |