Python算法基础之时间复杂度与数据结构
时间复杂度
时间复杂度: 是程序中基本步骤的数量
时间复杂度的计算规则
基本操作,只有常数项,计算时间复杂度为O(1)
顺序结构,时间复杂度按加法计算
条件结构,时间复杂度取最大值
循环结构,时间复杂度按乘法计算
判断一个算法的效率时往往只需要关注操作数量的最高次项,其他次要项和常数项可以忽略
如果没有特殊说明,通常是指最坏时间复杂度
例题:如果 a+b+c = 1000 且a2+b2 = c^2,求出所有的a,b,c
import time
def meiju():
start_time = time.clock()
for a in range(1, 1001):
for b in range(1, 1001):
for c in range(1, 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.clock() - start_time
print(end_time)
时间复杂度:T = 1000*1000*1000*2
如果题目要求改为 a+b+c =N 则上述程序的时间复杂度: T = N^3*2
则时间复杂度可以写为 T(n) = n ^ 3 *2 n是指运算规模
因为在计算时间复杂度的时候我们不需要分的特么细致,所以我们可以认为:
T(n) = n^3*2,
T(n) = n^3*10
T(n) = n^3 是同一个数量级 T(n) = n^3 = g(n)
常见的时间复杂度
12–O(1)–常数阶
2n+3–O(n) 线性阶
3n2+4n+2–O(n2)–平方阶
5log2n+20 --O(logn)–对数阶
2n+3nlog2n+19 --O(nlogn)-- nlogn阶
n3–O(n3)–立方阶
2n–O(2n)—指数阶
对于消耗的时间从小到大
O(1)<O(logn)<O(nlogn)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)
timeit 模块
timeit模块可以用来测试一小段Python代码的执行速度
class timeit.Timer(stmt = ‘pass’,setup = ‘pass’,timer = )
Timer是测量小段代码执行速度的类
stmt参数是要测试的代码语句(statment)
setup参数是运行代码时需要的设置
timer参数是一个定时器函数与平台无关
设置完成类以后就可以进行测试了:
timeit.Timer.timeit(number = 100000)
Timer类中测试语句执行速度的对象方法,number参数时测试代码时的测试次数,默认是1000000次。
方法返回执行代码的平均耗时,是一个float类型的秒数
#测试列表各种方法的耗时
from timeit import Timer
def test1():
li = []
for i in range(10000):
li.append(i)
def test2():
li = []
for i in range(10000):
li = li + [i]
def test3():
li = [i for i in range(10000)]
def test4():
li = list(range(10000))
def test5():
li = []
for i in range(1000):
li.insert(0, i)
timer1 = Timer('test1()', 'from __main__ import test1')
print('append:', timer1.timeit(1000))
timer2 = Timer('test2()', 'from __main__ import test2')
print('+:', timer2.timeit(1000))
timer3 = Timer('test3()', 'from __main__ import test3')
print('[i for i in range(1000)]:', timer3.timeit(1000))
timer4 = Timer('test4()', 'from __main__ import test4')
print('list(range(1000)):', timer4.timeit(1000))
timer5 = Timer('test5()', 'from __main__ import test5')
print('insert:', timer5.timeit(1000))
#其运行结果为:
append: 0.69229052
+: 141.460140445
[i for i in range(1000)]: 0.32157705699998473
list(range(1000)): 0.1889527519999774
insert: 0.24680397899999207
list列表操作时间复杂度
append()— O(1)
indexx[] ---- O(1)
pop(i) — O(n)
insert(i,item) — O(n)
contains(in) — O(n)
dict内置操作时间复杂度
copy — O(n)
get item —O(1)
set item— O(1)
delete item —O(1)
数据结构
数据结构 是对基本数据类型的一次封装
list,dict 为基本数据类型的组合,是高级数据类型。
程序 = 数据结构+算法
list查找时需要遍历,时间复杂度为列表长度
抽象数据类型(ADT):将原有的数据与该数据支持的操作放在一起,形成一个整体就是抽象数据类型
例如:
class stu(object): # object 是列表数据结构
'''函数内部定义了列表的增删改查'''
def adds(self):
pass
def pop(self):
pass
def sort(self):
pass
def modify(self):
pass
内存 连续的存储控件,一个单元就是一个字节,一个单元由八个数据位组成。32位机器 整型(int)占四个字节 字符串型(char)占一个字节
一维线性表:链表和顺序表
顺序表:
基本类型相同的元素(类型相同的元素占内存地址相同)存放在连续单元中。int型数据四个连续单元代表一个数
元素外置的顺序表:类型不同的元素的存储地址放在连续的单元中,每四个单元(即四个字节)存放一个元素的地址
顺序表的结构 表头信息:存储顺序表的详情信息(容量,元素个数:当前空间中已经存放了多少数据)、数据区
表头信息与数据区怎么链接在一起
@1 表头信息与数据区连续存在一起
@2分离式结构,表头信息除了存放容量大小和元素个数的两个空间以外还存在地址空间,指向数据区的内存起始地址
顺序表数据区的替换和扩充
@1 顺序表数据区的扩充方式1:每次扩充增加固定数目的存储位置,如每次扩充增加10个元素位置。
特点是:节省空间,但是扩充操作频繁,操作次数多
@2 每次扩充容量加倍,如每次扩充增加一倍存储空间
特点是: 减少了扩充操作的执行次数,但可能会浪费空间资源,以空间换时间,是推荐的方式。
支持扩充的顺序表成为动态顺序表
顺序表的操作
增加元素
@1尾部添加 时间复杂度 O(1)
@2保序的元素插入 最坏的打算,从第一个位置插入数字 时间复杂度 O(n) n是顺序表长度
@3非保序的元素插入 时间复杂度 为O(1)
删除元素
@1删除表尾元素 时间复杂度 O(1)
@2保序的元素删除 时间复杂度 O(n)
@3非保序的元素删除 时间复杂度 O(1)
list的基本实现
按照下标位置进行索引,时间复杂度是 O(1)
允许任意添加元素,而且在不断放入元素的过程中,表对象的标识不变(分离式、元素外置的顺序表)
python中在建立空表时,系统分配一块能容纳八个元素的存储区(元素外置);在执行插入操作时,如果元素存储区满就换一块四倍大的存储区。但如果此时的表已经很大
目前阈值为50000,则改变策略,采用加一倍的方法。为了避免出现过多的空闲区域
链表
前一个元素空间位置除了保存自身的元素内容外还要保存下个元素的地址,所以一个链接节点包含数据区和链接区
单向链表 从第一个元素指向最后一个元素
单向链表也叫做单链表,每个节点包含两个域,一个信息域(元素域)和一个链接域。这个链接指向链表中的下一个金额点,而最后一个节点的链接域则指向一个空值
元素域用来存放具体的数据,链接域存放下一个节点的位置,变量p指向链表的头节点的位置,从p出发能找到表中任意节点
单链表的实现:
class Node(object):
'''节点'''
def __init__(self, elem):
self.elem = elem
self.next = None
class SingleLinkList(object):
"""单链表"""
def __init__(self, node=None):
self.__head = node # 前面有双下划线__说明是私有属性
def is_empty(self):
"""判断链表是否为空"""
return self.__head == None
def lenth(self):
"""链表长度"""
# cur 游标,用来移动遍历节点
cur = self.__head
# count 记录数量
count = 0
while cur != None:
count += 1
cur = cur.next
return count
def travle(self):
"""遍历"""
cur = self.__head
while cur != None:
print(cur.elem, end=' ')
cur = cur.next
def add(self, item):
"""链表头部添加元素,头插法,时间复杂度O(1)"""
node = Node(item)
# 先让新的节点的next指向原来的链表的起始地址
node.next = self.__head
# 让链表的头部指向node
self.__head = node
def append(self, item):
"""链表尾部添加元素,尾插法,时间复杂度O(n)"""
node = Node(item)
if self.is_empty():
self.__head = node
else:
cur = self.__head
while cur.next != None:
cur = cur.next
cur.next = node
def insert(self, pos, item):
"""指明位置添加元素,时间复杂度 O(n)"""
# pos 从0开始的索引
# pre 指定位置的前一个位置
if pos <= 0: # 当pos< = 0认为在0节点之前插入元素
self.add(item)
elif pos > (self.lenth() - 1): # pos超出链表长度 则在尾部插入
self.append(item)
else:
pre = self.__head
count = 0
while count < pos - 1:
count += 1
pre = pre.next
# 当循环推出后 pre指向pos-1位置
node = Node(item)
node.next = pre.next
pre.next = node
def remove(self, item):
"""删除节点"""
if self.is_empty():
return
cur = self.__head
pre = None
while cur.next != self.__head:
if cur.elem == item:
# 先判断此节点是否是头节点
# 如果是头节点
if cur == self.__head:
# 头节点
# 找头节点
rear = self.__head
while rear.next != self.__head:
rear = rear.next
self.__head = cur.next
rear.next = self.__head
pass
# self.__head = cur.next
else:
pre.next = cur.next
return
else:
# 中间节点
pre = cur # 必须先移动pre游标
cur = cur.next
# 退出循环,cur指向尾节点
if cur.elem == item:
if cur == self.__head:
# 链表只有一个节点
self.__head = None
else:
pre.next = cur.next
def search(self, item):
"""查找节点是否存在,时间复杂度O(n)"""
if self.is_empty():
return False
cur = self.__head
# 把尾部落掉了
while cur.next != self.__head:
if cur.elem == item:
return True
else:
cur = cur.next
if cur.elem == item:
return True
return False
if __name__ == '__main__':
li = SingleLinkList()
print(li.is_empty())
print(li.lenth())
li.append(1)
print(li.is_empty())
print(li.lenth())
li.append(2)
li.add(8)
li.append(3)
li.append(4)
li.append(5)
li.append(6)
li.append(7)
li
li.travle()
#运行结果:
True
0
False
1
8 1 2 3 4 5 6 7
单向循环列表
单向循环列表与普通单链表的区别就是单向循环列表的最后一个元素的next区指向的是链表第一个元素
class Node(object):
'''节点'''
def __init__(self, elem):
self.elem = elem
self.next = None
class SingleLinkList(object):
"""单链表循环"""
def __init__(self, node=None):
self.__head = node # 前面有双下划线__说明是私有属性
if node:
node.next = node
def is_empty(self):
"""判断链表是否为空"""
return self.__head == None
def lenth(self):
"""链表长度"""
if self.is_empty():
return 0
# cur 游标,用来移动遍历节点
cur = self.__head
# count 记录数量
count = 1
while cur.next != self.__head:
count += 1
cur = cur.next
return count
def travle(self):
"""遍历"""
if self.is_empty():
return
cur = self.__head
while cur.next != self.__head:
print(cur.elem, end=' ')
cur = cur.next
# 退出循环时候,cur指向尾节点,但尾节点未打印
print(cur.elem, end=' ')
def add(self, item):
"""链表头部添加元素,头插法,时间复杂度O(1)"""
node = Node(item)
if self.is_empty():
self.__head = node
node.next = node
else:
# 先让新的节点的next指向原来的链表的起始地址
node.next = self.__head
# 让链表的头部指向node
self.__head = node
cur = self.__head
while cur.next != self.__head:
cur = cur.next
cur.next = node
def append(self, item):
"""链表尾部添加元素,尾插法,时间复杂度O(n)"""
node = Node(item)
if self.is_empty():
self.__head = node
node.next = node
else:
cur = self.__head
while cur.next != self.__head:
cur = cur.next
node.next = self.__head
cur.next = node
def insert(self, pos, item):
"""指明位置添加元素,时间复杂度 O(n)"""
# pos 从0开始的索引
# pre 指定位置的前一个位置
if pos <= 0: # 当pos< = 0认为在0节点之前插入元素
self.add(item)
elif pos > (self.lenth() - 1):
self.append(item)
else:
pre = self.__head
count = 0
while count < pos - 1:
count += 1
pre = pre.next
# 当循环推出后 pre指向pos-1位置
node = Node(item)
node.next = pre.next
pre.next = node
def remove(self, item):
"""删除节点"""
cur = self.__head
pre = None
while cur != None:
if cur.elem == item:
# 先判断此节点是否是头节点
# 如果是头节点
if cur == self.__head:
self.__head = cur.next
else:
pre.next = cur.next
break
else:
pre = cur # 必须先移动pre游标
cur = cur.next
def search(self, item):
"""查找节点是否存在,时间复杂度O(n)"""
cur = self.__head
while cur != None:
if cur.elem == item:
return True
else:
cur = cur.next
return False
coding = utf-8
队列
队列是只允许在一端进行插入操作,而在另一端进行删除操作的线性表
队列是一种先进先出的线性表,简称FIFO,允许插入的一端为队尾,允许删除的一端为队头。队列不允许在中间部位进行操作
队列实现用list列表来实现
class Queue(object):
def __init__(self):
self.__list = []
def enqueue(self, item):
"""往队列里添加一个item元素"""
self.__list.append(item)
def dequeue(self):
"""从队列头部删除一个元素"""
return self.__list.pop(0)
def is_impty(self):
"""判断一个队列是否为空"""
return self.__list == []
def size(self):
"""返回对列的大小"""
return len(self.__list)
if __name__ == '__main__':
q = Queue()
q.enqueue(1)
q.enqueue(2)
q.enqueue(3)
q.enqueue(4)
print(q.dequeue())
print(q.dequeue())
print(q.dequeue())
print(q.dequeue())
双端队列
class Queue():
"""双端队列"""
def __init__(self):
self.__list = []
def add_front(self, item):
self.__list.insert(0, item)
def add_rear(self, item):
self.__list.append(item)
def pop_front(self):
return self.__list.pop(0)
def pop_rear(self):
return self.__list.pop()
def is_impty(self):
return self.__list == []
def size(self):
return len(self.__list)
栈(stack)
栈有的地方成为堆栈,可存入数据元素,访问元素,删除元素。它的特点在于只能允许在容器的一段(栈顶)加入数据和输出数据的运算,没有了位置的概念,保证任何时候可以访问、删除的元素都是此前最后存入的那个元素确定了一种默认的访问顺序,由于栈数据结构只允许在一端进行操作,因而按照后进先出(LIFO)的原理运作。顺序表和链表可以用作栈
class Stack(object):
def __init__(self):
self.__list = []
def push(self, item):
"""添加一个新的元素到栈顶"""
self.__list.append(item) # 时间复杂度是O(1)
# self.__list.insert(0,item)时间复杂度是O(n)
# 如果是链表的话,选择链表的头部为栈的顶部
def pop(self):
"""弹出栈顶元素"""
return self.__list.pop()
# self.__list.pop(0)
def peek(self):
"""返回栈顶元素"""
if self.__list:
return self.__list(-1)
else:
return None
def is_empty(self):
"""判断栈是否为空"""
return self.__list == []
def size(self):
"""返回栈的元素个数"""
return len(self.__list)
if __name__ == '__main__':
stack = Stack()
stack.push(1)
stack.push(2)
stack.push(3)
stack.push(4)
print(stack.pop())
print(stack.pop())
print(stack.pop())
print(stack.pop())