…

文章目录
复杂度是衡量一个算法好坏的标准,可以从 时间 和 空间 两个维度进行比较。可能你之前听说某个算法的时间复杂度是O(N),空间复杂度是O(1),知道这是一个还不错的算法,那么你知道这些复杂度是如何计算出来的吗?本文将会揭开它们神秘的面纱,让你拥有一把衡量算法好坏的度量衡。
先说结论
- 时间复杂度主要是
衡量一个算法的运行快慢,通常由循环决定- 空间复杂度主要是
衡量一个算法运行所需的额外空间,通常由开辟的空间大小决定
常见错误理解
- 时间复杂度是就是一段代码实际运行运行所花的时间。这种理解是错误的,因为环境的不同会导致代码运行的快慢,比如将一个大型程序放在你电脑上运行,和放在神威·太湖之光上运行所花的时间是肯定不同的,为了统一评判,我们将
算法中基本操作的执行次数,称为算法的时间复杂度- 空间复杂度和代码长度有关,代码码越长越复杂。错误,假如我们只创建了常数个变量,纵使代码写的再长,这个算法的空间复杂度也是O(1),在
程序中创建的变量个数(在内存中申请的空间大小),称为空间复杂度,创建的变量数越多,算法所占空间就越复杂
当然这只是最基本的知识,关于时间&空间复杂度的更多知识可以往下看
在计算机科学中,算法的时间复杂度是一个函数,它定量地描述了该算法的运行时间
同大多数读者一样,我也不喜欢冗长复杂的官方解释,通俗来说,算法中基本操作的执行次数(循环部分),就是代表了该算法的时间复杂度,比如下面这段代码
//请问这段代码的时间复杂度是多少?
int main()
{
int N = 0;
scanf("%d", &N);
int count = 0;
int i = 0;
for (i = 0; i < N; i++)
{
count++;
}
return 0;
}
直接看循环部分,可以看到这个算法会循环N次,N是可变的,因此这个算法的时间复杂度就是N,简单吧,当然这只是一个最简单的例子,真实的程序循环比这复杂得多,此时就需要一个工具:大O渐进表示法,来帮助我们计算出算法的时间复杂度
大O符号:是用来描述函数渐进行为的数学符号,这个符号有点像数学中取极限
大O渐进表示法 的推导步骤:
2N ^ 2 + 2N + 100,需要去掉常数项1002N ^ 2 + 2N,显而易见 2N ^ 2 要大于 2N,2N ^ 2就是这里的最高阶项2N ^ 2 ,常数项 2 对整体时间复杂度影响是不大的,应该去除以上就是通过 大O渐进表示法 求时间复杂度的步骤,当然示例中的时间复杂度最终为O(N ^ 2)
大O渐进表示法 这样表示,是否合理呢?
答:很合理,尤其是放在计算机上使用
首先假设存在这么一个时间复杂度(不用
大O渐进表示法版): F(N) = N^2 + 2 * N + 10
经过大O渐进表示法处理后,变成 F(N) = O(N^2),让我们来测试一组数据:
N F(N) = N^2+2*N+10 F(N) = O(N^2) 相差率 10 100+20+10 = 130 100 23% 100 10000+200+10 = 10210 10000 2% 10000 100020010 100000000 0.02% 显然,随着数据的不断增大,二者间的差距会越来越小,而经过
大O渐进表示法计算后的时间复杂度,是更容易计算的,除非追求精确的数据,否则用大O渐进表示法是很合理的~
大O渐进表示法的核心作用就是去除那些对结果影响不大的项
时间复杂度这一块有几个比较经典的题目需要掌握一下,学会使用 大O渐进表示法 求出时间复杂度
// 计算Func1的时间复杂度?
void Func1(int N, int M)
{
int count = 0;
for (int k = 0; k < M; ++k)
{
++count;
}
for (int k = 0; k < N; ++k)
{
++count;
}
printf("%d\n", count);
}
答案:
O(N + M)
因为这里有两次循环,并且N和M都是未知数,无法区分出谁是最高阶项,因此两个都取出,都没有带常数项,不做去除操作。综上Func1的时间复杂度就是O(N + M)
// 计算Func2的时间复杂度?
void Func2(int N)
{
int count = 0;
for (int k = 0; k < 100; ++k)
{
++count;
}
printf("%d\n", count);
}
答案:
O(1)
显然,这里需要循环100次,都是常数项,直接遵循推导步骤一,时间复杂度为O(1)
这里只循环了100次,即使循环1k次、1w次,也都是常数项,一样是O(1)
//计算strchr的时间复杂度?
const char* strchr(const char* str, int character);
答案:
O(N)
说明:strchr是一个字符串寻找函数,作用是在字符串str中查找目标字符character
有三种情况:
- 最好的情况,只找一次,此时的时间复杂度为
O(1)- 最坏的情况,没有
目标字符,需要把整个字符串找一遍,时间复杂度为O(N)- 平均的情况,在中间就找到了,时间复杂度是
O(N / 2)面对这种多分支情况,我们要做预期管理,用最悲观的态度来判断程序,这样做的好处是预期值低,结果出来时不会有很大落差,生活中也可以像这样,做好准备。言归正传,这里选择最坏的情况,即
O(N),当然这种情况比较特殊,值得注意一下
//计算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;
}
}
答案:
O(N ^ 2)
冒泡排序是一个神奇的算法,每次冒泡比较的趟数都不同,可以这样推导
- 第一趟:比较 N - 1 次
- 第二趟:比较 N - 2 次
- 第三趟:比较 N - 3 次
- …………
- 最后一趟: 比较 1 次
总共比较的次数,就是时间复杂度,即 (N - 1) + (N - 2) + …… + 1,显然这是一个首项为 N - 1,尾项为 1 的等差数列,并且共有 N - 1 项,把高中学的知识用起来,N - 1 项和为
(N - 1) * N / 2,通过大O渐进表示法进行计算,最终结果为O(N ^ 2)
//计算BinarySearch的时间复杂度?
int BinarySearch(int* a, int n, int x)
{
assert(a);
int begin = 0;
int end = n - 1;
// [begin, end]:begin和end是左闭右闭区间,因此有=号
while (begin <= end)
{
int mid = begin + ((end - begin) >> 1);
if (a[mid] < x)
begin = mid + 1;
else if (a[mid] > x)
end = mid - 1;
else
return mid;
}
return -1;
}
答案:
O(logN)
折半查找,一个站在巨人肩膀上的算法,假如我们想在世界范围内找一个人(假设有70亿人,且数据已排序),通过二分查找,最多只需要查找33次,就能找出这个人,厉害吧?下面是二分查找的推导过程
假设需要查找的次数为 k 次,那么可以这样写
- N / 2 -> N / 2 ^ 1
- N / 4 -> N / 2 ^ 2
- N / 8 -> N / 2 ^ 3
- …………
其中,左边的序号就是查找的次数 k ,可得出式子
N = 2 ^ k,稍微变换下,得到k = logN,其中第二个式子就是二分查找的时间复杂度,可能细心的人能发现,我没有写底数2,不是不写,而是不好写,除非文本编辑器支持插入数学式,否则这个是很难表示的,鉴于这个东西应用的广泛性,有这样一个规定:在底数为2时,可以不写底数;如果底数为其他数,就需要写出来,其他底数都很少见的。 在有的地方,会把lgN看作logN,第一种方法是有歧义的,和以10为底的对数表示形式一致,这是不太好的,但如果我们看到了,要明白lgN也是一个以2为底的对数
二分查找为何厉害?因为二分查找在计算时,每次都是对半查找,即2 ^ k,是一个指数爆炸级查找,因此很快就能找到目标数,听说过一张纸对折64次就能碰到月球的故事吧?指数爆炸是个很庞大的数据
//计算斐波那契递归Fib的时间复杂度?
long long Fib(size_t N)
{
if (N < 3)
return 1;
return Fib(N - 1) + Fib(N - 2);
}
答案:
O(2 ^ N)
递归本来就是一个很麻烦的东西,更何况这是计算斐波那契数列
- 递归中的时间复杂度,计算的是每次递归中执行次数的累加
我们可以将递归斐波那契数列水平展开,即
1+2+4+8+16+32+……+2^N
根据大O渐进表示法,去除影响小的常数项,最终结果为O(2 ^ N)
10.24更正
说O(2 ^ N)是斐波那契数列的时间复杂度有些不准确,因为将斐波那契数列递归求值展开成一颗二叉树后,会发现这是一颗普通二叉树,部分节点有缺失,O(2 ^ N)用来描述满二叉树合理些。斐波那契数列的详细时间复杂度为O(1.618 ^ N),更详细的是一个无理数的指数级,想了解的可以看看这篇文章 递归求解斐波那契数列的时间复杂度问题
相信你看完这些经典例题后,能对 大O渐进表示法 有一个新的认识,加油,你会越来越强的!
算法的空间复杂度是指临时占用储存空间大小的量度
简单理解,空间复杂度是算法中变量的个数(申请的空间大小)。因为变量在使用前,要先声明,而声明会在内存中开辟空间,无论是在堆上还是栈上,都会造成内存损耗,损耗越大,空间复杂度就越高 ,先看代码:
//空间复杂度
int main()
{
int a = 1;
int b = 2;
int c = 3;
return 0;
}
看变量个数,有a、b、c三个变量,属于常数次,所以这个程序的空间复杂度是 O(1),空间复杂度也遵循大O渐进表示法,这里就不再介绍了,忘记了的同学可以往上翻翻
递归除外,递归会建立额外的函数栈帧
O(1) 或 O(N)眼看千遍,不如手过一遍,下面跟着我一起来看看试题,练练手吧!
//计算Fibonacci的空间复杂度?
//返回斐波那契数列的前n项
long long* Fibonacci(size_t n)
{
if (n == 0)
return NULL;
long long* fibArray = (long long*)malloc((n + 1) * sizeof(long long));
fibArray[0] = 0;
fibArray[1] = 1;
for (int i = 2; i <= n; ++i)
{
fibArray[i] = fibArray[i - 1] + fibArray[i - 2];
}
return fibArray;
}
答案:
O(N)
先看题目,这是一个迭代实现的斐波那契数列,没有使用递归,直接看看创建的变量个数:
- fibArray
- (n+1) * sizeof(long long)
- i
共计申请了三块空间,其中第二块空间最大,为最高阶项,即
n + 1,去除常数项,最终结果为O(N)
//计算阶乘递归Fac的空间复杂度?
long long Fac(size_t N)
{
if (N == 0)
return 1;
return Fac(N - 1) * N;
}
答案:
O(N)
递归的规则是先递出,再回归,如果中途遇到递归,继续递出,因此在计算递归的空间复杂度时,计算的是每次递归调用的变量个数相加(所开辟的空间),也可以看作递归的深度
显然这里的递归深度是N,开辟了N个栈帧,每个栈帧使用了常数个空间,空间复杂度自然就是O(N)了
你学会了吗?是不是感觉空间复杂度要比时间复杂度简单些?毕竟现在是大容量时代,内存都变得不值钱了,于是对空间的要求自然而然的变低了。
一般算法的常见复杂度类型如图所示

