数据结构与算法(一):一些基础吧
数据结构和算法是什么?
算法啊,就是独立存在的一种解决问题的一个思想
算法的五大特性:
- 输入:算法具有0个或者多个输入
- 输出:算法至少有1个或者多个输出
- 有穷性:算法在有限的步骤之后会自动结束而不会无限循环,并且每一个步骤可以在可接受的时间内完成。
- 确定性:算法中的每一步都有确定的含义,不会出现二义性
- 可行性:算法的每一步都是可行的,也就是说每一步都能够执行有限的次数完成
引入:首先,我们先来看一道题
注意:这里的^2代表 平方
# 三重循环 for a in range(0, 1001): for b in range(0, 1001): for c in range(0, 1001): if a**2 + b**2 == c**2 and a+b+c == 1000: print("a, b, c: %d, %d, %d" % (a, b, c))
然后,我们调用time模块进行一个简单的计时
import time start_time = time.time() 最容易想到的一种方法 end_time = time.time() print("执行时间:",end_time-start_time)
于是呢,你就会惊奇的发现居然总共执行了200多秒.......
我们优化一下代码
import time start_time = time.time() # 注意是两重循环 for a in range(0, 1001): for b in range(0, 1001-a): 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("执行时间:",end_time-start_time)
然后看一下结果........
仅仅只执行了0.x秒,造成这个的原因仅仅只是我去掉了一层的循环
两种算法,为何会有如此之大的差距呢?
算法效率的衡量
1.执行时间反应算法的效率:实现算法程序的执行时间可以反应出算法的效率,即算法的优劣
2.但靠时间比较算法的优劣比不一定可靠:硬件的基础同样会影响执行的时间
时间复杂度与"大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)
对大O算法的理解
对于算法进行特别具体的细致分析虽然很好,但在实践中的实际价值有限。对于
算法的时间性质和空间性质,最重要的是其数量级和趋势,这些是分析算法效率
的主要部分。而计量算法基本操作数量的规模函数中那些常量因子可以忽略不
计。例如,可以认为3n2和100n2属于同一个量级,如果两个算法处理同样规
模实例的代价分别为这两个函数,就认为它们的效率“差不多”,都为n2级。
最坏时间的复杂度
- 算法完成工作最少需要多少基本操作,即最优时间复杂度
- 算法完成工作最多需要多少基本操作,即最坏时间复杂度
- 算法完成工作平均需要多少基本操作,即平均时间复杂度
我们主要关注算法的最坏情况,即最坏时间复杂度
时间复杂度的几条基本计算规则
- 基本格式,只有常数项,认为其时间复杂度为O(1)
- 顺序结构,时间复杂度按加法进行计算
- 循环结构,时间复杂度按乘法进行计算
- 分支结构,时间复杂度取最大值
- 判断一个算法的效率时,往往只需要关注操作数量的最高次项,其它次要项和常数项可以忽略
- 在没有特殊说明时,我们所分析的算法时间复杂度都是指最坏时间复杂度
还是上面那个算法例子
1.
for a in range(0, 1001): for b in range(0, 1001): for c in range(0, 1001): if a**2 + b**2 == c**2 and a+b+c == 1000: print("a, b, c: %d, %d, %d" % (a, b, c))
三次循环,他的时间复杂度为:T(n) = O(n*n*n) = O(n3)
2.
for a in range(0, 1001): for b in range(0, 1001-a): c = 1000 - a - b if a**2 + b**2 == c**2: print("a, b, c: %d, %d, %d" % (a, b, c))
两次循环,他的时间复杂度为:T(n) = O(n*n*(1+1)) = O(n*n) = O(n2)
由此可见啊,我们尝试的第二种算法要比第一种算法的时间复杂度要好得多
常见的时间复杂度
注意:经常将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代码的执行速度
- Timer是测量小段代码执行速度的类
- stmt参数是要测试的代码语句(statment)
- setup参数是运行代码时需要的设置
- timer参数是一个定时器函数,与平台有关
timeit.Timer.timeit(number=1000000)
List的测试操作
def t1(): l = [] for i in range(1000): l = l + [i] def t2(): l = [] for i in range(1000): l.append(i) def t3(): l = [i for i in range(1000)] def t4(): l = list(range(1000)) from timeit import Timer test1 = Timer("t1()","from __main__ import t1") print('result1:',test1.timeit(number=100),'seconds') test2 = Timer("t2()","from __main__ import t2") print('result2:',test2.timeit(number=100),'seconds') test3 = Timer("t3()","from __main__ import t3") print('result3:',test3.timeit(number=100),'seconds') test4 = Timer("t4()","from __main__ import t4") print('result4:',test4.timeit(number=100),'seconds')
由于时间的问题,在这里我每个只测试了100次,number=100
即使次数不多们也能明显的看出速度的差距。
pop的测试操作
from timeit import Timer x = list(range(1000000)) pop_test1 = Timer("x.pop(0)","from __main__ import x") print("POP Test1:",pop_test1.timeit(number=1000)) x = list(range(1000000)) pop_test2 = Timer("x.pop()","from __main__ import x") print("POP Test2:",pop_test2.timeit(number=1000))
此时结果:
结果非常的明显:pop最后一个元素的效率远远高于pop第一个元素
数据结构
问题:我们如果用Python中的类型来保存一个班的学生信息呢?如果想要快速的通过学生姓名获取到其他的信息呢?
实际上当我们在思考这个问题的时候,我们已经用到了数据结构。列表和字典都可以存储一个班的学生信息,但是想要在列表
中获取一名同学的信息时,就要遍历这个列表,其时间复杂度为O(n),而使用字典存储时,可将学生姓名作为字典的键,学
生信息作为值,进而查询时不需要遍历便可快速获取到学生信息,其时间复杂度为O(1)。
我们为了解决问题,需要将数据保存下来,然后根据数据的存储方式来设计算法实现进行处理,那么数据的存储方式不同就
会导致需要不同的算法进行处理。我们希望算法解决问题的效率越快越好,于是我们就需要考虑数据究竟如何保存的问
题,这就是数据结构。
在上面的问题中我们可以选择Python中的列表或字典来存储学生信息。列表和字典就是Python内建帮我们封装好的两种
数据结构。
概念
数据是一个抽象的概念,将其进行分类后得到程序设计语言中的基本类型。如:int,float,char等。
数据元素之间不是独立的,存在这特定的关系,这些关系便是结构。
数据结构指数据对象中数据元素之间的关系。
Python中给我们提供了很多现成的数据结构类型,这些系统自己定义好的,不需要我们自己去定义的数据结构叫做Python的内置数据结构,比如:列表、元组、字典。而有些数据组织方式,Python系统类并没有直接的定义,需要我们自己去定义实现这些数据则组织方式,这些数据组织方式称之为Python拓展数据结构,比如栈、队列等
算法与数据结构的区别
数据结构只是静态的描述了数据元素之间的关系。
高效的程序需要在数据结构的基础上设计和选择算法
程序 = 数据结构 + 算法
总结:算法是为了解决实际问题而设计的,数据结构是算法需要处理的问题的载体
抽象的数据类型
抽象数据类型(ADT)的含义是指一个数学模型以及定义在此数据模型上的一组操作。即把数据类型和数据类型上的运算捆在一起,进行封装。引入抽象数据类型的目的是把数据类型表示和数据类型上运算的实现与这些数据类型和运算在程序中的引用隔开,使它们互相独立。
最常用的数据运算:
- 插入
- 删除
- 修改
- 查找
- 排序
顺序表
在程序中,经常需要将一组(通常是同为某个类型的)数据元素作为整体管理和使用,需要创建这样的元素组,用变量去记录他们,传进传出函数等。一组数据中包含的元素个数可能发生变化(可以增加或者删除元素)
对于这种需求,最简单的解决方案便是将这样一组元素看成一个序列,用元素在序列中的位置和顺序,表示时间应用中的某种有意义的信息,或者表示数据之间的某种关系。
这样一组序列元素的组织形式,我们可以将其抽象为线性表。一个线性表是某类元素的一个集合,还记录着元素之间的一种顺序关系。
线性表是最基本的数据结构之一,在实际程序中应用的非常广泛,它还经常被用作更复杂的数据结构的实现基础。
根据线性表的实际存储方式,分为两种实现模型:
- 顺序表,将元素顺序的存放在一块连续的存储区里,元素间的顺序关系由他们的存储顺序自然表示
- 链表,将元素存放在通过链接构造起来的一系列存储块中
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),则改变策略,采用加一倍的方法。引入这种改策略的方式,是为了避免出现过多的空闲的存储位置。
链表
为什么需要链表?
顺序表的构建需要预先知道数据大小来申请连续的存储空间,而在进行扩充时又需要进行数据的搬迁,所以使用起来并不是很
灵活。
链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。
链表的定义
链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是不像顺序表一样连续存储数据,而是在每一个节点(数据存储单元)里存放下一个节点的位置信息(即地址)。
单向链表
单向链表也叫做单链表,是链表中最简单的一种形式,它每个节点包含两个域,一个信息域(元素域)和一个链表域。这个链表指向链表种的下一个节点,而最后一个节点则指向了一个空值。
- 表元素域elem用来存放具体的数据
- 链表域next用来存放下一个节点的位置(Python的标识)
- 变量p指向链表的头节点(首节点)的位置,从p触发去找表中的节点。
节点的实现(一个例子):
class SingleNode(object): '''单链表的节点''' def __init__(self,item): #_item用于存放数据文件 self.item = item #_next是下一个节点的标识 self._next = None
单链表的操作:
- is_empty() 链表是否为空
- length() 链表长度
- travel() 遍历整个链表
- add(item) 链表头部添加元素
- append(item) 链表尾部添加元素
- insert(pos,item) 在指定位置添加元素
- remove(item) 删除节点
- search(item) 查找节点是否存在
单链表的实现(一个例子):
class SingleLinkList(object): '''单链表''' def __init__(self): self._head = None def is_empty(self): '''判断链表是否为空''' return self._head == None def length(self): '''链表长度''' cur = self._head #初始时指向头节点 count = 0 #尾节点指向None,当未达到尾部时 while cur != None: count += 1 cur = cur.next return count def travel(self): '''遍历链表''' cur = self._head while cur != None: print(cur.item) cur = cur.next print('')
头部添加元素的方法:
def add(self,item): '''头部添加元素''' #先创建一个保存item值的节点 node = SingleNode(item) #将新节点的链接域next指向头节点,即_head指向的位置 node.next = self._head # 将链表的头_head指向新节点 self._head = node
尾部添加元素的方法:
def append(self,item): '''尾部添加元素''' node = SingleNode(item) #首先判断链表是否为空,若是空链表,则将_head指向新的节点 if self.is_empty(): self._head = node #若是不为空,则找到尾部,将尾部的next指向新的节点 else: cur = self._head while cur.next != None: cur = cur.next cur.next = node
指定位置添加元素
def insert(self,pos,item): '''指定位置添加元素''' #若指定位置pos为第一个元素之前,则执行头部插入 if pos <= 0: self.add(item) #若指定位置超过链表尾部,则执行尾部插入 elif pos > (self.length()-1): self.append(item) #找到指定的位置 else: node = SingleNode(item) count = 0 #pre用来指向指定位置pos的前一个位置pos-1,初始从头节点开始移动到指定位置 pre = self._head while count < (pos-1): count += 1 pre = pre.next #先将新节点node的next指向插入位置的节点 node.next = pre.next #将插入位置的前一个节点的next指向新节点 pre.next = node
删除节点
def remove(self,item): """删除节点""" cur = self._head pre = None while cur != None: # 找到了指定元素 if cur.item == item: # 如果第一个就是删除的节点 if not pre: # 将头指针指向头节点的后一个节点 self._head = cur.next else: # 将删除位置前一个节点的next指向删除位置的后一个节点 pre.next = cur.next break else: # 继续按链表后移节点 pre = cur cur = cur.next
查找节点是否存在
def search(self,item): """链表查找节点是否存在,并返回True或者False""" cur = self._head while cur != None: if cur.item == item: return True cur = cur.next return False
链表与顺序表的对比
链表失去了顺序表随机读取的优点,同时链表由于增加了节点的指针域,空间开销比较大,但对于存储空间的使用要相对灵活。
链表与顺序表的各种操作复杂度如下:
注意虽然表面看起来复杂度都是 O(n),但是链表和顺序表在插入和删除时进行的是完全不同的操作。链表的主要耗时操作是
遍历查找,删除和插入操作本身的复杂度是O(1)。顺序表查找很快,主要耗时的操作是拷贝覆盖。因为除了目标元素在尾部
的特殊情况,顺序表进行插入和删除时需要对操作点之后的所有元素进行前后移位操作,只能通过拷贝和覆盖的方法进行。
单向循环链表
单列表的一个变形是单向循环链表,链表中最后一个节点的next域不再为None,而是执行链表的头节点
操作:
- is_empty()判断链表是否为空
- length()返回链表的长度
- trave()遍历
- add(item)在头部添加一个节点
- append(item)在尾部添加一个节点
- insert(pos,item)在指定位置pos添加节点
- remove(item)删除一个节点
- search(item)查找节点是否存在
实现:
#单项循环列表 class Node(object): """节点""" def __init__(self, item): self.item = item self.next = None class SinCycLinkedlist(object): """单向循环链表""" def __init__(self): self._head = None def is_empty(self): """判断链表是否为空""" return self._head == None def length(self): """返回链表的长度""" # 如果链表为空,返回长度0 if self.is_empty(): return 0 count = 1 cur = self._head while cur.next != self._head: count += 1 cur = cur.next return count def travel(self): """遍历链表""" if self.is_empty(): return cur = self._head print(cur.item) while cur.next != self._head: cur = cur.next print(cur.item) print("") def add(self, item): """头部添加节点""" node = Node(item) if self.is_empty(): self._head = node node.next = self._head else: #添加的节点指向_head node.next = self._head # 移到链表尾部,将尾部节点的next指向node cur = self._head while cur.next != self._head: cur = cur.next cur.next = node #_head指向添加node的 self._head = node def append(self, item): """尾部添加节点""" node = Node(item) if self.is_empty(): self._head = node node.next = self._head else: # 移到链表尾部 cur = self._head while cur.next != self._head: cur = cur.next # 将尾节点指向node cur.next = node # 将node指向头节点_head node.next = self._head def insert(self, pos, item): """在指定位置添加节点""" if pos <= 0: self.add(item) elif pos > (self.length()-1): self.append(item) else: node = Node(item) cur = self._head count = 0 # 移动到指定位置的前一个位置 while count < (pos-1): count += 1 cur = cur.next node.next = cur.next cur.next = node def remove(self, item): """删除一个节点""" # 若链表为空,则直接返回 if self.is_empty(): return # 将cur指向头节点 cur = self._head pre = None # 若头节点的元素就是要查找的元素item if cur.item == item: # 如果链表不止一个节点 if cur.next != self._head: # 先找到尾节点,将尾节点的next指向第二个节点 while cur.next != self._head: cur = cur.next # cur指向了尾节点 cur.next = self._head.next self._head = self._head.next else: # 链表只有一个节点 self._head = None else: pre = self._head # 第一个节点不是要删除的 while cur.next != self._head: # 找到了要删除的元素 if cur.item == item: # 删除 pre.next = cur.next return else: pre = cur cur = cur.next # cur 指向尾节点 if cur.item == item: # 尾部删除 pre.next = cur.next def search(self, item): """查找节点是否存在""" if self.is_empty(): return False cur = self._head if cur.item == item: return True while cur.next != self._head: cur = cur.next if cur.item == item: return True return False
测试:
ll = SinCycLinkedlist() ll.add(1) ll.add(2) ll.append(3) ll.insert(2, 4) ll.insert(4, 5) ll.insert(0, 6) print("length:",ll.length()) ll.travel() print(ll.search(3)) print(ll.search(7)) ll.remove(1) print("length:",ll.length()) ll.travel()
结果:
双向链表
双向链表又称为“双面列表”。
每一个节点都有连个链接:一个指向前一个节点,当此节点成为第一个节点时,指向空值;而另一个指向下一个节点,当此节点为最后一个节点时,指向空值。
操作:
- is_empty()链表是否为空
- length()链表长度
- travel()遍历链表
- add(item)链表头部添加
- append(item)链表尾部添加
- insert(pos,item)链表指定位置添加
- remove(item)删除指定节点
- search(item)查找节点是否存在
#双向链表 class Node(object): """双向链表节点""" def __init__(self, item): self.item = item self.next = None self.prev = None class DLinkList(object): """双向链表""" def __init__(self): self._head = None def is_empty(self): """判断链表是否为空""" return self._head == None def length(self): """返回链表的长度""" cur = self._head count = 0 while cur != None: count += 1 cur = cur.next return count def travel(self): """遍历链表""" cur = self._head while cur != None: print(cur.item) cur = cur.next print("") def add(self, item): """头部插入元素""" node = Node(item) if self.is_empty(): # 如果是空链表,将_head指向node self._head = node else: # 将node的next指向_head的头节点 node.next = self._head # 将_head的头节点的prev指向node self._head.prev = node # 将_head 指向node self._head = node def append(self, item): """尾部插入元素""" node = Node(item) if self.is_empty(): # 如果是空链表,将_head指向node self._head = node else: # 移动到链表尾部 cur = self._head while cur.next != None: cur = cur.next # 将尾节点cur的next指向node cur.next = node # 将node的prev指向cur node.prev = cur def search(self, item): """查找元素是否存在""" cur = self._head while cur != None: if cur.item == item: return True cur = cur.next return False def insert(self, pos, item): """在指定位置添加节点""" if pos <= 0: self.add(item) elif pos > (self.length() - 1): self.append(item) else: node = Node(item) cur = self._head count = 0 # 移动到指定位置的前一个位置 while count < (pos - 1): count += 1 cur = cur.next # 将node的prev指向cur node.prev = cur # 将node的next指向cur的下一个节点 node.next = cur.next # 将cur的下一个节点的prev指向node cur.next.prev = node # 将cur的next指向node cur.next = node def remove(self, item): """删除元素""" if self.is_empty(): return else: cur = self._head if cur.item == item: # 如果首节点的元素即是要删除的元素 if cur.next == None: # 如果链表只有这一个节点 self._head = None else: # 将第二个节点的prev设置为None cur.next.prev = None # 将_head指向第二个节点 self._head = cur.next return while cur != None: if cur.item == item: # 将cur的前一个节点的next指向cur的后一个节点 cur.prev.next = cur.next # 将cur的后一个节点的prev指向cur的前一个节点 cur.next.prev = cur.prev break cur = cur.next
操作:
ll = DLinkList() ll.add(1) ll.add(2) ll.append(3) ll.insert(2, 4) ll.insert(4, 5) ll.insert(0, 6) print("length:",ll.length()) ll.travel() print(ll.search(3)) print(ll.search(4)) ll.remove(1) print("length:",ll.length()) ll.travel()
结果:
栈
栈,有些地方称为堆栈,是一种容器,可以存入数据元素、访问元素、删除元素、他的特点在于只能允许容器的一端top,进行加入数据push和输出数据pop。没有了位置的概念,保证任何时候可以访问、删除的元素都是此前最后存入的那个元素,确定了一种默认的访问顺序。
由于栈数据结构只允许在一端进行操作,因而按照后进先出的原理运作
栈的操作:
- Stack()创建一个新的空栈
- push(item)添加一个元素item到栈顶
- pop(item)弹出栈顶元素
- peek()返回栈顶元素
- is_empty()判断栈是否为空
- size()返回栈的元素个数
class Stack(object): """栈""" def __init__(self): self.items = [] def is_empty(self): """判断是否为空""" return self.items == [] def push(self, item): """加入元素""" self.items.append(item) def pop(self): """弹出元素""" return self.items.pop() def peek(self): """返回栈顶元素""" return self.items[len(self.items)-1] def size(self): """返回栈的大小""" return len(self.items) if __name__ == "__main__": stack = Stack() stack.push("hello") stack.push("world") stack.push("nullnull") print(stack.size()) print(stack.peek()) print(stack.pop()) print(stack.pop()) print(stack.pop())
队列
队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
队列是一种先进先出的线性表
操作:
- Queue()创建一个空的队列
- enqueue(item)往队列中添加一个item元素
- dequeue()从队列头部删除一个元素
- is_empty()判断一个队列是否为空
- size()返回队列的大小
实现:
class Queue(object): """队列""" def __init__(self): self.items = [] def is_empty(self): return self.items == [] def enqueue(self, item): """进队列""" self.items.insert(0,item) def dequeue(self): """出队列""" return self.items.pop() def size(self): """返回大小""" return len(self.items) if __name__ == "__main__": q = Queue() q.enqueue("hello") q.enqueue("world") q.enqueue("nullnull") print(q.size()) print(q.dequeue()) print(q.dequeue()) print(q.dequeue())
双端队列
是一种具有队列和栈的性质的数据结构。
双端队列中的元素可以从两端弹出,其限定插入和删除操作在表的两端进行。双端队列可以在队列任意一端入队和 出对。
操作:
- Deque()创建一个空的双端队列
- add_front(item)从对头加入一个item元素
- add_rear(item)从队尾加入一个item元素
- remove_front()从对头删除一个item元素
- remove_rear()从队尾删除一个item元素
- is_empty()判断双端队列是否为空
- size()返回队列的大小
实现:
class Deque(object): """双端队列""" def __init__(self): self.items = [] def is_empty(self): """判断队列是否为空""" return self.items == [] def add_front(self, item): """在队头添加元素""" self.items.insert(0,item) def add_rear(self, item): """在队尾添加元素""" self.items.append(item) def remove_front(self): """从队头删除元素""" return self.items.pop(0) def remove_rear(self): """从队尾删除元素""" return self.items.pop() def size(self): """返回队列大小""" return len(self.items) if __name__ == "__main__": deque = Deque() deque.add_front(1) deque.add_front(2) deque.add_rear(3) deque.add_rear(4) print(deque.size()) print(deque.remove_front()) print(deque.remove_front()) print(deque.remove_rear()) print(deque.remove_rear())