算法复杂度
算法复杂度旨在描述 输入数据量 时,算法的 时间使用 和 空间使用 情况
-
时间:假设各操作的运行时间为固定常数,统计算法运行的 计算操作的数量 ,以代表算法运行所需时间
-
空间:统计在最差情况下 ,算法运行所需使用的 最大空间
输入数据大小 指算法处理的输入数据量,根据不同算法,具有不同的定义,例如:
- 排序算法: 代表需要排序的元素数量
- 搜索算法: 代表搜索范围的元素总数,例如数组大小、矩阵大小、二叉树节点数、图节点和边数等
时间复杂度
统计的是算法的 计算操作数量 ,而不是 运行的绝对时间
计算操作数量 和 运行绝对时间 呈正相关关系,并不相等。算法运行时间受到「编程语言 、计算机处理器速度、运行环境」等多种因素影响。
体现的是计算操作随数据大小 变化时的变化情况。假设算法运行总共需要 次操作、 次操作,此两情况的时间复杂度都为常数级 ;需要 次操作、 次操作 的时间复杂度都为
符号表示
时间复杂度具有 最差、平均、最佳 三种情况,分别使用 , , 三种符号表示
题目:输入长度为 N
的整数数组 nums
,判断此数组中是否有数字 7
,若有则返回 true
,否则返回 false
解题算法: 线性查找,即遍历整个数组,遇到 7
则返回 true
代码:
bool findSeven(vector<int>& nums) { |
最佳情况 :当数组首个数字为 7
时,无论 nums
有多少元素,线性查找的循环次数都为 次
最差情况 :nums
中所有数字都不为 7
,此时线性查找会遍历整个数组,循环 N
次
平均情况 :需要考虑输入数据的分布情况,计算所有数据情况下的平均时间复杂度。例如本题,需要考虑数组长度、数组元素的取值范围等
常见种类
根据从小到大排列,常见的算法时间复杂度主要有:
示例解析
对于以下所有示例,设输入数据大小为 ,计算操作数量为 。图中每个「蓝色方块」代表一个单元计算操作。
常数
运行次数与 大小呈常数关系,即不随输入数据大小 的变化而变化
int algorithm(int N) { |
对于以下代码,无论 取多大,都与输入数据大小 无关,因此时间复杂度仍为
int algorithm(int N) { |
线性
循环运行次数与 大小呈线性关系,时间复杂度为
int algorithm(int N) { |
之前一个例子的循环次数并不随 改变,而这里的循环次数会随 变化
对于以下代码,虽然是两层循环,但第二层与 大小无关,因此整体仍与 呈线性关系
int algorithm(int N) { |
平方
两层循环相互独立,都与 呈线性关系,因此总体与 呈平方关系
int algorithm(int N) { |
以 冒泡排序 为例,其包含两层独立循环:
- 第一层复杂度为 ;
- 第二层平均循环次数为 ,复杂度为 ,推导过程如下:
因此,冒泡排序的总体时间复杂度为 ,代码如下所示
vector<int> bubbleSort(vector<int>& nums) { |
指数
生物学科中的 “细胞分裂” 即是指数级增长
算法中,指数阶常出现于 递归 ,算法原理图与代码如下所示
int algorithm(int N) { |
阶乘
阶乘阶对应数学上常见的 “全排列”
如下图与代码所示,阶乘常使用 递归 实现,算法原理:第一层分裂出 个,第二层分裂出 个,…… ,直至到第 层时终止并回溯
int algorithm(int N) { |
对数
对数阶与指数阶相反,指数阶为 “每轮分裂出两倍的情况” ,而对数阶是 “每轮排除一半的情况” 。对数阶常出现于 二分法 、分治 等算法中,体现着 “一分为二” 或 “一分为多” 的算法思想
设循环次数为 ,则输入数据大小 与 呈线性关系,两边同时取 对数,则得到循环次数 与 呈线性关系,即时间复杂度为
int algorithm(int N) { |
如以下代码所示,对于不同 的取值,循环次数 与 呈线性关系,时间复杂度为 。无论底数 a 取值,时间复杂度都可以记作 :
int algorithm(int N) { |
如下图所示,为二分查找的时间复杂度示意图,每次二分将搜索区间缩小一半,二分的次数为
线性对数
两层循环相互独立,第一层和第二层时间复杂度分别为 和 ,则总体时间复杂度为
int algorithm(int N) { |
线性对数阶常出现于排序算法,例如 快速排序 、归并排序 、堆排序 等,其时间复杂度原理如下图所示
空间复杂度
空间复杂度涉及的空间类型有:
-
输入空间 : 存储输入数据所需的空间大小
-
暂存空间 : 算法运行过程中,存储所有中间变量和对象等数据所需的空间大小
-
输出空间 : 算法运行返回时,存储输出数据所需的空间大小
通常情况下,空间复杂度指在输入数据大小为 时,算法运行所使用的 暂存空间 + 输出空间 的总体大小
根据不同来源,算法使用的内存空间分为三类:
-
指令空间 :编译后,程序指令所使用的内存空间
-
数据空间 :算法中的各项变量使用的空间,包括:声明的常量、变量、动态数组、动态对象等使用的内存空间
struct Node {
int val;
Node *next;
Node(int x) : val(x), next(NULL) {}
};
void algorithm(int N) {
int num = N; // 变量
int nums[N]; // 动态数组
Node* node = new Node(N); // 动态对象
}```
3. **栈帧空间** :程序 **调用函数** 是基于 **栈** 实现的,函数在调用期间,占用常量大小的栈帧空间,直至返回后释放。如以下代码所示,在循环中调用函数,每轮调用 `test()` 返回后,栈帧空间已被释放,因此空间复杂度仍为 $O(1)$
```cpp
int test() {
return 0;
}
void algorithm(int N) {
for (int i = 0; i < N; i++) {
test();
}
}算法中,栈帧空间的累计常出现于递归调用 。如以下代码所示,通过递归调用,会同时存在 个未返回的函数
algorithm()
,此时累计使用 大小的栈帧空间int algorithm(int N) {
if (N <= 1) return 1;
return algorithm(N - 1) + 1;
}
符号表示
通常情况下,空间复杂度统计 算法在 “最差情况” 下使用的空间大小 ,以体现算法运行所需预留的空间量,使用符号 表示
最差情况有两层含义,分别为 最差输入数据 、算法运行中的 最差运行点
例如以下代码:输入整数 ,取值范围
void algorithm(int N) { |
- 最差输入数据 : 当 时,数组
nums
的长度恒定为 ,空间复杂度为 。当 时,数组nums
长度为 ,空间复杂度为 。因此,空间复杂度应为最差输入数据情况下的 - 最差运行点 : 在执行
nums(10)
时,算法仅使用 大小的空间;而当执行nums.resize(N)
时,算法使用 的空间;因此,空间复杂度应为最差运行点的
常见种类
根据从小到大排列,常见的算法空间复杂度有:
示例解析
对于以下所有示例,设输入数据大小为正整数 ,节点类 Node
、函数 test()
如以下代码所示
// 节点类 Node |
常数
普通常量、变量、对象、元素数量与输入数据大小 无关的集合,皆使用常数大小的空间
void algorithm(int N) { |
如以下代码所示,虽然函数 test()
调用了 次,但每轮调用后 test()
已返回,无累计栈帧空间使用,因此空间复杂度仍为
void algorithm(int N) { |
线性
元素数量与 呈线性关系的任意类型集合(常见于 一维数组 、链表 、哈希表 等),皆使用线性大小的空间
void algorithm(int N) { |
如下图与代码所示,此递归调用期间,会同时存在 个未返回的 algorithm()
函数,因此使用 大小的栈帧空间
int algorithm(int N) { |
平方
元素数量与 呈平方关系的任意类型集合(常见于矩阵),皆使用平方大小的空间
void algorithm(int N) { |
如下图与代码所示,递归调用时同时存在 个未返回的 algorithm()
函数,使用 栈帧空间;每层递归函数中声明了数组,平均长度为 ,使用 空间;因此总体空间复杂度为
int algorithm(int N) { |
指数
指数阶常见于 二叉树 、多叉树
例如,高度为 的 满二叉树(perfect binary tree) 的节点数量为 ,占用 大小的空间;同理,高度为 的 满 叉树 的节点数量为 ,占用 大小的空间
对数
对数阶常出现于分治算法的栈帧空间累计、数据类型转换等,例如:
- 快速排序 ,平均空间复杂度为 ,最差空间复杂度为 。拓展知识:通过应用 Tail Call Optimization ,可以将快速排序的最差空间复杂度限定至
- 数字转化为字符串 ,设某正整数为 ,则字符串的空间复杂度为 。推导如下:正整数 的位数为 ,即转化的字符串长度为 ,因此空间复杂度为
时空权衡
优良的算法应具备两个特性,即时间和空间复杂度皆较低
实际上,对于某个算法问题,同时优化时间复杂度和空间复杂度是非常困难的。降低时间复杂度,往往是以提升空间复杂度为代价的,反之亦然
由于当代计算机的内存充足,通常情况下,算法设计中一般会采取「空间换时间」的做法,即 牺牲部分计算机存储空间,来提升算法的运行速度
以 LeetCode 1. 两数之和 为例,暴力枚举 和 辅助哈希表 分别为 空间最优 和 时间最优 的两种算法
注:以上内容来自于 https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/r8ytog/