时间复杂度与空间复杂度系统梳理与实战
1.1 算法
1.1.1判断算法的效率
我们应该怎么判断算法的效率呢?其实就是程序运行的总时间越短效率就越高。
程序运行的总时间主要和两点有关
1.执行每条语句的耗时
这与硬件强相关。也就是说你的代码(算法)在不同的计算机硬件上运行时,执行同一条指令(语句)所花费的时间是不同的。这条语句本身的耗时,很大程度上取决于底层的硬件处理能力。而且一个好的算法在差硬件上可能表现不如一个坏算法在好硬件上。无法客观反映算法本身的优劣。
例如:
- 算法A 就像是:你一个人,一次搬一块砖。
- 算法B 就像是:你组织了一群人,大家先把所有砖头搬到一个临时集中的地方,然后大家再把这些砖头按大小排好序。
在体力好的时候,可能你一个人搬砖很快(算法A的好硬件),但如果砖头特别多,组织大家分组分批搬运并排序(算法B)可能会更快。但如果力气小(差硬件),你一个人搬砖就很慢,而组织大家排序的过程也变得非常痛苦。
2.每条语句的执行频率
一个程序中的每一条指令,其执行一次所花费的确切时间,确实与计算机的软硬件环境(比如 CPU 的主频、内存速度、编译器的优化程度等)密切相关。然而,在分析算法的效率时,我们并不总能、也不需要知道每条具体指令的确切耗时。
算法分析的重点在于估算“语句的执行次数”,而不是每一条语句的精确执行时间。 为什么?因为一段源码会经过编译、链接等过程,最终转化为机器能够理解和执行的机器码。这个转化过程本身就会影响最终的执行效率。更重要的是,即便是在同一台机器上,代码的细微修改、编译器的微小优化,都可能导致最终执行时间的变化。
因此,我们更关注的是,当问题的规模(例如数据集的大小、要处理的数据项数量) N 变大时,算法中关键语句(或逻辑块)的执行次数会如何变化。 这种执行次数的变化规律,更能够准确且独立于具体硬件和编译环境地反映出算法本身的效率。
1.1.2时间复杂度(渐进时间复杂度)
若是一个m次多项式,则 T(n) = O(
)
在计算算法时间复杂度时,可以忽略所有低次幂和最高次幂的系数,这样可以简化算法分析,也体现了增长率的含义。
随着问题规模n的增大,算法执行时间和增长率和f(n)增长率成正比
那么我们来看一下具体应该怎么计算,以下面这段代码为例:
for(int i=1; i<=n; i++) #频度为 n+1
{for(int j=1; j<=n; j++) #频度为 n*(n+1){c[i][j] = 0; #频度为 n*nfor(int k=1; k<=n; k++) #频度 n*n*(n+1){c[i][j] = c[i][j] + a[i][k] * b[k][j]; #频度为 n*n*n}}
}
i = 1; i <= n; i++ 第一条代码会执行n次,最后一次是执行判断是否继续执行,所以是n+1;下面的代码同理。我画了张图方便理解。
每条代码的执行频度我们算出来了,接下来我们算时间复杂度
如果n为0呢?这个特殊情况引出了一个重要概念:我们需要考虑不同输入情况下的时间复杂度。也就是要有不同的分析视角,当n=0时:f(n) = 1,T(n) = O(1) ,这就属于最好情况时间复杂度。
外层循环条件
i<=0
立即为假只执行1次条件检查就退出
总执行次数f(0)=1
时间复杂度为O(1)(常数时间复杂度)
这个简单的例子告诉我们,算法的效率评估不能只看单一情况。在实际应用中,算法的性能往往会因为输入数据的不同而有很大差异。因此,我们需要从多个角度来全面评估算法的时间复杂度。
三种时间复杂度分析视角
1. 最好情况时间复杂度(Best Case)
算法在最理想输入情况下的时间复杂度
对于我们的矩阵乘法:当n=0时,T(n) = O(1)
但这种情况不具代表性
2. 最坏情况时间复杂度(Worst Case)
算法在最不理想输入情况下的时间复杂度
对于矩阵乘法:随着n增大,T(n) = O(n³)
这是算法分析中最常用的指标,因为它给出了性能保证的上限(通常讨论)
3. 平均情况时间复杂度(Average Case)
算法在所有可能输入情况下的期望时间复杂度
需要知道输入数据的概率分布
对于矩阵乘法:由于执行次数与输入数据无关,平均情况也是O(n³)
了解了时间复杂度的不同分析视角后,我们还需要认识各种常见的时间复杂度类型。就像交通工具有不同的速度等级一样,算法的时间复杂度也有明显的"效率等级"划分。认识这些不同类型的时间复杂度,有助于我们更好地理解和比较不同算法的效率。
常见的时间复杂度分类
时间复杂度按照增长速率可以分为以下几类:
1. 常数时间复杂度 O(1)
执行时间不随输入规模n变化
示例:数组按索引访问、简单的算术运算
我们的矩阵乘法在n=0时就是O(1)
2. 对数时间复杂度 O(log n)
执行时间随n呈对数增长
示例:二分查找、平衡二叉树的搜索
3. 线性时间复杂度 O(n)
执行时间与n成正比
示例:遍历数组、顺序查找
4. 线性对数时间复杂度 O(n log n)
执行时间为n乘以log n
示例:快速排序、归并排序等高效排序算法
5. 平方时间复杂度 O(n²)
执行时间与n的平方成正比
示例:简单的双重循环、冒泡排序
6. 立方时间复杂度 O(n³)
执行时间与n的立方成正比
示例:我们的矩阵乘法、三重嵌套循环
7. 指数时间复杂度 O(2ⁿ)
执行时间随n呈指数增长
示例:穷举搜索、暴力破解
8. 阶乘时间复杂度 O(n!)
执行时间随n呈阶乘增长
示例:旅行商问题的暴力解法
时间复杂度增长趋势比较
为了更直观地理解不同时间复杂度的效率差异,我们来看一个简单的对比:
时间复杂度 | n=10 | n=20 | n=50 | 实用规模 |
---|---|---|---|---|
O(1) | 1 | 1 | 1 | 无限 |
O(log n) | ~3 | ~4 | ~6 | 极大 |
O(n) | 10 | 20 | 50 | 大 |
O(n log n) | ~30 | ~86 | ~282 | 大 |
O(n²) | 100 | 400 | 2500 | 中等 |
O(n³) | 1000 | 8000 | 125K | 小 |
O(2ⁿ) | 1024 | 1M | 1.1亿亿 | 很小 |
O(n!) | 362万 | 2.4亿亿 | 3.0×10⁶⁴ | 极小 |
现在让我们用几段实例代码熟悉一下不同的时间复杂度
计算时间复杂度-常量阶示例
for(int i=0; i<100000; i++) #频度为10001
{x++; #频度为10000s = 0; #频度为10000
}
f(n) = 10001 + 10000 + 10000 = 常数
所以 T(n) = O(1)
不随问题规模n的增长而增长,在算法当中语句频度是个常数,该算法的时间复杂度都是O(1)。
计算时间复杂度-线性阶示例
for(int i=0; i<n; i++) #频度为n+
{x++; #频度为ns = 0; #频度为n
}
f(n) = (n+1)+n+n=3n+1
所以T(n) = O(n)
计算时间复杂度-平方方阶示例
x = 0; #频度为 1
y = 0; #频度为 1
for(int k=1; k<=n; k++) #频度为 n+1
{ x++; #频度为n
}
for(int i=1; i<=n; j++) #频度为 n+1
{for(int j=1; j<=n; j++) #频度为 n*(n+1){y++; #频度为n*n)
}
f(n) = 1+1+(n+1)+n+(n+1)+n*(n+1)+n*n = 2+4n+4
T(n)=O()
计算时间复杂度-立方阶示例
x=1; #频度为 1
for(int i=1; i<=n; i++) #频度为 n
{for(int j=1; j<=i; j++) #频度为 (1+2+3+4+...+n)=n(n+1)/2{for(int k=1; k<=j; j++) #频度为 {x++; #频度为 1+[1+(1+2)]+[1+(1+2)+(1+2+3)]+...+n(n+1)/2=[n(n+1)(n+2)]/6}}
}
我们捋一下
结果为T(n) = O()
计算时间复杂度-对数阶示例
for(int i=1;i<=n; i=i*2)
{x++;s = 0;
}
计算结果:
时间复杂度汇总:
1.1.3 空间复杂度(Space Complexity)
什么是空间复杂度?
空间复杂度衡量的是算法在运行过程中临时占用存储空间的大小随问题规模n的增长趋势。与时间复杂度类似,我们也使用大O表示法来描述空间复杂度。
表达式:S(n) = O(f(n))
为什么需要同时关注空间复杂度?
资源有限性:计算机的内存资源是有限的,特别是在移动设备和嵌入式系统中
性能关联:过多的内存使用可能导致频繁的垃圾回收或页面交换,间接影响时间效率
可扩展性:在大数据时代,算法的空间效率直接影响其处理海量数据的能力
成本考量:内存占用直接影响硬件成本和能耗
空间复杂度的计算原则
空间复杂度主要考虑两个方面:
1. 固定空间(Fixed Space)
包括代码空间、简单变量空间、常量空间等
这些与问题规模n无关,记为O(1)
示例:基本数据类型变量、固定大小的数组
2. 可变空间(Variable Space)
包括动态分配的空间、递归栈空间等
这些与算法执行相关,随n变化而变化
示例:动态数组、递归调用栈
常见空间复杂度分类
与时间复杂度对应,空间复杂度也有类似的等级划分:
空间复杂度 | 特点 | 典型算法 |
---|---|---|
O(1) | 常数空间,不随n变化 | 原地排序、基本运算 |
O(log n) | 对数空间,递归深度为log n | 平衡树遍历、二分递归 |
O(n) | 线性空间,与n成正比 | 数组复制、哈希表 |
O(n²) | 平方空间,与n²成正比 | 邻接矩阵、动态规划表 |
计算空间复杂度-常数空间复杂度
int sum(int n) {int result = 0; // O(1)for (int i = 1; i <= n; i++) { // O(1) for iresult += i; // O(1)}return result; // O(1)
}
总空间复杂度:S(n) = O(1)
计算空间复杂度-线性空间复杂度
int[] fibonacci(int n) {if (n <= 0) return null;int fib[n]; // O(n)fib[0] = 0; fib[1] = 1; // O(1)for (int i = 2; i < n; i++) {fib[i] = fib[i-1] + fib[i-2]; // O(1)}return fib; // O(n)
}
总空间复杂度:S(n) = O(n)
计算空间复杂度-递归算法的空间复杂度
int factorial(int n) {if (n <= 1) return 1; // 递归基return n * factorial(n-1); // 递归深度为n,栈空间O(n)
}
总空间复杂度:S(n) = O(n)(递归栈空间)
时间复杂度 vs 空间复杂度:
在实际算法设计中,我们经常面临时空权衡:
策略 | 特点 | 适用场景 |
---|---|---|
以空间换时间 | 使用更多内存来减少运行时间 | 对响应速度要求高的系统 |
以时间换空间 | 接受更长的运行时间来节省内存 | 内存受限的嵌入式设备 |
平衡策略 | 在时间和空间之间寻找最佳平衡点 | 大多数通用应用程序 |
综合评估算法效率
一个优秀的算法应该综合考虑时间和空间效率:
首先确保正确性:再高效的错误算法也没有价值
分析时间复杂度:关注算法执行速度的渐进趋势
评估空间复杂度:考虑内存使用的合理性和可扩展性
结合实际约束:根据具体应用场景做出权衡决策
总结
通过系统学习时间复杂度和空间复杂度,我们建立了完整的算法效率分析框架:
时间复杂度告诉我们算法执行的速度趋势
空间复杂度告诉我们算法对内存资源的需求
综合评估帮助我们在具体场景中做出最优选择
这种多维度的分析方法将成为我们后续学习各种算法和数据结构的基础,帮助我们设计出既快速又节省资源的优秀算法。
在接下来的学习中,我们将把这个分析框架应用到排序算法、查找算法、图算法等各个领域,逐步构建起完整的算法知识体系。通过理论与实践的结合,我们能够更好地理解算法设计的精髓,为解决实际问题提供有效的工具和方法。