算法|010 python数据结构与算法(算法概论;时间复杂度)

数据结构与算法 是什么? 数据结构和算法在面对问题时能提供解决问题的思路;会在解决问题后对程序运行的效率和开销进行提升和优化;能在借用第三方工具和开源框架后,提供有针对性优化的方案。总之,数据结构和算法帮助我们提高编程能力,是开发人员必备基本功。
算法 当用代码解决问题:有a+b+c=1000,且a ^2 + b ^2=c ^2(a,b,c为自然数),如何求出所有a,b,c可能的组合?
首先的思路是:a,b,c三个数自己一个一个的去试(枚举法),先改变c的值,再去改变b的值,最后改变a的值,使其满足题目中的两个条件。
通过代码表示就是嵌套三个循环:

#测试枚举法 #如果a+b+c=1000,且a^2+b^2=c^2(a,b,c为自然数),如何求出所有a,b,c可能的组合? import time start_time=time.time() for a in range(0,1001): for b in range(0,1001): for c in range(0,1001): if a+b+c==1000 and a**2+b**2==c**2: print("a,b,c:%d,%d,%d"%(a,b,c)) end_time=time.time() print("time:%d"%(end_time-start_time)) print("finished")

运行结果:
a,b,c:0,500,500 a,b,c:200,375,425 a,b,c:375,200,425 a,b,c:500,0,500 time:201 finished

可以通过运行由最基础思路完成的代码发现,找出所有的组合运行时间很长,效率低下。因此还需要去优化代码。
此时我们可以考虑只设置两个变量,用前两个变量去表示第三个变量,以减少嵌套循环的层数。
import time start_time=time.time() for a in range(0,1001): for b in range(0,1001): c=1000-a-b if a**2+b**2==c**2: print("a,b,c:%d,%d,%d"%(a,b,c)) end_time=time.time() print("time:%d"%(end_time-start_time)) print("finished")

运行结果:
a,b,c:0,500,500 a,b,c:200,375,425 a,b,c:375,200,425 a,b,c:500,0,500 time:1 finished

由运行结果可知,相比于第一种解决思路,第二种方式明显提高了运算速度。
同一个问题,提升计算效率的解决问题的方案的思路就是算法。
算法的概念
【算法|010 python数据结构与算法(算法概论;时间复杂度)】算法是计算机处理信息的本质,因为计算机程序本质上是一个算法来告诉计算机确切的步骤来执行一个指定的任务。一般地,当算法在处理信息时,会从输入设备或数据的存储地址读取数据,把结果写入输出设备或某个存储地址供以后再调用。
算法是独立存在的一种解决问题的方法和思想。
对于算法而言,实现的语言并不重要,重要的是思想。算法可以有不同的语言描述实现版本。
算法的五大特性
  • 输入:算法具有0个或多个输入。
  • 输出:算法至少有1个或多个输出。
  • 有穷性:算法在有限的步骤之后会自动结束而不会无限循环,并且每一个步骤可以在可接受的时间内完成。
  • 确定性:算法中的每一步都有确定的含义,不会出现二义性。
  • 可行性:算法的每一步都是可行的,也就是说每一步都能够执行有限的次数完成。
算法效率衡量
再回看上面解决问题的代码,发现,不同计算方法之间效率是有差别的。
执行时间反应算法效率 对于同一个问题,我们给出了两种解决算法,在两种算法的实现中,我们发现两段程序执行的时间相差悬殊,由此我们可知:实现算法程序的执行时间可以反应出算法的效率,即算法的优劣。
时间复杂度与“大O记法” 单只依靠运行的时间来比较算法的优劣并不一定是客观准确的。因此我们引入新的概念。
假设计算机执行算法每个基本操作的时间是固定的一个时间单位,那么有多少基本操作就代表会花费多少时间单位。虽然对于不同的机器,确切的单位时间是不同的,但是对于算法进行多少个基本操作在规模数量级上是相同的,由此可以忽略机器环境的影响而客观的反应算法的时间效率。
对于算法的时间效率,我们可以用“大O记法”来表示。
“大O记法”:对于单调的整数函数f,如果存在一个整数函数g和实常数c>0,使得对于充分大的n总有f(n)<=c*g(n),就说函数g是f的一个渐近函数(忽略常数),记为f(n)=O(g(n))。也就是说,在趋向无穷的极限意义下,函数f的增长速度受到函数g的约束,也称函数f与函数g的特征相似。
时间复杂度:假设存在函数g,使得算法A处理规模为n的问题示例所用时间为T(n)=O(g(n)),则称O(g(n))为算法A的渐近时间复杂度,简称时间复杂度,记为T(n)
时间复杂度是描述算法效率的快慢的,最终的时间复杂度和问题规模(n)是有关系的。
对于算法的时间性质和空间性质,最重要的是数量级和趋势,这些是分析算法效率的主要部分,而计量算法基本操作数量的规模函数中那些常量因子可以忽略不计。
时间复杂度:只需要关心数量级,系数不影响走势。
时间复杂度:是运算步骤来衡量的。
最坏时间复杂度
分析算法时,存在几种可能的考虑:
  • 算法完成工作最少需要多少基本操作,即最优时间复杂度
  • 算法完成工作最多需要多少基本操作,即最坏时间复杂度
  • 算法完成工作平均需要多少基本操作,即平均时间复杂度
    对于最优时间复杂度,反映的只是最乐观最理想的情况,没有参考价值。
    对于最坏时间复杂度,提供了一种保证,表面算法在此种程度的基本操作中一定能完成工作。
    对于平均复杂度,是对算法的全面评价,因此它完整全面的反映了算法的性质。但这种衡量并没有保证,不是每个计算都能在这个基本操作内完成。而且,对于平均情况的计算,也会因为应用算法的实例分布可能并不均匀而难以计算。
    因此,我们主要关注算法的最坏情况,即最坏时间复杂度。
