做算法分析前需要明白程序和它所代表的算法是不同的。算法是给定某个输入能得到对应的结果,是解决问题的方法,程序则是用某种编程语言对算法编码。同一个算法可以用python、java、C语言等各种编程语言写出来,即便在同一种语言中,一个算法也可以对应多个程序。
以前我对代码可读性并没有太多的感触,直到看到同一个算法用不同的程序写出来才明白代码可读性是那么的重要,下面是同一种算法计算前n个整数之和,算法的思路是使用一个初始值0的累加器变量,然后遍历n个整数,并将值加到累加器上:
程序1
def sumOfN(n):
theSum=0
for i in range(1,n+1):
theSum=theSum+i
return theSum
sumOfN(10)
程序2
def foo(tom):
fred=0
for bill in range(1,tom+1):
barney=bill
fred=fred+barney
return fred
foo(10)
程序2的变量名和多余的赋值语句让这段程序代码很难读,不过两段代码从算法上考虑是一样的。
算法分析关心的是基于所使用的计算资源比较算法,追求更高的资源利用率和使用更少的资源。
计算资源:一是考虑算法解决问题占用的空间或内存,另一种是根据算法执行所需的时间进行分析和比较。
要通过执行时间对算法进行比较可以对算法进行基准测试,通过记录开始和结束时间差可以计算出每个算法所需要的时间。但是不同的计算机、程序、编译器、编程语言都会影响算法的执行时间,所以我们需要找到一个不受这些影响的比较指标,这样才能更好的比较不同实现下的算法优劣。
以前了解过大O表示法,知道大概的意思,但是有些细节并不了解,具体是选定了什么指标具体怎么计算出来的也有点云里雾里的,这次对大O表示法有了更深入的了解。
首先,算法的时间度量指标是什么,仔细观察程序设计语言特性,除了与计算资源无关的定义语句外,主要就是三种控制流语句(顺序、分支判断、循环)和赋值语句,而控制流语句仅仅起到了组织语句的作用,并不实时处理。赋值语句同时包含了(表达式)计算和(变量)存储两个基本资源,所以赋值语句是一个合适的选择。
选定了度量指标然后对于前面的累加算法计算总和所用的赋值语句的数目。赋值语句是1(theSum=0)加上n(theSum=theSum+i的运行次数)。可以将其定义为T,令T(n)=1+n。从T(n)可以看出,影响算法执行时间的主要因素是n,我们将其称为问题规模。这样一来就可以说处理1000000个整数的问题规模比处理1000个整数的问题规模大,所以前者话的时间比后者花的时间长。算法分析的目标就是要找出问题规模会怎么影响一个算法的执行时间。
从T(n)=1+n来看,随着n不断增加,1对算法的执行时间起的作用越来越小,也就是说T(n)函数中某一部分比其余部分增长得更快,为了方便比较,我们只需要比较这起决定性的部分就可以了。然后引出了一个数量级函数的感念,也被称为大O记法(O是指order),记作O(f(n)),也就是记录步骤数的近似方法。所以T(n)可以直接舍去1,直接说执行时间是O(n).
关于大O还有一个重要的内容需要知道的是如何将一段程序用大O表示法记录步骤。
下面给出一个例子:
a=5
b=6
c=10 #三条赋值语句:3
for i in range(n):
for j in range(n): #两个循环都是n,因为是嵌套循环是n**2
x=i*i
y=i*j
z=j*j #循环里有3条赋值语句所以是3*n**2
for k in range(n):
w=a*k+45#两条赋值语句在一个n循环里,所以是2n
v=b*b
d=33 #一条赋值语句;1
根据上面的内容看出:
T ( n ) = 3 + 3 n 2 + 2 n + 1 = 3 n 2 + 2 n + 4 T(n)=3+3n^2+2n+1=3n^2+2n+4 T(n)=3+3n2+2n+1=3n2+2n+4
很容易看出随着n增加,n的平方起主导作用,所以这段代码的时间复杂度是
O ( n 2 ) O(n^2) O(n2)
在实现列表、字典数据结构时,python设计师考虑将最常见操作变得非常快,也就是让最常用的操作性能最好,牺牲不太常用的法则。
列表最常用的是:按索引取值和赋值,另一个常见的操作是加长列表,这些的大O效率是O(1),也就是常数阶。加长列表有两种方式,要么采用追加,要么采用连接擦欧洲哦。追加方式是常数阶,如果待连接列表长度为k,那么连接操作的时间复杂度是O(k)。
字典的取值操作和赋值操作是常数阶,另一个重要的字典操作就是包含(检查某个键是否在字典中)也是常数阶。删除某个键值对也是常数阶。
证明列表索引为常数阶需要计算时间,另外就是当列表的元素个数增加时查询具体索引的时间基本不变。
这里需要使用内置模块timeit,timeit模块会统计多次执行语句要用多久,默认会执行100万次,并在完成后返回一个浮点数格式的秒数。
首先创建一个Timer对象,其参数是两条python语句,第一个参数是要统计时间的python语句,第二个是建立测试的语句。
from timeit import Timer
t1=Timer('x[200]','from __main__ import x')
for i in range(100000,1000001,100000):
x=list(range(i))
pt=t1.timeit(number=1000)#默认100万可以通过number修改外1000次
print('%.5f'%pt)#%.5f保留5位小数
在循环10万到100万,每次增长10万,也就是循环10次,每次统计所需时间,最后都是0.00006秒。
这里有几点需要说一下,Timer第二个参数测试语句可能会感觉有些奇怪,学过python基础可能都知道from和import,但是本例中’from __main__ import x
'可能就是第一次见了。这个是为了将x从__main__
命名空间导入到timeit设置计时的命名空间。这么做是为了在一个干净的环境中运行测试,一面某些变量以某种意外的方式干扰函数的性能。
刚开始写的时候也是出错好几次,遇到新模块时就会自乱阵脚将自己学过的一时忘掉了,为什么结果没循环,t1=Timer()是不是该放到循环里,也是尝试了好几次才成功的。这里需要记住的就是接触新知识点时不要乱了分寸把过去学过的也忘了,把握住学过的再结合新的知识才是更好的学习方法。