二分查找 logN 是真的强!
有些题目中,对时间复杂度和空间复杂度是有要求的,比如下面这两个简单题,用来练手就很不错,动起来吧!关于这两题的题解,后续会发布的
10.25更新
两道题目的题解文章已发布,欢迎查看
C语言题解 | 消失的数字 &轮转数组
以上就是本次关于时间复杂度和空间复杂度的全部内容了,作为数据结构中的第一课,算是比较偏向于理论的部分,学起来也还比较简单,开胃菜嘛,等后面手撕顺序表、链表、二叉树就爽了
如果你觉得本文写的还不错的话,期待留下一个小小的赞👍,你的支持是我分享的最大动力!
如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正

…
相关文章推荐
你真的了解时间复杂度吗
如何计算时间复杂度
时间复杂度计算 例题合集
我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h
我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i
我需要检查DateTime是否采用有效的ISO8601格式。喜欢:#iso8601?我检查了ruby是否有特定方法,但没有找到。目前我正在使用date.iso8601==date来检查这个。有什么好的方法吗?编辑解释我的环境,并改变问题的范围。因此,我的项目将使用jsapiFullCalendar,这就是我需要iso8601字符串格式的原因。我想知道更好或正确的方法是什么,以正确的格式将日期保存在数据库中,或者让ActiveRecord完成它们的工作并在我需要时间信息时对其进行操作。 最佳答案 我不太明白你的问题。我假设您想检查
有时我需要处理键/值数据。我不喜欢使用数组,因为它们在大小上没有限制(很容易不小心添加超过2个项目,而且您最终需要稍后验证大小)。此外,0和1的索引变成了魔数(MagicNumber),并且在传达含义方面做得很差(“当我说0时,我的意思是head...”)。散列也不合适,因为可能会不小心添加额外的条目。我写了下面的类来解决这个问题:classPairattr_accessor:head,:taildefinitialize(h,t)@head,@tail=h,tendend它工作得很好并且解决了问题,但我很想知道:Ruby标准库是否已经带有这样一个类? 最佳
这个问题在这里已经有了答案:Railsformattingdate(4个答案)关闭4年前。我想格式化Time.Now函数以显示YYYY-MM-DDHH:MM:SS而不是:“2018-03-0909:47:19+0000”该函数需要放在时间中.现在功能。require‘roo’require‘roo-xls’require‘byebug’file_name=ARGV.first||“Template.xlsx”excel_file=Roo::Spreadsheet.open(“./#{file_name}“,extension::xlsx)xml=Nokogiri::XML::Build
我正在尝试解析一个CSV文件并使用SQL命令自动为其创建一个表。CSV中的第一行给出了列标题。但我需要推断每个列的类型。Ruby中是否有任何函数可以找到每个字段中内容的类型。例如,CSV行:"12012","Test","1233.22","12:21:22","10/10/2009"应该产生像这样的类型['integer','string','float','time','date']谢谢! 最佳答案 require'time'defto_something(str)if(num=Integer(str)rescueFloat(s
给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最
我正在尝试使用Curbgem执行以下POST以解析云curl-XPOST\-H"X-Parse-Application-Id:PARSE_APP_ID"\-H"X-Parse-REST-API-Key:PARSE_API_KEY"\-H"Content-Type:image/jpeg"\--data-binary'@myPicture.jpg'\https://api.parse.com/1/files/pic.jpg用这个:curl=Curl::Easy.new("https://api.parse.com/1/files/lion.jpg")curl.multipart_form_
无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD
本教程将在Unity3D中混合Optitrack与数据手套的数据流,在人体运动的基础上,添加双手手指部分的运动。双手手背的角度仍由Optitrack提供,数据手套提供双手手指的角度。 01 客户端软件分别安装MotiveBody与MotionVenus并校准人体与数据手套。MotiveBodyMotionVenus数据手套使用、校准流程参照:https://gitee.com/foheart_1/foheart-h1-data-summary.git02 数据转发打开MotiveBody软件的Streaming,开始向Unity3D广播数据;MotionVenus中设置->选项选择Unit