时间复杂度的几条基本计算规则
  • 基本操作,即只有常数项,认为其时间复杂度为O(1)
  • 顺序结构,时间复杂度按加法进行计算
  • 循环结构,时间复杂度按乘法进行计算
  • 分支结构,时间复杂度取最大值
  • 判断一个算法的效率时,往往只需要关注操作数量的最高次项,其他次要项和常数项可以忽略
  • 在没有特殊说明时,我们所分析的算法的时间复杂度是指最坏时间复杂度
算法分析 上述问题第一次尝试
for a in range(0,1001): for b in range(0,1001): for c in range(0,1001): if a+b+c==1000 and a**2+b**2==c**2: print("a,b,c:%d,%d,%d"%(a,b,c))

时间复杂度:T(n)=O(nnn)=O(n3)
第二次尝试:
for a in range(0,1001): for b in range(0,1001): c=1000-a-b if a**2+b**2==c**2: print("a,b,c:%d,%d,%d"%(a,b,c))

时间复杂度:T(n)=O(nn(1+1))=O(n2)
由此可见,我们尝试的第二种算法要比第一个算法的时间复杂度好很多。
常见的时间复杂度
执行次数函数举例 非正式术语
12 O(1) 常熟阶
2n+3 O(n) 线性阶
3n2+2n+1 O(n2) 平方阶
5log2n+20 O(longn) 对数阶
2n+3nlong2n+19 O(nlogn) nlogn阶
6n3+2n2+3n+4 O(n3) O(立方阶)
2n O(2n) 指数阶
注:经常将log2n(以2为底的对数)简写成logn
常见是复杂度消耗时间从小到大:
O(1) python内置类型性能分析 函数是对基本步骤的封装,所以调用函数不能算作是基本步骤,计算时间复杂度的时候不能直接将函数算作一个基本步骤,要分析函数集的代码,才能计算出时间复杂度。
python内置的函数调用时,也还是有时间优劣之分。
timeit模块
timeit模块可以用来测试一小段python代码的执行速度。
class timeit.Timer(stmt='pass,setup='pass',timer=)
Timer是测量小段代码执行速度的类。
stmt参数是要测试的代码语句(statement);
setup参数是运行代码时需要的设置;
timer参数是一个定时器函数,与平台有关。timeit.Timer.timeit(number=1000000)
Timer类中测试语句执行速度的对象方法。number参数是测试代码时的测试次数,默认为1000000次。方法返回执行代码的平均耗时,一个float类型的秒数。
利用python编程时,文件名和包名不能重名,因为有时候会调用对应的包。
在python2中range返回的是列表对象,python3中range返回的是一个可迭代对象。
在python中列表构造有4中方式:
#列表构造 li1=[1,2] li2=[3,4] li=li1+li2#+操作符,拼接 li=[i for i in range(10000)]#列表生成器 li=list(range(10000))#把可迭代对象直接转换成列表 li=[]#往空列表中追加元素 for i in range(10000): li.append(i)

不同的构造方法的效率也有不同,通过timeit测试对应构造列表函数的效率:
from timeit import Timerdef t1(): li=[] for i in range (10000): li.append(i) def t2(): li=[] for i in range(10000): li=li+[i] def t3(): li=[i for i in range(10000)] def t4(): li=list(range(10000)) def t5(): li=[] for i in range(10000): li.extend([i]) #通过字符串方式调用函数 timer1=Timer("t1","from __main__ import t1") print("append:",timer1.timeit(1000))#append方法调用的时间 timer2=Timer("t2","from __main__ import t2") print("+:",timer2.timeit(1000))#+操作的时间效率 timer3=Timer("t3","from __main__ import t3") print("[i for i in range]:",timer3.timeit(1000))#列表构造器的时间效率 timer4=Timer("t4","from __main__ import t4") print("list(range()):",timer4.timeit(1000))#将可迭代对象直接转换成列表 timer5=Timer("t5","from __main__ import t5") print("extend:",timer5.timeit(1000)) #extend是接受列表和所有可迭代的元素,不创建新对象,只在原对象上进行修改。

运行结果:
append: 2.673400000000048e-05 +: 3.287400000000218e-05 [i for i in range]: 2.3255999999999277e-05 list(range()): 2.5767000000002926e-05 extend: 2.9659000000001323e-05

由运行结果可知,append只能接收单个元素,在添加元素时创造了新的对象;+操作时列表的拼接,效率比较低。在进行添加操作时,建议不使用+(因为运算速度较慢)
测试append添加列表和insert添加列表:
def t6(): li=[] for i in range(10000): li.append(i) def t7(): li=[] for i in range(10000): li.insert(0,i) timer6=Timer("t6","from __main__ import t6") print("append:",timer6.timeit(1000))#append添加列表默认从尾部插入 timer7=Timer("t7","from __main__ import t7") print("insert(0):",timer7.timeit(1000))#insert添加列表,(0,i)设置为从头部插入

运行结果:
算法|010 python数据结构与算法(算法概论;时间复杂度)
文章图片

由此可知,从尾部添加元素比从头部添加元素快很多。
pop操作测试
from timeit import Timerx=list(range(2000000)) pop_zero=Timer("x.pop(0)","from __main__ import x") print("pop_zero",pop_zero.timeit(number=1000),"seconds") x=list(range(2000000)) pop_end=Timer("x.pop()", 'from __main__ import x') print("pop_end",pop_end.timeit(number=1000),"seconds")

运行结果:
算法|010 python数据结构与算法(算法概论;时间复杂度)
文章图片

从运行结果可以看出,pop最后一个元素的效率远远高于pop第一个元素。原因是pop第一个元素涉及后面元素的前移。
list内置操作的时间复杂度 算法|010 python数据结构与算法(算法概论;时间复杂度)
文章图片

dict内置操作的时间复杂度 算法|010 python数据结构与算法(算法概论;时间复杂度)
文章图片

数据结构 算法是一种思想,关注的是解决问题的方法步骤,没有关心处理的是什么数据。数据结构解决的问题:现在的数据怎么去整合起来。
示例:用python中的类型来保存一个班的学生信息?如果想要快速的通过学生姓名获取其信息呢?
学生信息可以用列表、字典、元组来存。
列表:
[("zhang",21,"beijing"), ("wang",22,"shanghai"), ("lee",23,"shenzheng")] [ { "name":"zhang", "age":21 "hometown":"beijing" } ]

用字典来存学生信息,因为字典也是集合。在字典里用姓名作为键,年龄和家乡信息作为嵌套其中的字典:
{ "zhang":{ "age":21, "hometwon":"beijing" } }

假设在列表学生中无重名,查找一位学生,需要遍历列表,判断姓名元素是不是指定值。处理数据时,数据保存的类型不一样,查找和处理的算法就不一样。数据组织方式和算法之间是分不开的,数据组织方式就是数据结构。数据结构解决的就是数据的保存方式。
概念
数据是一个抽象的概念,将其进行分类后得到程序设计语言中的基本类型。如:int、float、char等。数据元素之间不是杜立德,存在特定的关系,这些关系便是结构。数据结构指数据对象中数据元素之间的关系。
python提供了很多现成的数据结构类型,这些系统定义好的,不需要我们自己去定义的数据结构叫做python内置数据结构,比如:列表、元组、字典。而有些数据组织方式,python系统里没有直接定义,需要我们自己去定义实现这些数据的组织方式,这些数据组织方式称为python的扩展数据结构,比如栈、队列等。
算法与数据结构的区别
数据结构知识静态的描述了数据元素之间的关系。
搞笑的程序需要在数据结构的基础上设计和选择算法。
程序=数据结构+算法
因此我们说:算法是为了解决实际问题而设计的,数据结构式算法需要处理的问题载体。
有一组数据时,其中的每一个元素都是基本元素,如何组合在一起就是数据结构。python中列表,字典,元组,等是基本类型的一种组合,是python封装的一种高级数据结构,不是简单的一种基本数据类型。
抽象数据类型
抽象数据类型(Abstract Data Type)的含义是指一个数学模型以及定义在此数学模型上的一组操作。即把数据类型和数据类型上的运算捆在一起,进行封装。引入抽象数据类型的目的是把数据类型的表示和数据类型上运算的实现与这些数据类型和运算在程序中的引用隔开,是它们互相独立。
最常用的数据运算有五种:
  • 插入(insert)
  • 删除(pop)
  • 修改(modify)
  • 查找(select)
  • 排序(sort)
抽象数据类型:先规定这个数据怎样保存,数据保存好后,定义数据的操作,还需要定义数据调用的接口。

    推荐阅读