大佬教程收集整理的这篇文章主要介绍了保姆级教学!彻底学会时间复杂度和空间复杂度,大佬教程大佬觉得挺不错的,现在分享给大家,也给大家做个参考。
大家好c;我是蛋蛋。
数据结构与算法c;作为编程界从入门到劝退的王者c;是很多初学者心中神圣而想侵犯的村花儿c;化身舔狗c;费尽心思c;舔到最后c;我命油我不油天。
作为一名大学之前编程能力为零c;以为计算机专业就是打游戏的咸鱼c;到参加 ACM 竞赛c;靠着划水和抱大腿拿了几块金银铜铁牌的前 ACM 混子选手c;想起之前疯狂跪舔数据结构和算法的日子c;至今我扔能掬出一把辛酸泪。
@H_772_18@
为了让臭宝们不再像我这样当个人这么难c;我决定和大家一起学习数据结构与算法c;我希望能用傻瓜的方式c;由浅入深c;从概念到实践c;一步一步的来c;这个过程可能会很长c;我希望在这个过程中你能喜欢上它c;能发现它们冰冷外表下有趣的灵魂。
学习数据结构与算法的第一课c;我永远都选复杂度分析c;在我看来c;这是数据结构与算法中最重要的知识点c;且不接受任何反驳。
@H_673_30@复杂度分析主要就是时间复杂度和空间复杂度c;接下来的文章也主要围绕这两方面来讲。废话不多说c;前排马扎瓜子准备好c;蛋蛋小课堂正式接客图片
刚刚我说过c;在本蛋看来c;复杂度分析是数据结构和算法中最重要的知识点c;毫不夸张的说c;这就是数据结构与算法学习的核心所在。你学会了它你就入的了门c;你学不会它c;你就永远不知道门儿在哪。
为什么复杂度分析会这么重要?
这个就要从盘古开天辟地c;呃c;从数据结构与算法的本身说起。
我平常白天做梦的时候c;总是想着当当咸鱼划划水就能赚大钱c;最好就是能躺着c;钱就直接砸到我脑阔上。数据结构与算法虽然没有本蛋这么大的梦想c;但是它的出现也是想着花更少的时间和更少的存储来解决问题。
那如何去考量“更少的时间和更少的存储”c;复杂度分析为此而生。
蛋蛋:所以为什么复杂度分析重要你们知道了叭? 臭宝:不知道不知道 蛋蛋:啥?还不知道??? 臭宝:对呀对呀 蛋蛋:。。。
既然你诚心诚意的不知道c;那我就大发慈悲的打个不恰当的比方。
如果把数据结构与算法看成武功招式c;那复杂度分析就是对应的心法。
如果只是学会了数据结构与算法的用法c;不学复杂度分析c;这就和你费尽千辛万苦在隔壁老王家次卧进门右手第二块地砖下偷挖出老王打遍村里无敌手的村林至宝王霸拳c;然后发现秘籍上只有招式c;没有内功心法c;好好得王霸拳变的还不如王八拳。只有学会了王霸之气c;才能虎躯一震c;王霸之气一噗c;震走村口光棍李养的哈巴狗。
@H_673_30@臭宝:哇c;厉害厉害厉害c;你胖你说的都对c;但还是没必要学啊。 蛋蛋:??? 臭宝:现在有很多工具啊包啊c;代码随便跑一下c;就能轻轻松松知道运行了多少时间占了多少内存啊。算法的效率不就轻松比对出来了么? 蛋蛋:。。。
吐样吐森破!吃葡萄不吐葡萄皮!
你们说的这种主流叫做事后统计法。
简单来说c;就是你需要提前写好算法代码和编好测试数据c;然后在计算机上跑c;通过最后得出的运行时间判断算法时效的高低c;这里的运行时间就是我们日常的时间。
我且不用“万一你费劲心思写好的算法代码本身是个很糟糕的解法”这种理由去反驳你c;事后统计法本身存在很多缺陷c;它并不是一个对我们来说有用的度量指标:
首先c;事后统计法太依赖计算机的软件和硬件等性能。代码在 core i7 处理器的就比 core i5 处理器的运算速度快c;更不用说不同的操作系统、不同的编程语言等软件方面c;就算是在同一台电脑上c;用的所有的东西都一样c;内存的占用或者是 CPU 的使用率也会造成运行时间的差异。
再者c;事后统计法太依赖于测试数据集的规模。同样是排序算法c;你随便整 5 个 10 个的数排序c;就算最垃圾的排序算法c;也看起来快的和法拉利一样c;如果是来10w 个 100w 个c;那不同的算法的差距就很大了c;而且同样是 10w 个 100w 个数c;顺序和乱序的时间又不同了。
那么问题来了c;到底测试数据集选多少个合适?数据的顺序如何定又算合适?
说不出来了叭?
可以看出c;我们需要一个不依赖于性能和规模等外力影响就可以估算算法效率、判断算法优劣的度量指标c;而复杂度分析天生就是干这个的c;这也是为什么我们要学习并必须学会复杂度分析!
时间复杂度c;也就是指算法的运行时间。
对于某一问题的不同解决算法c;运行时间越短算法效率越高c;相反c;运行时间越长c;算法效率越低。
那么如何估算时间复杂度呢?
大佬们在拽掉脑阔上最后一根头发的时候发现c;当用运行时间去描述一个算法快慢的时候c;算法中执行的总步数显得尤为重要。
因为只是估算c;我们可以假设每一行代码的运行时间都为一个 Btimec;那么算法的总的运行时间 = 运行的总代码行数。
下面我们来看这么一段简单的代码。
def dogeggs_sum (n):
sum = 0
for dogegg in range(n):
sum += dogegg
return sum
在上面假设的情况下c;这段求累加和的代码总的运行时间是多少呢?
第 2 行代码需要 1 Btime 的运行时间c;第 4 和 第 5行分别运行了 n 次c;所以每个需要 n * Btime 的运行时间c;所以总的运行时间就是 (1 + 2n) * Btime。
我们一般用T 函数来表示赋值语句的总运行时间c;所以上面总的运行时间就可以表达成 T(n) = (1 + 2n) * Btimec;翻译一下就是“数据集大小为 nc;总步数为 (1 + 2n) 的算法的执行时间为 T(n)”。
通过公式c;我们可以看出 T(n) 和总步数是成正比关系c;这个规律的发现其实是很重要的c;因为这个告诉了我们一种趋势c;数据集规模和运行时间之间有趋势!
所有人准备c;我们很熟悉的大 O 闪亮登场了!
@H_673_30@大 O 表示法c;表示的是算法有多快。
这个多快c;不是说算法代码真正的运行时间c;而是代表着一种趋势c;一种随着数据集的规模增大c;算法代码运行时间变化的一种趋势。一般记作 O(f(n))。
那么之前的公式就变成 T(n) = O(f(n))c;其中 f(n) 是算法代码执行的总步数c;也叫操作数。
n 作为数据集大小c;它可以取 1c;10c;100c;1000 或者更大更大更大的数c;到这你@R_452_10585@一件很有意思的事儿c;那就是当数据集越来越大时c;你会发现代码中的某些部分“失效”了。
还是以之前的代码为例。当 n = 1000 时c;1 + 2n = 2001c;当 n = 10000 时c;1 + 2n = 20001c;当这个 n 持续增大时c;常数 1 和系数 2 对于最后的结果越来越没存在感c;即对趋势的变化影响不大。
所以对于我们这段代码样例c;最终的 T(n) = O(n)。
接下来再用两段代码进一步学一下求时间复杂度分析。
代码 1
def dogeggs_sum (n):
sum = 0
for dogegg in range(n):
for i in range(n):
sum += dogegg * i
return sum
这段代码我会从最开始一点点带你来。
第 2 行需要运行 1 次 c;第 4 行需要运行 n 次c;第 5 行和第 6 行分别需要运行 n² 次。所以总的运行次数 f(n) = 1 + n + 2n²。
当 n 为 5 的时候c;f(n) = 1 + 5 + 2 * 25c;当 n 为 10000 的时候c;f(n) = 1 + 10000 + 2 * 100000000c;当 n 更大呢?
这个时候其实很明显的就可以看出来 n² 起到了决定性的作用c;像常数 1c;一阶 n 和 系数 2 对最终的结果(即趋势)影响不大c;所以我们可以把它们直接忽略掉c;所以执行的总步数就可以看成是“主导”结果的那个c;也就是 f(n) = n²。
自然代码的运行时间 T(n) = O(f(n)) = O(n²)。
代码 2
def dogeggs_sum (n):
sum1 = 0
for dogegg1 in range(n):
sum1 += dogegg1
sum2 = 0
for dogegg2 in range(n):
for i in range(n):
sum2 += dogegg2 * i
sum3 = 0
for dogegg3 in range(n):
for i in range(n):
for j in range(n):
sum3 += dogegg3 * i * j
return sum1 + sum2 + sum3
上面这段代码是求三部分的和c;经过之前的学习应该很容易知道c;第一部分的时间复杂度是 O(n)c;第二部分的时间复杂度是 O(n²)c;第三部分是 O(n³)。
正常来讲c;这段代码的 T(n) = O(n) + O(n²) + O(n³)c;按照我们取“主导”部分c;显然前面两个小弟都应该直接 passc;最终 T(n) = O(n³)。
通过这几个例子c;聪明的小婊贝们肯定会发现c;对于时间复杂度分析来说c;只要找起“主导”作用的部分代码即可c;这个主导就是最高的那个复杂度c;也就是执行次数最多的那部分 n 的量级。
剩下的就是多加练习c;有意识的多去想多去练c;就可以和我一样 帅气 稳啦。
这里推荐两个比较好的资料c;一份是笔记c;一份最优解c;两者加起来无敌。 两个月斩获 70k starc;前字节大神刷题笔记 清华学霸的刷题笔记(leetcode最优解)
算法学习过程中c;我们会遇到各种各样的时间复杂度c;但纵使你代码七十二变c;几乎也逃不过下面这几种常见的时间复杂度。
上表的时间复杂度由上往下依次增加c;O(n) 和 O(n²) 前面已经讲过了c;O(2n) 和 O(n!) 效率低到离谱c;以后不幸碰到我再提一下c;就不再赘述。
下面主要来看剩下几种最常见的时间复杂度。
O(1)
O(1) 是常数时间复杂度。
在这你要先明白一个概念c;不是只执行 1 次的代码的时间复杂度记为 O(1)c;只要你是常数c;像 O(2)、O(3) … O(100000) 在复杂度的表示上都是 O(1)。
n = 10000
res = n / 2
print(res)
比如像上面这段代码c;它运行了 3 次c;但是它的时间复杂度记为 O(1)c;而不是 O(3)。
因为无论 n 等于多少c;对于它们每行代码来说还是只是执行了一次c;代码的执行时间不随 n 的变化而变化。
O(logn)
O(logn) 是对数时间复杂度。首先我们来看一段代码:
dogegg = 1
while dogegg < n:
dogegg = dogegg * 2
根据之前讲的c;我们先找“主导”c;在这主导就是第 4 行代码c;只要算出它的时间复杂度c;总的时间复杂度就知道了。
其实很简单c;上面的代码翻译一下就是初始化 dogegg = 1c; 乘上多少个 2 以后会 ≥ n。
假设需要 x 个c;那么相当于求:
即:
所以上述代码的时间复杂度应该为 O(log2n)。
但是对于对数复杂度来说c;不管你是以 2、3 为底c;还是以 10 为底c;通通记作 O(logn)c;这是为什么呢?
这就要从对数的换底公式说起。
根据换底公式c;log2n 可以写成:
对于时间复杂度来说:
所以在对数时间复杂度中我们就忽略了底c;直接用 O(logn) 来表示对数时间复杂度。
O(nlogn)
O(nlogn)c;真的是很常见的时间复杂度c;像后面会学到的快速排序、归并排序的时间复杂度都是 O(nlogn)。
如果只是简单看的话是在 logn 外面套了层 for 循环c;比如像下面这段代码:
def stupid_cnt(n):
cnt = 0
for i in range(n):
dogegg = 1
while dogegg < n:
dogegg = dogegg * 2
cnt += 1
return cnt
当然像上面这种 stupid 代码一般人不会写c;我也只是举个例子给大家看c;我是狗蛋c;大家千万不要以为我是傻狗。
@H_673_30@最好情况、最坏情况和平均情况时间复杂度
除了数据集规模影响算法的运行时间外c;“数据的具体情况”也会影响到运行时间。
我们来看这么一段代码:
def find_word(lst, word):
flag = -1
for i in range(len(lst)):
if lst[i] == word:
flag = i
break
return flag
上面这段简单代码是求变量 word 在列表 lst 中出现的位置c;我用这段来解释“数据的具体情况”是什么意思。
变量 word 可能出现在列表 lst 的任意位置c;假设 a = [‘a’, ‘b’, ‘c’, ‘d’]:
当 word = ‘a’ 时c;正好是列表 lst 的第 1 个c;后面的不需要再遍历c;那么本情况下的时间复杂度是 O(1)。
当 word = ‘d’ 或者 word = ‘e’ 时c;这两种情况是整个列表全部遍历完c;那么这些情况下的时间复杂度是 O(n)。
这就是数据的具体情况不同c;代码的时间复杂度不同。
根据不同情况c;我们有了最好情况时间复杂度、最坏情况时间复杂度和平均情况时间复杂度这 3 个概念。
最好情况就是在最理想的情况下c;代码的时间复杂度。对应上例变量 word 正好是列表 lst 的第 1 个c;这个时候对应的时间复杂度 O(1) 就是这段代码的最好情况时间复杂度。
最坏情况就是在最差的情况下c;代码的时间复杂度。对应上例变量 word 正好是列表 lst 的最后一个c;或者 word 不存在于列表 lstc;这个时候对应的时间复杂度 O(n) 是这段代码的最坏情况时间复杂度。
大多数情况下c;代码的执行情况都是介于最好情况和最坏情况之间c;所以又出现了平均情况时间复杂度。
那怎么计算平均时间复杂度呢?这个说起来有点复杂c;需要用到概率论的知识。
在这里我还是用上面的例子来讲c;因为只是简单的科普一下c;为了方便计算c;我假设的会有点随意:
从大的方面来看c;查找变量 x 在列表 lst 中的位置有两种情况:在或者不在。假设变量 x 在或者不在列表 lst 中的概率都为 1/2。
如果 x 在列表 lst 中c;那么 x 有 n 种情况c;它可能出现在 0 到 n-1 中任意位置c;假设出现在每个位置的概率都相同c;都为 1/n。
每个出现的概率(即权重)知道了c;所以平均时间复杂度为:
是不是看着有点眼熟c;这就是我们都学过的加权平均值。
什么c;没学过?问题不大。加权平均值就是将各数值乘以相应的权数c;然后加总求和得到总体值c;再除以总的单位数。
所以最终的平均时间复杂度就为:
臭宝:又是最好c;又是最坏c;又是平均的c;这么多到底用哪个呀? 蛋蛋:这个还用问?当然是选择最坏情况时间复杂度啊!
最好情况c;反应最理想的情况c;怎么可能天上天天掉馅饼c;没啥参考价值。
平均情况c;这倒是能反映算法的全面情况c;但是一般“全面”c;就和什么都没说一样c;也没啥参考价值。
最坏情况c;干啥事情都要考虑最坏的结果c;因为最坏的结果提供了一种保障c;触底的保障c;保障代码的运行时间这个就是最差的c;不会有比它还差的。
所以c;不需要纠结各种情况c;算时间复杂度就算最坏的那个时间复杂度即可。
空间复杂度相较于时间复杂度来说c;比较简单c;@R_292_9262@内容不多。
空间复杂度和时间复杂度一样c;反映的也是一种趋势c;只不过这种趋势是代码运行过程中临时变量占用的内存空间。
臭宝:为什么是“临时”咧? 蛋蛋:这就要从代码在计算机中的运行说起啦。
代码在计算机中的运行所占用的存储空间呐c;主要分为 3 部分:
前面两个部分是本身就要占这些空间c;与代码的性能无关c;所以我们在衡量代码的空间复杂度的时候c;只关心运行过程中临时占用的内存空间。
空间复杂度记作 S(n)c;表示形式与时间复杂度一致。
在这同样 n 作为数据集大小c;f(n) 指的是规模 n 所占存储空间的函数。
下面我们用一段简单代码来学习下如何分析空间复杂度。
def dogeggs_sum(lst):
sum = 0
for i in range(len(lst)):
sum += lst[i]
return sum
上述代码是求列表 lst 的所有元素之和c;根据之前说的c;只计算临时变量所占用的内存空间。
形参 lst 所占的空间不计c;那么剩下的临时变量只有 sum 和 ic;sum 是存储和c;是常数阶c;i 是存储元素位置c;也是常数阶c;它俩所分配的空间和规模 n 都没有关系c;所以整段代码的空间复杂度 S(n) = O(1)。
常见的空间复杂度有 O(1)、O(n)、O(n²)c;O(1) 在上节讲了c;还有就是像 O(logn) 这种也有c;但是等闲不会碰到c;在这里就不罗嗦了。
O(n)
def create_lst(n):
lst = []
for i in range(n):
lst.append(i)
return lst
上面这段傻傻的代码有两个临时变量 lst 和 i。
其中 lst 是被创建的一个空列表c;这个列表占用的内存随着 for 循环的增加而增加c;最大到 nc;所以 lst 的空间复杂度为 O(n)c;i 是存储元素位置的常数阶c;与规模 n 无关c;所以这段代码最终的空间复杂度为 O(n)。
O(n²)
def create_lst(n):
lst1 = []
for i in range(n):
lst2 = []
for j in range(n):
lst2.append(j)
lst1.append(lst2)
return lst1
还是一样的分析方式c;很明显上面这段傻二次方的代码创建了一个二维数组 lstc;一维 lst1 占用 nc;二维 lst2 占用 n²c;所以最终这段代码的空间复杂度为 O(n²)。
到这里c;复杂度分析就全部讲完啦c;只要臭宝们认真看完这篇文章c;相信会对复杂度分析有个基本的认识。复杂度分析本身不难c;只要多加练习c;平时写代码的时候有意识的多想估算一下自己的代码c;感觉就会慢慢来啦。
这是玩转数据结构与算法的第一篇c;写完真心不太容易c;希望开了个好头。码字不易c;臭宝们多多支持呀c;一键三连再来留言c;么么哒呀。
我们下次见~
@H_673_30@以上是大佬教程为你收集整理的保姆级教学!彻底学会时间复杂度和空间复杂度全部内容,希望文章能够帮你解决保姆级教学!彻底学会时间复杂度和空间复杂度所遇到的程序开发问题。
如果觉得大佬教程网站内容还不错,欢迎将大佬教程推荐给程序员好友。
本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
如您有任何意见或建议可联系处理。小编QQ:384754419,请注明来意。