算法之时间复杂度和空间复杂度
一,概述
算法(Algorithm)是指用来操作数据、解决程序问题的一组方法。对于同一个问题,使用不同的算法,也许最终得到的结果是一样的。数据结构和算法本生解决的就是“快”和“省”的问题,那就是如何让代码跑得快,还能节省存储空间。好比打造一台法拉利,不仅跑得快还省油,拥有好的算法与数据结构,程序跑得快,还省内存并且长时间运行也不会出故障,就像跑车长时间运行车子也不会出现异常震动,同时还快。
那么我们应该如何去衡量不同算法之间的优劣呢?
主要还是从算法所占用的「时间」和「空间」两个维度去考量。
- 时间维度:是指执行当前算法所消耗的时间,我们通常用「时间复杂度」来描述。
一个算法执行所耗费的时间,从理论上是不能算出来的,必须上机运行测试才能知道。但我们不可能也没有必要对每个算法都上机测试,只需知道哪个算法花费的时间多,哪个算法花费的时间少就可以了。并且一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)。
前面提到的时间频度T(n)中,n称为问题的规模,当n不断变化时,时间频度T(n)也会不断变化。但有时我们想知道它变化时呈现什么规律,为此我们引入时间复杂度的概念。一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数,记作T(n)=O(f(n)),它称为算法的渐进时间复杂度,简称时间复杂度。 - 空间维度:是指执行当前算法需要占用多少内存空间,我们通常用「空间复杂度」来描述。
二,大O表示法
大O表示法O(f(n)中的f(n)的值可以为1、n、logn、n²等,因此我们可以将O(1)、O(n)、O(logn)、O(n²)分别可以称为常数阶、线性阶、对数阶和平方阶,那么如何推导出f(n)的值呢?我们接着来看推导大O阶的方法。
-
推导大O阶
推导大O阶,我们可以按照如下的规则来进行推导,得到的结果就是大O表示法:
1.用常数1来取代运行时间中所有加法常数。
2.修改后的运行次数函数中,只保留最高阶项
3.如果最高阶项存在且不是1,则去除与这个项相乘的常数。
三,时间复杂度分析
- 常见的时间复杂度量级
从小到大依次是:
常数阶(O(1)) < 对数阶(O(logn)) < 线性阶(n) < 线性对数阶(O(nlogn)) < 平方阶(O(n^2)) < 立方阶(O(n^3)) < 阶乘阶(O(n!)) < n次方阶(O(n^n))
-
常数阶
先举了例子,如下所示。
///语句执行1次 let n = 10 ///语句执行1次 let sum = (1 + n)*n/2 ///语句执行1次 print("The sum is :\(sum)") ///总共执行次数:3
上面算法的运行的次数的函数为f(n)= (1 + 1 + 1),根据推导大O阶的规则1(用常数1来取代运行时间中所有加法常数),我们需要将常数3改为1,则这个算法的时间复杂度为O(1)。如果sum = (1+n)*n/2这条语句再执行10遍,因为这与问题大小n的值并没有关系,所以这个算法的时间复杂度仍旧是O(1),我们可以称之为常数阶。
-
线性阶
线性阶主要要分析循环结构的运行情况,如下所示。
///语句执行1次 var i = 0 ///语句执行n次 while i < n { ///语句执行n次 i+=1 ///语句执行n次 print("Current i is \(i)") } ///总执行数:3n+1 = 1 + n + n + n
上面算法的运行的次数的函数为 f(n)= 3n + 1次,根据规则 2(修改后的运行次数函数中,只保留最高阶项) 和 规则3(如果最高阶项存在且不是1,则去除与这个项相乘的常数。),因此该算法的时间复杂度是O(n)。
-
对数阶
接着看如下代码:
let number = 1; // 语句执行一次 while (number < n) { // 语句执行log(n)次 number *= 2; // 语句执行log(n)次 }
///总执行数:2log(n)+1 = 1 + log(n) + log(n)上面算法的运行的次数的函数为 f(n)= (2log(n) + 1)次,number每次都放大两倍,我们假设这个循环体执行了m次,那么
2^m = n
即m = log(n)
,所以整段代码执行次数为1 + 2*logn,则f(n) = log(n)
,时间复杂度为O(log(n))。
-
平方阶
下面的代码是循环嵌套:
/// 语句执行n次 for (let i = 0; i < n; i++) { ///语句执行n^2次 for (let j = 0; j < n; j++) { ///语句执行n^2 print('I am here!') } }
上面算法的运行的次数的函数为 f(n)= (2*n^2 + n)次, 内层循环的时间复杂度在讲到线性阶时就已经得知是O(n),现在经过外层循环n次,那么这段算法的时间复杂度则为O(n²)。
四,空复杂度分析
时间复杂度的全称是渐进时间复杂度,表示算法的执行时间与数据规模之间的增长关系。类比一下,空间复杂度全称就是渐进空间复杂度,表示算法的存储空间与数据规模之间的增长关系。
void print(int n) {
int i = 0;
int[] a = new int[n];
for (i; i <n; ++i) {
a[i] = i * i;
}
}
跟时间复杂度分析一样,我们可以看到,第 2 行代码中,我们申请了一个空间存储变量 i,但是它是常量阶的,跟数据规模 n 没有关系,所以我们可以忽略。第 3 行申请了一个大小为 n 的 int 类型数组,除此之外,剩下的代码都没有占用更多的空间,所以整段代码的空间复杂度就是 O(n)。
打个不恰当的比喻,就像我们的手机现在工艺越来越好,手机也越来越薄。占用体积越来越小。也就是用更好的模具设计放置零件,而模具就像是空间复杂度更小的体积容纳更多的原件。
我们常见的空间复杂度就是 O(1)、O(n)、O(n2 ),像 O(logn)、O(nlogn) 这样的对数阶复杂度平时都用不到。而且,空间复杂度分析比时间复杂度分析要简单很多。所以,对于空间复杂度,掌握刚我说的这些内容已经足够了。
-
空间复杂度 O(1)
如果算法执行所需要的临时空间不随着某个变量n的大小而变化,即此算法空间复杂度为一个常量,可表示为 O(1)。
int i = 1; int j = 2; ++i; j++; int m = i + j;
代码中的 i、j、m 所分配的空间都不随着处理数据量变化,因此它的空间复杂度 S(n) = O(1)。
-
空间复杂度 O(n)
int[] m = new int[n] for(i=1; i <= n; ++i) { j = i; j++; }
这段代码中,第一行new了一个数组出来,这个数据占用的大小为n,后面虽然有循环,但没有再分配新的空间,因此,这段代码的空间复杂度主要看第一行即可,即 S(n) = O(n)。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)