前言:
昨天主要学习了迭代和递归的区别与效率:
我们常用的循环就是迭代,循环执行的过程中不涉及栈帧空间的开销。
而递归调用每调用一次都会使用额外的栈帧空间来存储递归的参数、地址等上下文信息,
经历多次递归后往往需要占用大量栈帧空间,导致效率不如迭代。
今天主要回顾了算法的效率评估的两个方面:时间和空间复杂度:
- 时间效率:算法运行速度的快慢。
- 空间效率:算法占用内存空间的大小。
时间效率-推算方法:
第一步:统计操作数量(基本操作)
- 忽略常数项。因为它们都与 n 无关,所以对时间复杂度不产生影响。
- 省略所有系数。n 前面的系数对时间复杂度没有影响。
- 循环嵌套时使用乘法。总操作数量等于外层循环和内层循环操作数量之积
例1:请分析下列程序段的时间复杂度
void algorithm(int n) {
int a = 1; // +0(变量的初始化不影响复杂度)
a = a + n; // +0
// +n(系数不影响)
for (int i = 0; i < 5 * n + 1; i++) {
printf("%d", 0);
}
// +n*n(嵌套循环取乘积)
for (int i = 0; i < 2 * n; i++) {
for (int j = 0; j < n + 1; j++) {
printf("%d", 0);
}
}
}
答:时间复杂度为O(n^2),执行次数与n有关并且两层嵌套循环取乘积
例2:请分析下列程序段的时间复杂度
// 在某运行平台下
void algorithm(int n) {
int a = 2; // 1 ns
a = a + 1; // 1 ns
a = a * 2; // 10 ns
// 循环 n 次
for (int i = 0; i < n; i++) { // 1 ns ,每轮都要执行 i++
printf("%d", 0); // 5 ns
}
}
答:时间复杂度为O(n),执行次数跟元素n的大小有关
第二步:判断渐近上界(由最高阶决定:“抓大头”)
时间效率-推算方法:
算法在运行过程中使用的内存空间主要包括以下几种:
- 输入空间:用于存储算法的输入数据。(涉及用户自主输入,一般不统计)
- 暂存空间:用于存储算法在运行过程中的变量、对象、函数上下文等数据。(栈帧和指令空间)
- 输出空间:用于存储算法的输出数据。
例:
/* 函数 */
int func() {
// 执行某些操作...
return 0;
}
int algorithm(int n) { // 输入数据(不统计)
const int a = 0; // 暂存数据(常量初始化)
int b = 0; // 暂存数据(变量初始化)
int c = func(); // 栈帧空间(调用函数产生的地址、参数等上下文数据)
return a + b + c; // 输出数据
}
空间效率-推算方法:
和时间效率类似,只是由统计“操作的数量”变为“使用空间的大小”
例1:请分析下列代码片段的空间复杂度
void algorithm(int n) {
int a = 0; // “有限”变量初始化:O(1)
int b[10000]; // “有限”变量初始化:O(1)
if (n > 10)
int nums[n] = {0}; // “未知量”数组初始化:O(n) 因为数组长度取决于n的个数
// 所占空间与n线性相关。类似,时间复杂度上也是:遍历数组的次 // 数与元素个数有关,故时间复杂度也为O(n)
}
在递归函数中,需要统计递归产生的额外栈帧空间:(返回时会释放)
int func() {
// 执行某些操作
return 0;
}
/* 循环 O(1) */
void loop(int n) {
for (int i = 0; i < n; i++) {
func(); // 递归调用时,递归函数每轮返回之后就会释放栈帧空间,因此为O(1)
}
}
/* 递归 O(n) */
void recur(int n) {
if (n == 1) return;
return recur(n - 1); // recur()没有返回,故每“递”一次栈帧空间都在增加,“递”n次:O(n)
// 递归函数 `recur()` 在运行过程中会同时存在n个未返回的 `recur()` ,从而占用O(n)的栈帧空间。
}
小结:
本章节主要学习了时间和空间复杂度的分析方法:时间效率找“基本操作(统计操作熟练)”、空间效率找“使用空间(迭代看循环趟数与n、递归看递归深度和有无返回释放栈帧空间)”,需要我们能够理解分析方法的原理和迭代与递归的工作原理和优缺点及应用场景,要求能够读懂代码、看懂算法、分析利弊,从而帮助我们能够实现算法的最优选择和应用。