算法和算法分析python
先说点可有可无的。初中的时候,知道有CS这门专门的学科存在的时候最开始的概念中CS就是等同于算法。这有多是由于当时的前桌是后来一代CS传奇WJMZBMR。。由于当时看起来十分高端,再加上后来努力的方向彻底和CS不搭边,因此对于算法二字一直心中抱着一种敬畏之情,以为是整个CS中最干的干货部分。后来决定入这行以后,个人领导对我说算法这东西虽然很高大上,可是在平常工做中咱们用的并很少(咱们部门主要作运维和DevOps,确实对这方面的需求不大)因此也就一直耽搁着。可是随着深刻,以及在网上和各类场合查阅到愈来愈多的资料中接触算法和一些算法术语愈来愈频繁。我以为是时候学习一下了。因此我就买了一本北大一位先生著的数据结构与算法~Python语言描述~,一方面想了解一点算法和数据结构的知识,另外一方面也能够学习一下Python。这本书不是很厚,做者也说了没有涉及很高深的知识,虽然我也不必定能学会学好,可是我想,努力一下试试看把,什么都不学总比在床上刷刷B站,玩玩游戏要好一点。算法
■ 问题,问题实例和算法编程
要搞清楚算法的一些概念,区分这三个概念十分重要。问题对应的是一种需求,对应一种需求,人们能够经过分析和推断来抽象出一个计算机须要解决的问题。问题具备通用的特色,好比判断一个正整数是否为素数这就是一个问题。问题实例通俗点来讲就是一个具体的问题,它很明确的指出一个问题的很具体的描述,通常具备正确解。好比1013这个正整数是否为素数,相对于上面的那个问题,就是一个问题实例。显然,一个问题反映出全部相关问题实例的共性。算法是对解决问题过程的一个严格的描述。由于算法是和问题对应的,因此这个问题的全部实例均可以用算法来求得解。好比设计一个判断某个正整数是否为素数的算法A,这个算法A对应的是上面的问题,固然把这个算法A套用在问题实例中,咱们就能够得出1013以及其余各类各样的正整数是否是素数了。设计模式
● 算法具备的性质数据结构
算法是一种对问题解决过程的具体描述,为了使其严格有效,算法一般具备如下性质app
有穷性(算法描述的有穷性):算法应该能够用有限的语言,尤为是语言中有限的祈使(或者计算机语言中就是指令)进行描述。运维
可行性:算法中的的指令必须清晰明确,所描述的过程彻底能够经过机器来机械地执行。函数
肯定性:根据某个问题(一般也是以问题实例的形式给出,经过对问题实例的分析以及配合算法的测试来抽象出一个问题),算法将产生一个惟一肯定的动做序列,任何一个相关问题的实例经过这个肯定的动做序列以后,就能够获得相关解性能
终止性(算法行为的有穷性):对于任何实例,算法产生的动做序列都是有限的学习
输入/输出:算法有明确的输入和输出
● 算法的描述形式
算法能够用天然语言描述,这样对不了解计算机语言的人比较友好,可是一般比较啰嗦并且容易出现歧义
若是用计算机语言描述算法,能够作到很精确(由于最终算法就是以这种形式呈现到程序中的。)可是对于通常阅读者,即便是懂计算机语言的人而言阅读起来也须要一些力气,能够说对阅读者不是很友好
折中一下,用伪代码的形式描述。伪代码结合了计算机语言(一般用于逻辑结构的表示)和天然语言(对于具体的内容操做表示)。伪代码描述的形式结合了天然语言的表达友好和机器语言的简洁清晰。
■ 算法设计与分析
所谓算法设计,就是从一个问题出发,经过分析和思考来获得一个可以解决问题的算法。算法设计中有一些常见的设计模式:
枚举法:列举出问题全部可能的解并从中筛选出合适的解。这种方法能够说是利用了计算机强大的计算性能。电脑比人脑高明之处就在于它能够快速地重复大量类似或者相同的工做,以快速获得结果。
贪心发:根据问题的信息获得部分解,认为部分解能够做为解的一种或者把部分解逐步扩充获得完整解。在问题比较复杂时适用が,一般找到的解并非最好的解
分治法:把问题分解成一个个小问题,而后逐步解决这些小问题,组合每一个小问题的解获得整个问题的解
回溯法:当问题解决没有清晰的路径时,程序须要逐步试错,当发现一种方法走不通的时候就要回溯到以前的路径点来尝试新的路径
动态规划:当问题很难直接了当地求解,须要更多信息时,能够在解决问题的过程当中逐渐积累信息,这些信息能够为后面问题解决过程所用,然后面的这些过程又能够进一步地积累到更多的信息
分支限界法:能够看作是回溯法的一种优化,在搜索过程当中可能会获得一些没有用的信息,就把这些信息给删除以减少求解成本
以上算法设计模式,并非严格推导出来的,而是前人在无数的实践中的一些总结,固然这些描述也是很是抽象的,光看下来可能无法知道任何有用的信息。可是须要记住的是,真正的算法设计过程一般须要综合考虑多种设计模式。
另,算法实现为一个程序以后就须要开始运算,而运算做为处理信息的一种过程确定是要有运算消耗的。好比时间上的消耗和空间上的消耗。这种消耗除了跟算法有关,跟硬件状况,运行环境,实现方式(哪一种语言)等等。在以上条件都相同的状况下,算法就肯定一个程序的消耗。消耗越小的算法,其运行效率固然也就越高了。
当咱们设计出一个算法以后,咱们就须要分析这个算法是否是够高效。在有些状况下,由于计算机的高效的特性,算法是否高效可能显得不是那么有意义,可是在更多时候,极可能决定了算法有没有存在的价值。好比一个算法要花三天算出明天的天气预报和三小时算出明天的天气预报意义彻底不同。为了衡量算法是否是高效,咱们还须要一种度量。
● 算法度量的单位和方法
在计算过程当中,硬件每执行算法中的一个操做所带来的时间上和空间上的消耗都是不一样的,而为了算法度量可以有必定通用性(比较不一样操做算法的效率),在制定算法度量的时候就须要必定抽象,好比下面的两条假设:
1. 所用的计算设备准备了一组储存单元,每一个单元都能保存固定的一点有限数据(以此标准化空间上的消耗)
2. 机器可以执行的一次基本操做都是消耗一个单位时间(以此标准化时间上的消耗)
假设中提到的储存单元的大小,以及单位时间的长短,可能根据硬件,环境等条件不一样而不一样,可是这不是算法度量须要考虑的。在算法比较中,一般默认是比较除了算法以外,其余条件都彻底相同的两个程序的执行效率。因此能够借助上面两条假设把算法度量抽象化,标准化。
虽然算法是针对地解决问题的,可是机器不可能看得懂一个问题的描述,因此一般算法度量仍是得以具体的问题实例来进行。这就带出一个概念,问题规模。好比解1013是否为素数和10331310131是否为素数这两个问题,显然二者能够套用同一套算法,可是二者的消耗彻底不一样。对于这样一种算法,到底算高效仍是不高效,并非经过一个问题实例的具体消耗能决定的。因此,算法的度量一般是一种计算资源消耗和问题规模相关的函数关系。若是问题规模很小,不论用哪一种算法的消耗都差很少,且在能够接受的范围内,那么算法度量就显得不是那么有意义。而当问题规模愈来愈大时,若是计算消耗增加得愈来愈快,那么就能够说算法的效率不是太好,应该避免。问题规模在上面求素数那两个实例中,能够认为是数的大小,或者数的位数等等,通常来讲只要有问题实例的一个统一的度量,具体这个度量是什么并非很重要。总之能看出来哪些问题规模较大哪些较小便可。
另外还须要注意的一点,即便是规模相同的问题实例,在有些算法中消耗也是不一样的。好比判断1013和1012是不是素数的话,好比在算法中最开始添加一个判断:若是是偶数就直接返回否,这样二者消耗就相差不少了。对于这种状况,其实对于规模相同的问题实例,咱们一般关注的是最坏的状况下算法的消耗(有时候也会关注平均消耗),可是不太会关注比较乐观的状况下的消耗。
● 算法复杂度
算法复杂度就是一种算法的度量方法。如上所说,对于抽象的算法一般没法给出精确地度量,因此要作的是估计算法的复杂性,而算法复杂性量化一点说就是算法的消耗处在的量级(由于不论在何种外部条件下,算法度量中的单位时间和空间都是很小的,因此多一个少一个不是颇有所谓)。在估算量级的过程当中,常量因子能够认为没有什么价值,好比100n**2和3n**2都是n**2量级的(n是问题规模的描述)。这里借用了微积分中经常使用的无穷小的概念,并采起了无穷小的记法f(n) = O(g(n))。f(n)就是算法复杂度这个算法度量(一个消耗关于问题规模的函数),而g(n)是相似于n**2,logn,n,1(常量函数)的一个关于问题规模的n的函数。把g(n)记入大O代表算法复杂度f(n)随着n的增加,其增加速度受到g(n)的限制。两个算法,只要其g(n)相同,就能够认为两个算法的量级相同,就认为二者的复杂度基本同样。
经常使用的g(n)有1,logn,n,nlogn,n**2,n**3,2**n。这几个函数从前到后其增加速率逐渐变快。具备这些g(n)的算法复杂度也被称为常量复杂度,对数复杂度,平方复杂度,指数复杂度等等。假如一个算法A1是对数复杂度而A2是平方复杂度,一般而言一样规模的问题实例用A1算法进行运算的消耗要远小于用A2算法计算的消耗(固然这只是一般,上面也说了算法度量只是关注最坏状况,假如某个实例恰好是A2的乐观状况那么可能A2很快就能算出来了)
● 算法分析
算法分析就是经过一个已知的算法来得出其复杂度的过程。以考虑时间开销的时间复杂度为例,从算法层面看,一个普通的程序一般包含了基本操做,顺序结构,循环结构和选择结构这几种结构。
基本操做的复杂度一般认为是常量复杂度,好比赋值,四则运算,以及这些的组合都是基本操做。
顺序结构是指多个操做按顺序复合的状况。一般其复杂性是每一步操做复杂性的总和。
循环结构的复杂度是循环头的复杂度乘以循环体的复杂度。
选择结构的复杂度是各个选择子句中最大复杂度(这里又体现出考虑最坏状况)
好比这样一个Python程序:
#把n阶矩阵m1和m2的乘积存入矩阵m for i in range(n): #O(n) for j in range(n): #O(n) x = 0.0 #O(1) for k in range(n): #O(n) x = x + m1[i][k] * m2[k][j] #O(1) m[i][j] = k #O(1)
其复杂度T(n)是:
T(n) = O(n)*O(n)*(O(1)+O(n)*O(1)+O(1)) = O(n)*O(n)*O(n) = O(n**3)
能够看到python中的for i in range(n)这样的语句,由于是遍历一个长度为n的列表,其复杂度n个O(1)相加,即O(n)。循环头的O(n)再拿去乘以循环体的复杂度,嵌套循环则用括号在算式中也嵌套。在获得算式后面的化简过程遵循无穷小之间的运算规律。好比括号中只考虑阶最高的无穷小,相加的低阶无穷小被忽略了,相乘的无穷小则其参数互相相加。
最终能够获得这条算法的复杂度是立方复杂度。
■ Python的复杂度
上面所说的算法复杂度是泛泛而谈,具体到Python中又有一些特殊的状况。好比Python做为一门比较高级(相对底层而言)的语言,它已经提供了不少包装好了的“基本操做”。在用这些操做的时候,有时候咱们会觉得咱们作的是一个基本操做可是实际上有多是一个复杂度并不是为O(1)的操做。下面是一些简单的说明,具体的分析留到后面具体的章节中
基本运算和赋值是基本操做,复杂度是O(1)
序列的复制和切片操做是O(n),跟序列的长度有关
list,tuple的元素访问、赋值和修改都是O(1)
构造一个空的对象是O(1),若是像是list,str这种类型构造时若是指定了长度为n的内容那么就是O(n)
dict加入新键值对最坏状况下是O(n)可是平均状况下的复杂度是O(1)
以上复杂度都是针对时间消耗而言。对于空间消耗须要注意的是
Python中对于各类组合元素(一般是指str,list,tuple,dict等python自带的高级一点的数据类型)都没有预设最大元素个数。但在实际使用中,从内存角度看元素个数长度只会增不会减。好比li = [1,2,3]以后li中确实是有3个元素的长度。若是li.append(4)以后就是4个长度。这很好理解。可是若是此时del(li[3])以后,虽然len(li)变成了3可是内存中的li对象依然保持4个元素 的长度,这是须要注意的
Python中的数据结构
■ 什么是数据结构
书上有很大一堆比较学术性的解释。在个人体验中,我认为所谓数据结构就是人为地规定一些数据格式来方便对问题的抽象和编程。从集合论来看,通常而言,一个数据结构D = (E,R)。其中E表示一个数量有穷的数据集合而R表明E中这些数据之间的某种关系。换言之,一个具体的数据结构就是要有具体的数据和这些数据之间的逻辑关系。
一些典型的数据结构有:
集合结构:其数据元素之间没有明确的关系指定,即R是一个空集,这样的数据结构就是把元素包装成一个总体,是最简单的一类数据结构
序列结构:数据元素之间有明确的前后关系,存在一个排位在最前的元素。除了最后的元素以外每一个元素都有惟一的后元素。序列结构还能够细分红简单线性结构,环形结构和ρ型结构
层次结构:其数据元素分属于一些不一样的层次,一个上层元素能够关联着一个或者多个下层元素,关系R造成一种层次性。
树形结构:属于层次结构的一种。
图结构:数据元素之间能够有任意的互相关联,其R十分复杂且灵活多变,是一类复杂的数据结构。其实前面全部的数据结构均可以认为是图结构的一种简化或限制的状况
根据数据结构的不一样特色,还能够细分数据结构结构性数据结构和功能性数据结构。结构性数据结构(Python中如list,str,tuple)等,结构性数据结构指出的是一种有具体结构要求的数据结构。功能性数据结构没有结构上死的规定,能够看作是容器同样支持存放数据,而后利用其特性进行一些运算,功能性数据结构的例子有栈,队列,优先队列等等。
■ 内存单元和地址
(不知道为何这部份内容要放在数据结构中。。)
内存的基本结构是一批线性排列的数据单元,每一个单元有惟一的编号被称为单元地址,对内存中的数据进行访问必需要知道相关单元的地址。在许多计算机中,能够一次性存取多个单元的内容,在如今常见的64位计算机中,CPU一次能够存取8个字节的数据,也就是说能够一次性访问8个数据单元。
正如上面提到过的大多组合数据类型存取值是一个O(1)的操做,这也就说明了,基于单元地址的对内存中一个存储单元的访问是一个O(1)的操做,这和单元所在位置,内存总体大小等无关。
■ Python对象和数据结构
● python中的变量和对象
对于初学Python的人来讲这两个概念常常容易搞混,其实Python在数据存储的本质上来讲和C,Java等语言是不一样的。在Python中,给变量约束一个值看似和C中差很少,可是实际上,python首先把这个值构形成一个对象存储在内存中,而后把这个内存中对象的地址约束给相应的变量。因此在Python中,咱们不须要指出某个变量的类型和它应该有的长度,由于不管是什么变量,都存的是一个地址,全部变量所须要的空间大小是同样的。而那个地址指向的内存储存单元(或者以该单元为开始的一片内存空间中)储存的才是真的数据。这种变量的实现方式被称为变量的引用语义。而像C同样把值直接存在变量的储存区的作法被称为变量的值语义。
在Python中,经过变量来取得一些具体数据的操做也是O(1)的因此这方面的消耗并不比低级语言大不少。
● Python中对象的表示
表示是指为了让电脑可以更好的理解逻辑数据的构造的数据结构。Python中的对象的表示实际上是已经设计完成,不须要咱们太多关心的,可是了解一下有利于咱们更好地进行工做。
Python语言的实现基于一套精心设计的连接结构,变量与其值对象的关系经过连接的方式实现,对象之间的联系一样也经过连接。一个复杂的对象内部也可能包含了几个子部分。相互之间经过连接创建联系,例如一个list中包含了10个字符串的话,在实现中,这个list在内存中其实保存了这10个字符串各自的连接关系。
Python中的组合对象能够是任意大规模的,每一个对象须要的储存单元数量不一样,还能够有内部的复杂结构。对于这样一种复杂的状况,要有效地安排,管理内存是比较麻烦的。不过好在Python自带了一套存储管理系统,负责管理可用内存,释放再也不使用的内存,安排各类对象的存储以实现灵活有效的内存管理。程序中要求创建对象时,管理系统会为它安排存储;当某些对象再也不使用时,就回收其占有的内存。存储管理系统屏蔽了具体内存使用的细节,减轻了编程人员的负担。