技术标签: 算法 python Python数据结构与算法 数据结构
我们已经知道算法是具有有限步骤的过程,其最终的目的是为了解决问题,而根据我们的经验,同一个问题的解决方法通常并非唯一。这就产生一个有趣的问题:如何对比用于解决同一问题的不同算法?为了以合理的方式提高程序效率,我们应该知道如何准确评估一个算法的性能。
通过本节学习,应掌握以下内容:
算法分析的主要目标是从运行时间和内存空间消耗等方面比较算法。
一个好的算法首先应该是“正确”的,其对于每个输入实例均能终止并给出正确的结果,能够正确解决给定的计算问题。此外,还需要考虑以下方面:
一个算法同时可以满足存储空间小、运行时间短、其它性能也好是很难做到的,很多情况下,我们不得不对性能进行取舍,在实际选择算法时,我们通常遵循以下原则:
算法效率分析根据算法执行所需的时间进行分析和比较,这也称为算法的执行时间或运行时间。要衡量算法的执行时间,一个方法就是做基准分析,这是一种事后统计的方法,其使用绝对的时间单位来记录程序计算出结果所消耗的实际时间。在 Python
中,可以使用 time
模块的 time
函数记录程序的开始时间和结束时间,然后计算差值,就可以得到以秒为单位的算法执行时间。
以计算斐波那契数列第 n 项为例(斐波那契数列从第3项开始,每一项都等于前两项之和),在计算斐波那契数列第 n 项前后调用 time
函数,计算执行时间:
import time
def fibo(n):
start = time.time()
a, b = 1, 1
if n > 2:
for i in range(n-2):
a, b = b, a + b
end = time.time()
running = end-start
return b, running
for i in range(5):
results = fibo(100000)
print('It takes {:.8f} seconds to calculate the 10000th item of Fibonacci sequence'.format(results[1]))
代码执行结果如下:
It takes 0.08275080 seconds to calculate the 10000th item of Fibonacci sequence
It takes 0.08277822 seconds to calculate the 10000th item of Fibonacci sequence
It takes 0.08176851 seconds to calculate the 10000th item of Fibonacci sequence
It takes 0.08178067 seconds to calculate the 10000th item of Fibonacci sequence
It takes 0.08081150 seconds to calculate the 10000th item of Fibonacci sequence
但是这种方法计算的是执行算法的实际时间,有两个明显的缺陷:1) 必须先运行依据算法编制的程序;2) 依赖于特定的计算机、编译器与编程语言等软硬件环境,容易掩盖算法本身的优劣。因此,我们希望找到一个独立于程序或计算机的指标,以用来比较不同实现下的算法。
为了摆脱与计算机硬件、软件有关的因素,我们需要一种事前分析估算的方法。可以认为特定算法的“运行工作量”大小取决于问题的规模,或者说,它是问题规模的函数,这时我们就需要量化算法的操作或步骤。一个算法是由控制结构和基本操作构成的,因此可以将算法的执行时间描述成解决问题所需重复执行的基本操作数。需要注意的是,确定合适的基本操作取决于不同的算法。例如在计算斐波那契数列第 n 项时,赋值语句就是一个基本操作,而在计算矩阵乘法时,乘法运算则是其基本操作。
在上一节的 fibo
函数中,整个算法的执行时间与基本操作(赋值)重复执行的次数n 成正比,具体而言是 1 加上 n-2 个赋值语句,如果使用将其定义为函数可以表示为 T ( n ) = n − 1 T(n)=n-1 T(n)=n−1,其中 n n n 为大于 2 的正整数。 n n n 常用于表示问题规模,我们可以使用问题规模 n n n 的某个函数 f ( n ) f(n) f(n) 表示算法中基本操作重复执行的次数,算法的时间量度可以表示如下:
T ( n ) = O ( f ( n ) ) T(n)=O(f(n)) T(n)=O(f(n))
随问题规模 n n n 的增大, T ( n ) T(n) T(n) 函数的某一部分会比其余部分增长得更快,算法间进行比较时这一起部分起决定性作用, T ( n ) T(n) T(n) 增长最快的部分也称为数量级函数。算法执行时间的增长率和 f ( n ) f(n) f(n) 的增长率相同,称作算法的渐近时间复杂度 (asymptotic time complexity),简称时间复杂度。数量级 (order of magnitude) 常被称作大 O O O 记法或大 O O O 表示法。
通过以上分析,我们可以将算法的渐近复杂度规则描述如下:
假设某一算法的基本步骤数为 T ( n ) = 3 n 2 + 50 n + 2000 T(n)=3n^2+50n+2000 T(n)=3n2+50n+2000,当 n n n 很小时 2000 对于函数的影响最大,但是随着 n n n 的增长 n 2 n^2 n2 将逐渐变得更重要,以至于可以忽略其他两项以及 n 2 n^2 n2 的系数 3,因此可以说 T ( n ) T(n) T(n) 的数量级是 n 2 n^2 n2 或写为 O ( n 2 ) O(n^2) O(n2)。
算法的性能有时不仅依赖问题的规模,还取决于算法的输入值,输入令算法运行最慢的情况称为最坏情况,输入令算法运行最快的情况称为最好情况,随机输入的情况下算法的性能介于两种极端情况之间,称为平均情况。
下表列出了一些常见的大 O O O 表示法实例:
复杂度 | 解释 | 示例 |
---|---|---|
O ( 1 ) O(1) O(1) | 常数复杂度 | 100, 500, 1, 30, … |
O ( l o g n ) O(logn) O(logn) | 对数复杂度 | l o g 2 n log_2n log2n, l o g 10 n log_{10}n log10n, 2 l o g 2 n 2log_2n 2log2n, … |
O ( n ) O(n) O(n) | 线性复杂度 | 8 n + 10 8n+10 8n+10, n n n, 100 n 100n 100n, … |
O ( n l o g n ) O(nlogn) O(nlogn) | 对数线性复杂度 | 10 n l o g n + 50 10nlogn+50 10nlogn+50, 5 n l o g n + 30 n 5nlogn+30n 5nlogn+30n, … |
O ( n k ) O(n^k) O(nk) | 多项式复杂度,其中 k k k 为常数 | 4 n 2 − 10 n 4n^2-10n 4n2−10n, 2 n 3 + 10 n 2 2n^3+10n^2 2n3+10n2, 4 n 2 + 5 n l o g n 4n^2+5nlogn 4n2+5nlogn, … |
O ( c n ) O(c^n) O(cn) | 指数复杂度,其中 c c c 为常数 | 2 n + 5 n 2 2^n+5n^2 2n+5n2, 4 n + 10 n l o g n 4^n+10nlogn 4n+10nlogn, … |
常见算法复杂度的增长率比较如下:
常数复杂度表示,算法的渐进复杂度域输入的规模无关,例如求列表的长度等都属于常数复杂度。常数复杂度和代码中是否包含循环没有必然关系,例如循环打印 100 次 “Hello world”,这与输入规模并没有什么关系,因此其也是属于常数复杂度。
对数复杂度表示函数的增长速度至少是输入规模的对数,当我们谈论对数复杂度时,我们并不关系对数的底数,这是由于可以使用换底公式,将原来底数的对数乘以一个常数转换为另一个底数:
l o g a n = l o g a b ∗ l o g b n log_an=log_ab*log_bn logan=logab∗logbn
其中, a a a 和 b b b 均为常数。例如以下代码,将一个正整数转换为字符串:
def int_to_str(num):
digits = "0123456789"
result = ''
if num == 0:
result = '0'
else:
while num > 0:
result = digits[num % 10] + result
num = num // 10
return result
上述代码中只包括一个循环,且没有调用其它函数,因此我们只需找出循环迭代次数——在 num 为 0 之前所需的整数除法的次数 l o g 10 n log_{10}n log10n。因此函数 int_to_str 的复杂度是 O ( l o g n ) O(logn) O(logn)。
线性复杂度在列表中等序列数据类型总十分常见,因为算法通常需要遍历处理序列中的每一个元素。例如将列表中的每个元素加上常数 10:
def add_constant(list_o):
for i in range(len(list_o)):
list_o[i] += 10
这个函数的复杂度就与列表的长度成线性关系,也就是 O ( n ) O(n) O(n)。
线性对数复杂度是两项的乘积,每个项都依赖于输入的规模,例如将列表中每一项正整数转换为字符串。很多实用算法的复杂度都是对数线性的。
多项式复杂度的增长速度是输入规模的 k k k 次幂,其中最常见的是平方复杂度,例如求列表 list_a 和 list_b 的交集:
def intersect(list_a, list_b):
# 第一部分
temp = []
for i in list_a:
for j in list_b:
if i == j:
temp.append(i)
break
# 第二部分
result = []
for i in temp:
if i not in result:
result.append(i)
return result
intersect 函数第一部分的复杂度显然是 O ( l e n ( l i s t _ a ) ) ∗ O ( l e n ( l i s t _ b ) ) O(len(list\_a))*O(len(list\_b)) O(len(list_a))∗O(len(list_b)),第二部分代码用于去除第一部分得到结果列表中的重复元素,虽然其中仅包含一个循环语句,但是测试条件 if i not in result
需要检查 result 中的每个元素,因此第二部分的复杂度为 O ( l e n ( t e m p ) ) ∗ O ( l e n ( r e s u l t ) ) O(len(temp))*O(len(result)) O(len(temp))∗O(len(result)),tmp 和 result 的长度取决于 list_a 和 list_b 中长度较小的那个,根据渐进复杂度规则可以将其忽略。最终,intersect 函数的复杂度就是 O ( n 2 ) O(n^2) O(n2)。
指数复杂度算法的解决时间随输入规模的指数增长。在以下示例中,由于 1 左移 num 位得到 end,因此 end 实际上等于 2 n u m 2^{num} 2num,因此循环中计算了 2 n u m 2^{num} 2num 次加法,时间复杂度为 O ( 2 n ) O(2^{n}) O(2n)。
def calculate(num):
result = 0
end = 1 << num
for i in range(end):
result += i
return result
为了直观的观察到各种复杂度的增长情况,使用统计图来对比各种复杂度算法的运行时间增长速度。
从上图可以看出,对数复杂度随问题规模的增长,运行时间的增长很小,几乎和常数复杂度算法一样优秀,通常只有当输入规模很大时才能直观的看出两者之间的差别,而线性复杂度和对数复杂度的区别在输入规模很小时就非常明显了。
虽然 O ( l o g n ) O(logn) O(logn) 的增长速度很慢,但是在线性乘法因子的加持下,其增长速率高于线性复杂度,但与平方复杂度的增长速度相比,就不值一提了,因此在实际情况下,具有 O ( n l o g n ) O(nlogn) O(nlogn) 复杂度的算法执行速度还是很快的。而指数复杂度除了对那些规模特别小的输入,其运行时间都是不现实的,即使立方复杂度和其相比都相形见绌。
在以上内容中讨论的都是代码的时间复杂度。这是由于,与时间复杂度相比,要想感觉到空间复杂度 (space complexity) 的影响比较困难。对于用户来说,程序运行完成需要 1 分钟还是 10 分钟是明显能够感觉到的,但程序使用的内存是 1 兆字节还是 10 兆字节则无法直观觉察。这也就是时间复杂度通常比空间复杂度更受关注的原因。通常只有当运行程序所需的存储空间超过了计算机内存时,空间复杂度才会受到关注。
类似于算法的时间复杂度,空间复杂度作为算法所需存储空间的量度,可以表示为:
S ( n ) = O ( f ( n ) ) S(n)=O(f(n)) S(n)=O(f(n))
一个程序的执行除了需要存储空间来寄存本身所用指令、常数变量和输入数据外,也需要一些辅助空间用于存储数据处理的中间数据。若输入数据所占空间只取决于问题本身,和算法无关,则只需要分析除输入和程序之外的额外空间,否则应同时考虑输入本身所需空间。若额外空间相对于输入数据量来说是常数,则称此算法为原地工作。
由于在之后的学习中,我们需要经常使用列表和字典作为构建其他数据结构的基石,因此了解这些数据结构操作的时间复杂度是必要的。
Python 列表常见操作的时间复杂度如下表所示:
操作 | 大 O O O 表示法 | 操作 | 大 O O O 表示法 |
---|---|---|---|
索引及索引赋值 | O ( 1 ) O(1) O(1) | in 及 not in | O ( n ) O(n) O(n) |
append() | O ( 1 ) O(1) O(1) | 切片 | O ( n ) O(n) O(n) |
pop() | O ( 1 ) O(1) O(1) | 删除切片及切片赋值 | O ( n ) O(n) O(n) |
pop(i) | O ( n ) O(n) O(n) | 反转 | O ( n ) O(n) O(n) |
insert(i, item) | O ( n ) O(n) O(n) | 连接 | O ( n ) O(n) O(n) |
del | O ( n ) O(n) O(n) | sort() | O ( n l o g n ) O(nlogn) O(nlogn) |
遍历 | O ( n ) O(n) O(n) | 乘法 | O ( n 2 ) O(n^2) O(n2) |
在列表中虽然 append() 操作和 insert() 操作都是向列表中添加一个元素,不同的是 append() 向列表末尾追加一个元素,而 insert() 在指定位置处插入元素,其后的元素都要向后移一位,因此它们的时间复杂度也不相同。
为了获取执行时间,这里使用 timeit
模块,该模块能够在一致的环境中执行函数。要使用 timeit
模块,首先需要创建一个 Timer
对象,其接受两个参数:第 1 个参数是要为之计时的 Python
语句;第 2 个参数是建立测试的 Python
语句。timeit
模块会统计多次执行语句要用多久,默认情况下,timeit
会执行 100 万次语句,并在完成后返回一个浮点数格式的秒数,可以给 timeit
传入参数 number
,以指定语句的执行次数。
import timeit
import random
append_timeit = timeit.Timer('x.append(1)', 'from __main__ import x')
insert_timeit = timeit.Timer('x.insert(0, 1)', 'from __main__ import x')
for i in range(10000, 2000000, 20000):
x = list(range(i))
# 测试函数运行 1000 次所花的时间
append_time = append_timeit.timeit(number=1000)
x = list(range(i))
# 测试函数运行 1000 次所花的时间
insert_time = insert_timeit.timeit(number=1000)
print("{}, {}, {}".format(i, append_time, insert_time))
在上例中,计时的语句是对 append 和 insert 操作的调用。建立测试的语句是初始化代码或构建环境导入语句,是执行代码的准备工作,示例中的 from __main__ import x
将 x 从 __main__
命名空间导入到 timeit
设置计时的命名空间,用于在测试中使用列表对象 x,这么是为了在一个干净的环境中运行计时测试,以免某些变量以某种意外的方式干扰函数的性能。
从上图中可以看出,列表越长,insert
操作的耗时也随之变长,而 append
操作的耗时很稳定,符合 O ( n ) O(n) O(n) 和 O ( 1 ) O(1) O(1) 的特征。
Python 字典常见操作的时间复杂度如下表所示:
操作 | 大 O O O 表示法 | 操作 | 大 O O O 表示法 |
---|---|---|---|
取值 | O ( 1 ) O(1) O(1) | 赋值 | O ( 1 ) O(1) O(1) |
del | O ( 1 ) O(1) O(1) | in 及 not in | O ( n ) O(n) O(n) |
复制 | O ( n ) O(n) O(n) | 遍历 | O ( n ) O(n) O(n) |
对比两表可以发现,即使是同一操作使用不同数据结构其复杂度也是不同的,例如包含操作 in。为了验证它们之间的不同,编写以下程序进行实验:
import timeit
import random
for i in range(10000, 1000000, 20000):
t = timeit.Timer('random.randrange({}) in x'.format(i), 'from __main__ import random, x')
x = list(range(i))
list_time = t.timeit(number=1000)
x = {
j: j for j in range(i)}
dict_time = t.timeit(number=1000)
print("{}, {}, {}".format(i, list_time, dict_time))
从上图可以看出,随着规模的增长,对于字典而言,包含操作的耗时始终是基本恒定的,而对于列表而言,其包含操作的耗时呈线性增长。
A. 求浙江计算机二级办公室软件高级应用试题库(历年真题)最好 及其步骤系统. [email protected]邮件已发B. 求浙江计算机二级办公软件高级应用题库习题复习资料!!!网络文库里搜索你想要找的文档就可以,一般都可以找到。C. 大学里办公软件高级应用与案例精选选择题题库WPS Office考试内容 一、基础知识 1、计算机的概念、类型及其应用领域;计算机系统的配置及主要技术指标。 2、...
c ++ CreateFile函数错误[关闭](c++ CreateFile function error [closed])我想使用函数CreateFile来创建一个文件,但有些东西是错的,我不知道是什么。 GetLastError()给出错误87,这是参数不正确,但我找不到哪一个。码:HANDLE Create;Create = CreateFile("D:\Test.txt",GENERIC...
今天带来一次有关于深度学习中的数据增强方法的分享。00什么是数据增强在深度学习项目中,寻找数据花费了相当多的时间。但在很多实际的项目中,我们难以找到充足的数据来完成任务。...
whereselect * from table_name where 条件A and 条件B;select * from table_name where 条件A and 条件B or 条件C;select * from table_name where (字段名A,字段名B)=(值C,值D);注意:第一步会先运行and,第二步会运行or;如果要先运行or,则需要加小括号。Where运算符运算符描述=等于<>不等于。注释:在 SQL 的一些版本中,该
XSP06是一款符合PD、QC、三星协议的快充取电芯片,支持从手机充电器/车充等电源上诱骗出需要的电压给产品供电。支持固定电压模式和使用单片机控制切换电压。芯片特性:1、符合USB PD2.0/3.0和QC2.0/3.0快充协议2、支持从PD、QC、AFC协议的充电器取电3、通过引脚选择电压档位4、支持取电电压5V、9V、12V、15V、20V应用范围:小家电、音箱、吸尘器、卷发器、无线充、筋膜枪等例如:1、无线充电产品:一般需要5V、9V、12V电压输入。2、笔记本电脑诱骗线:使..
前言该文档(也可以看做笔记),皆是本人看黑马教学和自己学校教的知识结合写的,其中有知识点,也有自己的一点小理解。因为文档比较长,比较耗时,可根据自己需求选择章节观看。文章目录前言第一章:Java的简单介绍一、Java语言的发明二、Java的三个体系三、Java的发展史四、Java语言的特点五、Java的两种核心机制六、JDK、JRE、JVM三者之间的区别和联系第二章:Java开发环境搭建一:安装JDK二: 配置JDK环境变量三: IDEA安装第三章:Java基础语法一:Java 的三种注释二:Jav
SSM整合新建数据库:程序员90%的操作是增删改查,只是花样不同【固定配置直接拷贝即可】依赖:junit,数据库驱动,连接池,servlet等静态资源导出:导入一些properties,xml文件连接数据库建包:pojo,dao,service,controller实体类:dao下的mapper:对应的xml文件:一个xml对应一个接口,声明namespace写...
Cesium 上手不完全指北将最近学习的 CesiumJS 做一个系统梳理,从项目配置开始,记录常用 API 的使用。环境搭建与安装首先,什么是 Cesium,Cesium 是一款开源的基于 JavaScript 的 3D 地图框架,即地图可视化框架。产品基于 WebGL 技术,可以使用 CesiumJS 创建虚拟场景的 3D 地理信息平台。其目标是用于创建以基于 Web 的地图动态数据可视化。在提升平台的性能、准确率、虚拟化能力、易用性方面提供各种支持。更多介绍和信息可通过官网进行学习。注册
在某个项目中需要调研下node-red的功能,我大概花了三天时间研究了相关的官方文档,写了几个Demo总结了下node-red相关的功能。如需转载,请注明出处 https://www.cnblogs.com/pengdai/p/9143617.html —–2017年12月 @pdai内容目录内容目录IntroductionFeaturesBrowser-based...
Mybatis-Plus配置日志我们所有的sql现在是不可见的,我们希望知道它是怎么执行的,所以我们必须要看日志!# 配置日志mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl配置完毕日志之后,后面的学习就需要注意这个自动生成的SQL,你们就会喜欢上Mybatis-Plus!CRUD扩展插入操作insert插入 @Test//测试插入 public void t
Postgresql源码解读参考(转)参考网址http://blog.itpub.net/6906/viewspace-2662026/参考网址2:https://blog.csdn.net/ascoders/article/details/96472826
概述其实,计算机软件的用户界面(User Interface,UI)和用户体验(User eX-perience,UX)是一个有着丰富内容的学术领域,软件工程师们在长期工作中也积累了很多相关的经验无论软件还是硬件,都有很多功能部件,各个部件还要有机地结合起来,才能满足用户的需求。在用户体验领域中,一个著名的用户体验的例子是茶壶茶壶有什么功能部件呢?茶壶盖,茶壶体,茶壶把,茶壶嘴下面的茶