什么是算法
算法是独立存在的一种解决问题的方法和思想。
算法的五大特性
输入: 算法具有0个或多个输入
输出: 算法至少有1个或多个输出
有穷性: 算法在有限的步骤之后会自动结束而不会无限循环,并且每一个步骤可以在可接受的时间内完成
确定性:算法中的每一步都有确定的含义,不会出现二义性
可行性:算法的每一步都是可行的,也就是说每一步都能够执行有限的次数完成
第一次尝试:
算法效率衡量
1.执行时间反应算法效率
对于同一问题,我们给出了两种解决算法,在两种算法的实现中,我们对程序执行的时间进行了测算,发现两段程序执行的时间相差悬殊(214.583347秒相比于0.182897秒),由此我们可以得出结论:实现算法程序的执行时间可以反应出算法的效率,即算法的优劣。
2.单靠时间值绝对可信吗?
假设我们将第二次尝试的算法程序运行在一台配置古老性能低下的计算机中,情况会如何?很可能运行的时间并不会比在我们的电脑中运行算法一的214.583347秒快多少。
单纯依靠运行的时间来比较算法的优劣并不一定是客观准确的!
程序的运行离不开计算机环境(包括硬件和操作系统),这些客观原因会影响程序运行的速度并反应在程序的执行时间上。那么如何才能客观的评判一个算法的优劣呢?
3.时间复杂度与“大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)
4.如何理解“大O记法”
对于算法进行特别具体的细致分析虽然很好,但在实践中的实际价值有限。对于算法的时间性质和空间性质,最重要的是其数量级和趋势,这些是分析算法效率的主要部分。而计量算法基本操作数量的规模函数中那些常量因子可以忽略不计。例如,可以认为3n2和100n2属于同一个量级,如果两个算法处理同样规模实例的代价分别为这两个函数,就认为它们的效率“差不多”,都为n2级。
5.最坏时间复杂度
分析算法时,存在几种可能的考虑:
算法完成工作最少需要多少基本操作,即最优时间复杂度
算法完成工作最多需要多少基本操作,即最坏时间复杂度
算法完成工作平均需要多少基本操作,即平均时间复杂度
对于最优时间复杂度,其价值不大,因为它没有提供什么有用信息,其反映的只是最乐观最理想的情况,没有参考价值。
对于最坏时间复杂度,提供了一种保证,表明算法在此种程度的基本操作中一定能完成工作。
对于平均时间复杂度,是对算法的一个全面评价,因此它完整全面的反映了这个算法的性质。但另一方面,这种衡量并没有保证,不是每个计算都能在这个基本操作内完成。而且,对于平均情况的计算,也会因为应用算法的实例分布可能并不均匀而难以计算。
因此,我们主要关注算法的最坏情况,亦即最坏时间复杂度。
6.时间复杂度的几条基本计算规则
基本操作,即只有常数项,认为其时间复杂度为O(1)
顺序结构,时间复杂度按加法进行计算
循环结构,时间复杂度按乘法进行计算
分支结构,时间复杂度取最大值
判断一个算法的效率时,往往只需要关注操作数量的最高次项,其它次要项和常数项可以忽略
在没有特殊说明时,我们所分析的算法的时间复杂度都是指最坏时间复杂度
但是,我一次面试的时候,被问到某算法的时间复杂度,我脱口而出该算法的最坏时间复杂度,面试官说我错了,人家问的是最优时间复杂度......
算法分析
1第一次尝试的算法核心部分
1 for a in range(0, 1001):2 for b in range(0, 1001):3 for c in range(0, 1001):4 if a**2 + b**2 == c**2 and a+b+c == 1000:5 print("a, b, c: %d, %d, %d" % (a, b, c))
View Code
时间复杂度:
T(n) = O(n*n*n) = O(n3)
2.第二次尝试的算法核心部分
1 for a in range(0, 1001):2 for b in range(0, 1001-a):3 c = 1000 - a -b4 if a**2 + b**2 == c**2:5 print("a, b, c: %d, %d, %d" % (a, b, c))
View Code
时间复杂度:
T(n) = O(n*n*(1+1)) = O(n*n) = O(n2)
由此可见,我们尝试的第二种算法要比第一种算法的时间复杂度好多的。
常见时间复杂度
执行次数函数举例阶非正式术语
12
O(1)
常数阶
2n+3
O(n)
线性阶
3n2+2n+1
O(n2)
平方阶
5log2n+20
O(logn)
对数阶
2n+3nlog2n+19
O(nlogn)
nlogn阶
6n3+2n2+3n+4
O(n3)
立方阶
2n
O(2n)
指数阶
注意,经常将log2n(以2为底的对数)简写成logn
常见时间复杂度之间的关系
所消耗的时间从小到大
O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n3) < O(2n) < O(n!) < O(nn)
Python内置类型性能分析
timeit模块
timeit模块可以用来测试一小段Python代码的执行速度。
1 class timeit.Timer(stmt='pass', setup='pass', timer=)2 Timer是测量小段代码执行速度的类。3
4 stmt参数是要测试的代码语句(statment);5
6 setup参数是运行代码时需要的设置;7
8 timer参数是一个定时器函数,与平台有关。9
10 timeit.Timer.timeit(number=1000000)11 Timer类中测试语句执行速度的对象方法。number参数是测试代码时的测试次数,默认为1000000次。方法返回执行代码的平均耗时,一个float类型的秒数。
View Code
list的操作测试
1 deftest1():2 l =[]3 for i in range(1000):4 l = l +[i]5
6
7 deftest2():8 l =[]9 for i in range(1000):10 l.append(i)11
12
13 deftest3():14 l = [i for i in range(1000)]15
16
17 deftest4():18 l = list(range(1000))19
20
21 from timeit importTimer22
23 t1 = Timer("test1()", "from __main__ import test1")24 print("concat", t1.timeit(number=1000), "seconds")25 t2 = Timer("test2()", "from __main__ import test2")26 print("append", t2.timeit(number=1000), "seconds")27 t3 = Timer("test3()", "from __main__ import test3")28 print("comprehension", t3.timeit(number=1000), "seconds")29 t4 = Timer("test4()", "from __main__ import test4")30 print("list range", t4.timeit(number=1000), "seconds")31
32 #concat 1.8858006307834618 seconds
33 #append 0.0998275414804981 seconds
34 #comprehension 0.040617778672828786 seconds
35 #list range 0.01583742648539621 seconds
View Code
list内置操作的时间复杂度
dict内置操作的时间复杂度
数据结构
我们如何用Python中的类型来保存一个班的学生信息? 如果想要快速的通过学生姓名获取其信息呢?
实际上当我们在思考这个问题的时候,我们已经用到了数据结构。列表和字典都可以存储一个班的学生信息,但是想要在列表中获取一名同学的信息时,就要遍历这个列表,其时间复杂度为O(n),而使用字典存储时,可将学生姓名作为字典的键,学生信息作为值,进而查询时不需要遍历便可快速获取到学生信息,其时间复杂度为O(1)。
我们为了解决问题,需要将数据保存下来,然后根据数据的存储方式来设计算法实现进行处理,那么数据的存储方式不同就会导致需要不同的算法进行处理。我们希望算法解决问题的效率越快越好,于是我们就需要考虑数据究竟如何保存的问题,这就是数据结构。
在上面的问题中我们可以选择Python中的列表或字典来存储学生信息。列表和字典就是Python内建帮我们封装好的两种数据结构。
概念
数据是一个抽象的概念,将其进行分类后得到程序设计语言中的基本类型。如:int,float,char等。数据元素之间不是独立的,存在特定的关系,这些关系便是结构。数据结构指数据对象中数据元素之间的关系。
Python给我们提供了很多现成的数据结构类型,这些系统自己定义好的,不需要我们自己去定义的数据结构叫做Python的内置数据结构,比如列表、元组、字典。而有些数据组织方式,Python系统里面没有直接定义,需要我们自己去定义实现这些数据的组织方式,这些数据组织方式称之为Python的扩展数据结构,比如栈,队列等。
算法与数据结构的区别
数据结构只是静态的描述了数据元素之间的关系。
高效的程序需要在数据结构的基础上设计和选择算法。
程序 = 数据结构 + 算法
总结:算法是为了解决实际问题而设计的,数据结构是算法需要处理的问题载体
抽象数据类型(Abstract Data Type)
抽象数据类型(ADT)的含义是指一个数学模型以及定义在此数学模型上的一组操作。即把数据类型和数据类型上的运算捆在一起,进行封装。引入抽象数据类型的目的是把数据类型的表示和数据类型上运算的实现与这些数据类型和运算在程序中的引用隔开,使它们相互独立。
最常用的数据运算有五种:
插入
删除
修改
查找
排序
顺序表
1.顺序表的基本形式
图a表示的是顺序表的基本形式,数据元素本身连续存储,每个元素所占的存储单元大小固定相同,元素的下标是其逻辑地址,而元素存储的物理地址(实际内存地址)可以通过存储区的起始地址Loc (e0)加上逻辑地址(第i个元素)与存储单元大小(c)的乘积计算而得,即:
Loc(ei) = Loc(e0) + c*i
故,访问指定元素时无需从头遍历,通过计算便可获得对应地址,其时间复杂度为O(1)。
如果元素的大小不统一,则须采用图b的元素外置的形式,将实际数据元素另行存储,而顺序表中各单元位置保存对应元素的地址信息(即链接)。由于每个链接所需的存储量相同,通过上述公式,可以计算出元素链接的存储位置,而后顺着链接找到实际存储的数据元素。注意,图b中的c不再是数据元素的大小,而是存储一个链接地址所需的存储量,这个量通常很小。
图b这样的顺序表也被称为对实际数据的索引,这是最简单的索引结构。
2.顺序表的结构与实现
2.1顺序表的结构
一个顺序表的完整信息包括两部分,一部分是表中的元素集合,另一部分是为实现正确操作而需记录的信息,即有关表的整体情况的信息,这部分信息主要包括元素存储区的容量和当前表中已有的元素个数两项。
2.2顺序表的两种基本实现方式
图a为一体式结构,存储表信息的单元与元素存储区以连续的方式安排在一块存储区里,两部分数据的整体形成一个完整的顺序表对象。
一体式结构整体性强,易于管理。但是由于数据元素存储区域是表对象的一部分,顺序表创建后,元素存储区就固定了。
图b为分离式结构,表对象里只保存与整个表有关的信息(即容量和元素个数),实际数据元素存放在另一个独立的元素存储区里,通过链接与基本表对象关联。
元素存储区替换
一体式结构由于顺序表信息区与数据区连续存储在一起,所以若想更换数据区,则只能整体搬迁,即整个顺序表对象(指存储顺序表的结构信息的区域)改变了。
分离式结构若想更换数据区,只需将表信息区中的数据区链接地址更新即可,而该顺序表对象不变。
元素存储区扩充
用分离式结构的顺序表,若将数据区更换为存储空间更大的区域,则可以在不改变表对象的前提下对其数据存储区进行了扩充,所有使用这个表的地方都不必修改。只要程序的运行环境(计算机系统)还有空闲存储,这种表结构就不会因为满了而导致操作无法进行。人们把采用这种技术实现的顺序表称为动态顺序表,因为其容量可以在使用中动态变化。
扩充的两种策略
每次扩充增加固定数目的存储位置,如每次扩充增加10个元素位置,这种策略可称为线性增长。
特点:节省空间,但是扩充操作频繁,操作次数多。
每次扩充容量加倍,如每次扩充增加一倍存储空间。
特点:减少了扩充操作的执行次数,但可能会浪费空间资源。以空间换时间,推荐的方式。
3.顺序表的操作
3.1增加元素
如图所示,为顺序表增加新元素111的三种方式
a. 尾端加入元素,时间复杂度为O(1)
b. 非保序的加入元素(不常见),时间复杂度为O(1)
c. 保序的元素加入,时间复杂度为O(n)
3.2删除元素
a. 删除表尾元素,时间复杂度为O(1)
b. 非保序的元素删除(不常见),时间复杂度为O(1)
c. 保序的元素删除,时间复杂度为O(n)
4.Python中的顺序表
Python中的list和tuple两种类型采用了顺序表的实现技术,具有前面讨论的顺序表的所有性质。
tuple是不可变类型,即不变的顺序表,因此不支持改变其内部状态的任何操作,而其他方面,则与list的性质类似。
list的基本实现技术:
Python标准类型list就是一种元素个数可变的线性表,可以加入和删除元素,并在各种操作中维持已有元素的顺序(即保序),而且还具有以下行为特征:
基于下标(位置)的高效元素访问和更新,时间复杂度应该是O(1);
为满足该特征,应该采用顺序表技术,表中元素保存在一块连续的存储区中。
允许任意加入元素,而且在不断加入元素的过程中,表对象的标识(函数id得到的值)不变。
为满足该特征,就必须能更换元素存储区,并且为保证更换存储区时list对象的标识id不变,只能采用分离式实现技术。
在Python的官方实现中,list就是一种采用分离式技术实现的动态顺序表。这就是为什么用list.append(x) (或 list.insert(len(list), x),即尾部插入)比在指定位置插入元素效率高的原因。
在Python的官方实现中,list实现采用了如下的策略:在建立空表(或者很小的表)时,系统分配一块能容纳8个元素的存储区;在执行插入操作(insert或append)时,如果元素存储区满就换一块4倍大的存储区。但如果此时的表已经很大(目前的阀值为50000),则改变策略,采用加一倍的方法。引入这种改变策略的方式,是为了避免出现过多空闲的存储位置。
链表
1.为什么需要链表
顺序表的构建需要预先知道数据大小来申请连续的存储空间,而在进行扩充时又需要进行数据的搬迁,所以使用起来并不是很灵活。
链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。
2.链表的定义
链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是不像顺序表一样连续存储数据,而是在每一个节点(数据存储单元)里存放下一个节点的位置信息(即地址)。
3.单向链表
单向链表也叫单链表,是链表中最简单的一种形式,它的每个节点包含两个域,一个信息域(元素域)和一个链接域。这个链接指向链表中的下一个节点,而最后一个节点的链接域则指向一个空值。
表元素域elem用来存放具体的数据。
链接域next用来存放下一个节点的位置(python中的标识)
变量p指向链表的头节点(首节点)的位置,从p出发能找到表中的任意节点。
节点实现
1 classSingleNode(object):2 """单链表的结点"""
3 def __init__(self,item):4 #_item存放数据元素
5 self.item =item6 #_next是下一个节点的标识
7 self.next = None
View Code
单链表的操作
is_empty() 链表是否为空
length() 链表长度
travel() 遍历整个链表
add(item) 链表头部添加元素
append(item) 链表尾部添加元素
insert(pos, item) 指定位置添加元素
remove(item) 删除节点
search(item) 查找节点是否存在
单链表的实现:
1 lass SingleLinkList(object):2 """单链表"""
3
4 def __init__(self):5 self._head =None6
7 defis_empty(self):8 """判断链表是否为空"""
9 return self._head ==None10
11 deflength(self):12 """链表长度"""
13 #cur初始时指向头节点
14 cur =self._head15 count =016 #尾节点指向None,当未到达尾部时
17 while cur !=None:18 count += 1
19 #将cur后移一个节点
20 cur =cur.next21 returncount22
23 deftravel(self):24 """遍历链表"""
25 cur =self._head26 while cur !=None:27 print(cur.item)28 cur =cur.next29 print('')
View Code
头部添加元素
1 defadd(self, item):2 """头部添加元素"""
3 #先创建一个保存item值的节点
4 node =SingleNode(item)5 #将新节点的链接域next指向头节点,即_head指向的位置
6 node.next =self._head7 #将链表的头_head指向新节点
8 self._head = node
View Code
尾部添加元素
1 defappend(self, item):2 """尾部添加元素"""
3 node =SingleNode(item)4 #先判断链表是否为空,若是空链表,则将_head指向新节点
5 ifself.is_empty():6 self._head =node7 #若不为空,则找到尾部,将尾节点的next指向新节点
8 else:9 cur =self._head10 while cur.next !=None:11 cur =cur.next12 cur.next = node
View Code
指定位置添加元素
1 definsert(self, pos, item):2 """指定位置添加元素"""
3 #若指定位置pos为第一个元素之前,则执行头部插入
4 if pos <=0:5 self.add(item)6 #若指定位置超过链表尾部,则执行尾部插入
7 elif pos > (self.length()-1):8 self.append(item)9 #找到指定位置
10 else:11 node =SingleNode(item)12 count =013 #pre用来指向指定位置pos的前一个位置pos-1,初始从头节点开始移动到指定位置
14 pre =self._head15 while count < (pos-1):16 count += 1
17 pre =pre.next18 #先将新节点node的next指向插入位置的节点
19 node.next =pre.next20 #将插入位置的前一个节点的next指向新节点
21 pre.next = node
View Code
删除节点
1 defremove(self,item):2 """删除节点"""
3 cur =self._head4 pre =None5 while cur !=None:6 #找到了指定元素
7 if cur.item ==item:8 #如果第一个就是删除的节点
9 if notpre:10 #将头指针指向头节点的后一个节点
11 self._head =cur.next12 else:13 #将删除位置前一个节点的next指向删除位置的后一个节点
14 pre.next =cur.next15 break
16 else:17 #继续按链表后移节点
18 pre =cur19 cur = cur.next
View Code
查找节点是否存在
1 defsearch(self,item):2 """链表查找节点是否存在,并返回True或者False"""
3 cur =self._head4 while cur !=None:5 if cur.item ==item:6 returnTrue7 cur =cur.next8 return False
View Code
测试
1 if __name__ == "__main__":2 ll =SingleLinkList()3 ll.add(1)4 ll.add(2)5 ll.append(3)6 ll.insert(2, 4)7 print "length:",ll.length()8 ll.travel()9 print ll.search(3)10 print ll.search(5)11 ll.remove(1)12 print "length:",ll.length()13 ll.travel()
View Code
链表与顺序表的对比
链表失去了顺序表随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大,但对存储空间的使用要相对灵活。
链表与顺序表的各种操作复杂度如下所示:
操作链表顺序表
访问元素
O(n)
O(1)
在头部插入/删除
O(1)
O(n)
在尾部插入/删除
O(n)
O(1)
在中间插入/删除
O(n)
O(n)
注意虽然表面看起来复杂度都是 O(n),但是链表和顺序表在插入和删除时进行的是完全不同的操作。链表的主要耗时操作是遍历查找,删除和插入操作本身的复杂度是O(1)。顺序表查找很快,主要耗时的操作是拷贝覆盖。因为除了目标元素在尾部的特殊情况,顺序表进行插入和删除时需要对操作点之后的所有元素进行前后移位操作,只能通过拷贝和覆盖的方法进行。
4.单向循环链表
单链表的一个变形是单向循环链表,链表中最后一个节点的next域不再为None,而是指向链表的头节点
操作
is_empty() 判断链表是否为空
length() 返回链表的长度
travel() 遍历
add(item) 在头部添加一个节点
append(item) 在尾部添加一个节点
insert(pos, item) 在指定位置pos添加节点
remove(item) 删除一个节点
search(item) 查找节点是否存在
实现
1 classNode(object):2 """节点"""
3 def __init__(self, item):4 self.item =item5 self.next =None6
7
8 classSinCycLinkedlist(object):9 """单向循环链表"""
10 def __init__(self):11 self._head =None12
13 defis_empty(self):14 """判断链表是否为空"""
15 return self._head ==None16
17 deflength(self):18 """返回链表的长度"""
19 #如果链表为空,返回长度0
20 ifself.is_empty():21 return022 count = 1
23 cur =self._head24 while cur.next !=self._head:25 count += 1
26 cur =cur.next27 returncount28
29 deftravel(self):30 """遍历链表"""
31 ifself.is_empty():32 return
33 cur =self._head34 print(cur.item)35 while cur.next !=self._head:36 cur =cur.next37 print(cur.item)38 print("")39
40
41 defadd(self, item):42 """头部添加节点"""
43 node =Node(item)44 ifself.is_empty():45 self._head =node46 node.next =self._head47 else:48 #添加的节点指向_head
49 node.next =self._head50 #移到链表尾部,将尾部节点的next指向node
51 cur =self._head52 while cur.next !=self._head:53 cur =cur.next54 cur.next =node55 #_head指向添加node的
56 self._head =node57
58 defappend(self, item):59 """尾部添加节点"""
60 node =Node(item)61 ifself.is_empty():62 self._head =node63 node.next =self._head64 else:65 #移到链表尾部
66 cur =self._head67 while cur.next !=self._head:68 cur =cur.next69 #将尾节点指向node
70 cur.next =node71 #将node指向头节点_head
72 node.next =self._head73
74 definsert(self, pos, item):75 """在指定位置添加节点"""
76 if pos <=0:77 self.add(item)78 elif pos > (self.length()-1):79 self.append(item)80 else:81 node =Node(item)82 cur =self._head83 count =084 #移动到指定位置的前一个位置
85 while count < (pos-1):86 count += 1
87 cur =cur.next88 node.next =cur.next89 cur.next =node90
91 defremove(self, item):92 """删除一个节点"""
93 #若链表为空,则直接返回
94 ifself.is_empty():95 return
96 #将cur指向头节点
97 cur =self._head98 pre =None99 #若头节点的元素就是要查找的元素item
100 if cur.item ==item:101 #如果链表不止一个节点
102 if cur.next !=self._head:103 #先找到尾节点,将尾节点的next指向第二个节点
104 while cur.next !=self._head:105 cur =cur.next106 #cur指向了尾节点
107 cur.next =self._head.next108 self._head =self._head.next109 else:110 #链表只有一个节点
111 self._head =None112 else:113 pre =self._head114 #第一个节点不是要删除的
115 while cur.next !=self._head:116 #找到了要删除的元素
117 if cur.item ==item:118 #删除
119 pre.next =cur.next120 return
121 else:122 pre =cur123 cur =cur.next124 #cur 指向尾节点
125 if cur.item ==item:126 #尾部删除
127 pre.next =cur.next128
129 defsearch(self, item):130 """查找节点是否存在"""
131 ifself.is_empty():132 returnFalse133 cur =self._head134 if cur.item ==item:135 returnTrue136 while cur.next !=self._head:137 cur =cur.next138 if cur.item ==item:139 returnTrue140 returnFalse141
142 if __name__ == "__main__":143 ll =SinCycLinkedlist()144 ll.add(1)145 ll.add(2)146 ll.append(3)147 ll.insert(2, 4)148 ll.insert(4, 5)149 ll.insert(0, 6)150 print("length:",ll.length())151 ll.travel()152 print(ll.search(3))153 print(ll.search(7))154 ll.remove(1)155 print("length:",ll.length())156 ll.travel()
View Code
5.双向链表
一种更复杂的链表是“双向链表”或“双面链表”。每个节点有两个链接:一个指向前一个节点,当此节点为第一个节点时,指向空值;而另一个指向下一个节点,当此节点为最后一个节点时,指向空值。
操作
is_empty() 链表是否为空
length() 链表长度
travel() 遍历链表
add(item) 链表头部添加
append(item) 链表尾部添加
insert(pos, item) 指定位置添加
remove(item) 删除节点
search(item) 查找节点是否存在
实现
1 classNode(object):2 """双向链表节点"""
3 def __init__(self, item):4 self.item =item5 self.next =None6 self.prev =None7
8
9 classDLinkList(object):10 """双向链表"""
11 def __init__(self):12 self._head =None13
14 defis_empty(self):15 """判断链表是否为空"""
16 return self._head ==None17
18 deflength(self):19 """返回链表的长度"""
20 cur =self._head21 count =022 while cur !=None:23 count += 1
24 cur =cur.next25 returncount26
27 deftravel(self):28 """遍历链表"""
29 cur =self._head30 while cur !=None:31 print(cur.item)32 cur =cur.next33 print("")34
35 defadd(self, item):36 """头部插入元素"""
37 node =Node(item)38 ifself.is_empty():39 #如果是空链表,将_head指向node
40 self._head =node41 else:42 #将node的next指向_head的头节点
43 node.next =self._head44 #将_head的头节点的prev指向node
45 self._head.prev =node46 #将_head 指向node
47 self._head =node48
49 defappend(self, item):50 """尾部插入元素"""
51 node =Node(item)52 ifself.is_empty():53 #如果是空链表,将_head指向node
54 self._head =node55 else:56 #移动到链表尾部
57 cur =self._head58 while cur.next !=None:59 cur =cur.next60 #将尾节点cur的next指向node
61 cur.next =node62 #将node的prev指向cur
63 node.prev =cur64
65
66
67 defsearch(self, item):68 """查找元素是否存在"""
69 cur =self._head70 while cur !=None:71 if cur.item ==item:72 returnTrue73 cur =cur.next74 return False
View Code
指定位置插入节点
1 definsert(self, pos, item):2 """在指定位置添加节点"""
3 if pos <=0:4 self.add(item)5 elif pos > (self.length()-1):6 self.append(item)7 else:8 node =Node(item)9 cur =self._head10 count =011 #移动到指定位置的前一个位置
12 while count < (pos-1):13 count += 1
14 cur =cur.next15 #将node的prev指向cur
16 node.prev =cur17 #将node的next指向cur的下一个节点
18 node.next =cur.next19 #将cur的下一个节点的prev指向node
20 cur.next.prev =node21 #将cur的next指向node
22 cur.next = node
View Code
删除元素
1 defremove(self, item):2 """删除元素"""
3 ifself.is_empty():4 return
5 else:6 cur =self._head7 if cur.item ==item:8 #如果首节点的元素即是要删除的元素
9 if cur.next ==None:10 #如果链表只有这一个节点
11 self._head =None12 else:13 #将第二个节点的prev设置为None
14 cur.next.prev =None15 #将_head指向第二个节点
16 self._head =cur.next17 return
18 while cur !=None:19 if cur.item ==item:20 #将cur的前一个节点的next指向cur的后一个节点
21 cur.prev.next =cur.next22 #将cur的后一个节点的prev指向cur的前一个节点
23 cur.next.prev =cur.prev24 break
25 cur = cur.next
View Code
测试
1 if __name__ == "__main__":2 ll =DLinkList()3 ll.add(1)4 ll.add(2)5 ll.append(3)6 ll.insert(2, 4)7 ll.insert(4, 5)8 ll.insert(0, 6)9 print("length:",ll.length())10 ll.travel()11 print(ll.search(3))12 print(ll.search(4))13 ll.remove(1)14 print("length:",ll.length())15 ll.travel()
View Code
栈
栈(stack),有些地方称为堆栈,是一种容器,可存入数据元素、访问元素、删除元素,它的特点在于只能允许在容器的一端(称为栈顶端指标,英语:top)进行加入数据(英语:push)和输出数据(英语:pop)的运算。没有了位置概念,保证任何时候可以访问、删除的元素都是此前最后存入的那个元素,确定了一种默认的访问顺序。
由于栈数据结构只允许在一端进行操作,因而按照后进先出(LIFO, Last In First Out)的原理运作。
栈结构实现:
栈可以用顺序表实现,也可以用链表实现。
栈的操作
Stack() 创建一个新的空栈
push(item) 添加一个新的元素item到栈顶
pop() 弹出栈顶元素
peek() 返回栈顶元素
is_empty() 判断栈是否为空
size() 返回栈的元素个数
1 classStack(object):2 """栈"""
3
4 def __init__(self):5 self.__li =[]6
7 defis_empty(self):8 """判断是否为空"""
9 return self.__li ==[]10
11 defpush(self, item):12 """加入元素"""
13 self.__li.append(item)14
15 defpop(self):16 """弹出元素"""
17 return self.__li.pop()18
19 defpeek(self):20 """返回栈顶元素"""
21 return self.__li[len(self.__li) - 1]22
23 defsize(self):24 """返回栈的大小"""
25 return len(self.__li)26
27
28 if __name__ == "__main__":29 stack =Stack()30 stack.push("hello")31 stack.push("world")32 stack.push("lcg")33 print(stack.size())34 print(stack.peek())35 print(stack.pop())36 print(stack.pop())37 print(stack.pop())
View Code
队列
队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
队列是一种先进先出的(First In First Out)的线性表,简称FIFO。允许插入的一端为队尾,允许删除的一端为队头。队列不允许在中间部位进行操作!假设队列是q=(a1,a2,……,an),那么a1就是队头元素,而an是队尾元素。这样我们就可以删除时,总是从a1开始,而插入时,总是在队列最后。这也比较符合我们通常生活中的习惯,排在第一个的优先出列,最后来的当然排在队伍最后。
队列的实现:
同栈一样,队列也可以用顺序表或者链表实现。
操作
Queue() 创建一个空的队列
enqueue(item) 往队列中添加一个item元素
dequeue() 从队列头部删除一个元素
is_empty() 判断一个队列是否为空
size() 返回队列的大小
1 classQueue(object):2 """队列"""
3 def __init__(self):4 self.__li =[]5
6 defis_empty(self):7 return self.__li ==[]8
9 defenqueue(self, item):10 """进队列"""
11 self.__li.insert(0,item)12
13 defdequeue(self):14 """出队列"""
15 return self.__li.pop()16
17 defsize(self):18 """返回大小"""
19 return len(self.__li)20
21 if __name__ == "__main__":22 q =Queue()23 q.enqueue("hello")24 q.enqueue("world")25 q.enqueue("lcg")26 print(q.size())27 print(q.dequeue())28 print(q.dequeue())29 print(q.dequeue())
View Code
双端队列
双端队列(deque,全名double-ended queue),是一种具有队列和栈的性质的数据结构。
双端队列中的元素可以从两端弹出,其限定插入和删除操作在表的两端进行。双端队列可以在队列任意一端入队和出队。
操作
Deque() 创建一个空的双端队列
add_front(item) 从队头加入一个item元素
add_rear(item) 从队尾加入一个item元素
remove_front() 从队头删除一个item元素
remove_rear() 从队尾删除一个item元素
is_empty() 判断双端队列是否为空
size() 返回队列的大小
1 classDeque(object):2 """双端队列"""
3 def __init__(self):4 self.__li =[]5
6 defis_empty(self):7 """判断队列是否为空"""
8 return self.__li ==[]9
10 defadd_front(self, item):11 """在队头添加元素"""
12 self.__li.insert(0,item)13
14 defadd_rear(self, item):15 """在队尾添加元素"""
16 self.__li.append(item)17
18 defremove_front(self):19 """从队头删除元素"""
20 return self.__li.pop(0)21
22 defremove_rear(self):23 """从队尾删除元素"""
24 return self.__li.pop()25
26 defsize(self):27 """返回队列大小"""
28 return len(self.__li)29
30
31 if __name__ == "__main__":32 deque =Deque()33 deque.add_front(1)34 deque.add_front(2)35 deque.add_rear(3)36 deque.add_rear(4)37 print(deque.size())38 print(deque.remove_front())39 print(deque.remove_front())40 print(deque.remove_rear())41 print(deque.remove_rear())
View Code
排序与搜索
排序算法(英语:Sorting algorithm)是一种能将一串数据依照特定顺序进行排列的一种算法。
排序算法的稳定性
稳定性:稳定排序算法会让原本有相等键值的纪录维持相对次序。也就是如果一个排序算法是稳定的,当有两个相等键值的纪录R和S,且在原本的列表中R出现在S之前,在排序过的列表中R也将会是在S之前。
当相等的元素是无法分辨的,比如像是整数,稳定性并不是一个问题。然而,假设以下的数对将要以他们的第一个数字来排序。
1
(4,1) (3,1) (3,7)(5,6)
在这个状况下,有可能产生两种不同的结果,一个是让相等键值的纪录维持相对的次序,而另外一个则没有:
1
2
(3,1) (3,7) (4,1) (5,6) (维持次序)
(3,7) (3,1) (4,1) (5,6) (次序被改变)
不稳定排序算法可能会在相等的键值中改变纪录的相对次序,但是稳定排序算法从来不会如此。不稳定排序算法可以被特别地实现为稳定。作这件事情的一个方式是人工扩充键值的比较,如此在其他方面相同键值的两个对象间之比较,(比如上面的比较中加入第二个标准:第二个键值的大小)就会被决定使用在原先数据次序中的条目,当作一个同分决赛。然而,要记住这种次序通常牵涉到额外的空间负担。
冒泡排序
冒泡排序(英语:Bubble Sort)是一种简单的排序算法。它重复地遍历要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
冒泡排序算法的运作如下:
比较相邻的元素。如果第一个比第二个大(升序),就交换他们两个。
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
针对所有的元素重复以上的步骤,除了最后一个。
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
冒泡排序的分析
交换过程图示(第一次):
代码实现:O(n2)
1 defbubble_sort(alist):2 n =len(alist)3 for j in range(n - 1):4 for i in range(0, n - 1 -j):5 if alist[i] > alist[i + 1]:6 alist[i], alist[i + 1] = alist[i + 1], alist[i]7
8
9 if __name__ == '__main__':10 li = [1, 2, 3, 4, 9, 8, 7, 6]11 bubble_sort(li)12 print(li)
View Code
优化:O(n)
1 defbubble_sort(alist):2 n =len(alist)3 for j in range(n - 1):4 exchange =False5 for i in range(n - 1 -j):6 if alist[i] > alist[i + 1]:7 alist[i], alist[i + 1] = alist[i + 1], alist[i]8 exchange =True9 if notexchange:10 break
View Code
时间复杂度
最优时间复杂度:O(n) (表示遍历一次发现没有任何可以交换的元素,排序结束。)
最坏时间复杂度:O(n2)
稳定性:稳定
冒泡排序的演示
选择排序
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对n个元素的表进行排序总共进行至多n-1次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。
选择排序分析
排序过程:
红色表示当前最小值,黄色表示已排序序列,蓝色表示当前位置。
代码实现:
1 defselect_sort(alist):2 n =len(alist)3 for j in range(n - 1):4 min_index =j5 for i in range(j + 1, n):6 if alist[min_index] >alist[i]:7 min_index =i8 alist[j], alist[min_index] =alist[min_index], alist[j]9
10
11 if __name__ == '__main__':12 li = [1, 2, 3, 4, 9, 8, 7, 6]13 select_sort(li)14 print(li)
View Code
时间复杂度
最优时间复杂度:O(n2)
最坏时间复杂度:O(n2)
稳定性:不稳定(考虑升序每次选择最大的情况)
选择排序演示:
插入排序
插入排序(英语:Insertion Sort)是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
插入排序分析
代码实现:
1 definsert_sort(alist):2 n =len(alist)3 for j in range(1, n):4 i =j5 while i >0:6 if alist[i] < alist[i - 1]:7 alist[i], alist[i - 1] = alist[i - 1], alist[i]8 i -= 1
9 else:10 break
11
12
13 if __name__ == '__main__':14 li = [1, 2, 3, 4, 9, 8, 7, 6]15 insert_sort(li)16 print(li)
View Code
时间复杂度
最优时间复杂度:O(n) (升序排列,序列已经处于升序状态)
最坏时间复杂度:O(n2)
稳定性:稳定
希尔排序
希尔排序(Shell Sort)是插入排序的一种。也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因DL.Shell于1959年提出而得名。 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
希尔排序过程
希尔排序的基本思想是:将数组列在一个表中并对列分别进行插入排序,重复这过程,不过每次用更长的列(步长更长了,列数更少了)来进行。最后整个表就只有一列了。将数组转换至表是为了更好地理解这算法,算法本身还是使用数组进行排序。
例如,假设有这样一组数[ 13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10 ],如果我们以步长为5开始进行排序,我们可以通过将这列表放在有5列的表中来更好地描述算法,这样他们就应该看起来是这样(竖着的元素是步长组成):
1
2
3
4
13 14 94 33 82
25 59 94 65 23
45 27 73 25 39
10
然后我们对每列进行排序:
1
2
3
4
10 14 73 25 23
13 27 94 33 39
25 59 94 65 82
45
将上述四行数字,依序接在一起时我们得到:[ 10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45 ]。这时10已经移至正确位置了,然后再以3为步长进行排序:
1
2
3
4
5
6
10 14 73
25 23 13
27 94 33
39 25 59
94 65 82
45
排序之后变为:
1
2
3
4
5
6
10 14 13
25 23 33
27 25 59
39 65 73
45 94 82
94
最后以1步长进行排序(此时就是简单的插入排序了)
希尔排序的分析
代码实现
1 defshell_sort(alist):2 n =len(alist)3 #初始步长
4 gap = n / 2
5 while gap >0:6 #按步长进行插入排序
7 for i inrange(gap, n):8 j =i9 #插入排序
10 while j>=gap and alist[j-gap] >alist[j]:11 alist[j-gap], alist[j] = alist[j], alist[j-gap]12 j -=gap13 #得到新的步长
14 gap = gap / 2
15
16 alist = [54,26,93,17,77,31,44,55,20]17 shell_sort(alist)18 print(alist)
View Code
时间复杂度
最优时间复杂度:根据步长序列的不同而不同
最坏时间复杂度:O(n2)
稳定想:不稳定
快速排序
快速排序(英语:Quicksort),又称划分交换排序(partition-exchange sort),通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
步骤为:
从数列中挑出一个元素,称为"基准"(pivot),
重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
快速排序的分析
代码实现
1 defquick_sort(alist, start, end):2 """快速排序"""
3
4 #递归的退出条件
5 if start >=end:6 return
7
8 #设定起始元素为要寻找位置的基准元素
9 mid =alist[start]10
11 #low为序列左边的由左向右移动的游标
12 low =start13
14 #high为序列右边的由右向左移动的游标
15 high =end16
17 while low
<
<
<
<