timeit 模块可以用来测试一段 python 代码的执行速度.
class timeit.Timer(stmt=‘pass’, setup=‘pass’, timer=
)
Timer 是测量小段代码执行速度的类;
stmt参数是要测试的代码语句 (statment);
setup 参数是运行代码是需要的设置;
timer 参数是一个定时器函数, 与平台有关.
Timer类中测试语句执⾏速度的对象⽅法。number参数是测试代码时的测试
次数,默认为1000000次。⽅法返回执⾏代码的平均耗时,⼀个float类型的
秒数。
# –*– coding: utf-8 –*–
# @Time : 2019/4/5 17:34
# @Author : Damon_duanlei
# @FileName : operate_list.py
# @BlogsAddr : https://blog.csdn.net/Damon_duanlei
import timeit
def func1():
L = []
for i in range(1000):
L = L + [i]
def func2():
L = []
for i in range(1000):
L.append(i)
def func3():
L = [i for i in range(1000)]
def func4():
L = list(range(1000))
if __name__ == '__main__':
timer1 = timeit.Timer("func1()", "from __main__ import func1")
timer2 = timeit.Timer("func2()", "from __main__ import func2")
timer3 = timeit.Timer("func3()", "from __main__ import func3")
timer4 = timeit.Timer("func4()", "from __main__ import func4")
print("concat ", timer1.timeit(number=1000), "seconds")
print("append ", timer2.timeit(number=1000), "seconds")
print("comprehension ", timer3.timeit(number=1000), "seconds")
print("list range ", timer4.timeit(number=1000), "seconds")
执行结果:
concat 1.0439852095780988 seconds
append 0.06328647369343643 seconds
comprehension 0.02607241096934021 seconds
list range 0.018956846533179972 seconds
pop操作
# –*– coding: utf-8 –*–
# @Time : 2019/4/5 17:51
# @Author : Damon_duanlei
# @FileName : operate_list_pop.py
# @BlogsAddr : https://blog.csdn.net/Damon_duanlei
import timeit
X = list(range(2000000))
pop_zero = timeit.Timer("X.pop(0)", "from __main__ import X")
print("pop_zero ", pop_zero.timeit(number=1000), "seconds")
L = list(range(2000000))
pop_end = timeit.Timer("L.pop()", "from __main__ import L")
print("pop_end ", pop_end.timeit(number=1000), "seconds")
运行结果:
pop_zero 1.9001117043889595 seconds
pop_end 6.579917987492578e-05 seconds
测试 pop 操作: 从结果可以看出, pop 最后一个元素的效率远远高于 pop 第一个元素
数据是一个抽象的概念, 将其进行分类后得到程序设计语言中的基本类型. 如: int, float, char 等. 数据元素之间不是独立的, 存在特定的关系 , 这些关系便是结构, 数据结构指数据元素之间的关系.
python提供了很多现成的数据结构类型, 这些是系统自己定义好的. 不需要自己去定义的数据结构叫做 python 的内置数据结构, 比如列表, 元组,字典. 而有些数据组织方式, python 系统里面没有直接定义, 需要我们自己去定义实现这些数据的组织方式, 这些数据组织方式称为 python 的拓展数据结构, 比如 栈 , 队列等.
数据结构只是静态的描述了数据元素之间的关系.
高效的程序需要在数据结构的基础上设计和选择算法
程序 = 数据结构 + 算法
总结: 算法是为了解决实际问题而设计的, 数据结构是算法需要处理的问题的载体
抽象数据类型 (ADT) 的含义是指一个数学模型以及定义在此数学模型上的一组操作. 即把数据类型和数据类型上的运算捆在一起, 进行封装. 引入抽象数据类型的目的是把数据类型的表示和数据类型上运算的实现与这些数据类型和运算在程序中的引用隔开, 使他们相互独立.
最常见的数据运算有五种
在程序中, 经常需要将一组 (通常是同为某个类型 的) 数据元素作为整体管理和使用, 需要创建这种元素组, 用变量记录他们, 传进传出函数等. 一组数据中包含的元素个数可能发生变化 (可以增加或删除元素)
对于这种需求, 最简单的解决方案便是将这样一组元素看成一个序列, 用元素在序列里的位置和顺序, 表示实际应用中的某种有异议的信息, 或者表示数据之间的某种关系.
这样的一组序列元素的组织形式, 我们可以将其抽象为线性表. 一个线性表是某类元素的一个集合, 还记录着元素之间的一种顺序关系. 线性表示最基本的数据结构之一, 在实际程序中应用非常广泛. 他还经常被用作更复杂的数据结构的基础实现.
根据线性表的市级存储方式, 分为两种实现模型:
上图 a 表示的是顺序表的基本类型(元素内置), 数据元素本身连续存储, 每个元素所占的存储单元大小固定相同, 元素的下标是其逻辑地址, 而元素存储的物理地址 (实际内存地址) 可以通过存储区的起始地址Loc(e0)加上逻辑地址 (第 i 个元素) 与存储单元大小 ( c ) 的乘机计算而得, 即:
Loc(ei) = Loc(e0) + i * c
故, 访问指定元素时无需从头遍历, 通过计算便可获得对应地址, 其时间复杂度为 O(1).
如果元素的大小不统一, 则须采用图 b 的元素外置的形式, 将实际数据元素另行存储, 而顺序表中各单元位置保存对应元素的地址信息 (即链接) . 由于每个链接所需的存储量相同, 通过上述公式, 可以计算出元素链接的存储位置, 而后顺着链接找到实际存储的数据元素. 注意, 图b 中的 c 不再是数据元素的大小, 而是存储一个链接地址所需的存储量, 这个量通常很小.
图 b 这样的顺序表也被称为对实际数据的索引, 这是最简单的索引结构.
一个顺序表的完整信息包括两部分, 一部分是表中的元素集合, 另一部分是为实现正确操作而需记录的信息, 即有关表的整体情况的信息, 这部分信息主要包括元素存储区的容量和当前表中已有的元素个数两项
图 a 为一体式结构, 存储表信息的单元与元素存储区以连续的方式安排在一块存储区里, 两部分数据的整体形成一个完整的顺序表对象.
一体式结构整体性强, 易于管理. 但是由于数据元素存储区域是表的一部分, 顺序表创建后, 元素存储区就固定了.
图 b 为分离式结构, 表对象里只保存与整个表有关的信息 (即容量和元素个数), 实际数据元素存放在另一个独立的元素存储区里, 通过链接与基本表对象关联.
一体式结构由于顺序表信息区与数据区连续存储在一起, 所以若想更换数据区, 则只能整体搬迁, 即整个顺序表对象 (指存储顺序表的结构信息的区域) 改变了.
分离式结构若想更换数据区, 只需将表信息区中的数据区连接地址更新即可, 而该顺序表对象不变.
采用分离是结果的顺序表, 若将数据区更换为存储空间更大的区域, 则可以在不改变表对象的前提下对其数据存储区进行扩充, 所有使用这个表的方法都不必修改. 只要程序的运行环境还有空闲存储, 这种表结构就不会因为满了导致操作无法进行. 人们把采用这种技术实现的顺序表称为动态顺序表, 因为其容量可以在使用中动态改变.
扩充的两种策略
每次扩从增加固定数目的存储位置, 如每次扩充增加 10 个元素位置, 这种策略可称为线性增长.
特点: 节省空间,但是扩种操作频繁, 操作次数多.
每次扩充容量加倍, 如每次扩充增加一倍存储空间.
特点: 减少了扩充操作的执行次数, 但是可能会浪费空间资源. 以空间换时间, 推荐的方式.
如图所示, 为顺序表增加元素 111 的三种方式
a. 尾端加入元素, 时间复杂度 O(1)
b. 非保序的加入元素 (不常用), 时间复杂度为 O(1)
c. 保序的元素加入, 时间复杂度为 O(n)
a. 删除表尾元素, 时间复杂度为 O(1)
b. 非保序的元素删除, 时间复杂度 O(1)
c. 保序的元素删除, 时间复杂度 O(n)
python 中的 list 和 tuple 两种类型采用了顺序表的实现技术, 具有前面章节讨论的顺序表的所有性质.
tuple 是不可变类型, 即不变的顺序表, 因此不支持改变其内部状态的任何操作, 而其他方面, 则与 list 的性质类似.
python标准类型list就是一种元素个数可变的线性表, 可以加入和删除元素, 并在各种操作中维持已有元素的顺序, 而且还具有以下行为特征:
基于下标 (位置) 的高效元素访问和更新, 时间复杂度应该是 O(1);
为满足该特征, 应该采用顺序表技术, 表中元素保存在一块连续的存储区中.
允许任意加入元素, 而且在不断加入元素的过程中, 表对象的标识 (函数 id得到的值) 不变.
为满足该特征, 就必须能更换元素存储区, 并且为保证更换存储区时 list对象的标识 id 不变, 只能采用分离式实现技术.
在 python 的 官方实现中, **list 就是一种采用分离式技术实现的动态顺序表.**这就是为什么用 list.append(x) 或 list.insert(len(list), x) 比在指定位置插入元素效率高的原因.
在 python 官方实现中, list实现采用了如下的策略:
/* This over-allocates proportional to the list size, mak
ing room
* for additional growth. The over-allocation is mild, b
ut is
* enough to give linear-time amortized behavior over a l
ong
* sequence of appends() in the presence of a poorly-perf
orming
* system realloc().
* The growth pattern is: 0, 4, 8, 16, 25, 35, 46, 58, 7
2, 88, ...
*/
new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6);
/* check for integer overflow */
if (new_allocated > PY_SIZE_MAX - newsize) {
PyErr_NoMemory();
return -1;
} else {
new_allocated += newsize;
}
顺序表的结构需要预先知道数据大小来申请连续的存储空间, 而在进行扩充是有需要进行数据的搬迁, 所以使用起来并不是很灵活.
链表结构可以充分利用计算机内存空间, 实现灵活的内存动态管理.
链表 (Linked list) 是一种常见的基础数据结构, 是一种线性表, 但是不想顺序表一样两虚存储数据, 而是在每一个节点里存放下一个节点的位置信息 (即地址).
单向链表也叫做单链表, 是链表中最简单的一种形式, 它的每个节点包含两个域, 一个信息域 (元素域) 和一个链表域. 这个链接指向链表中的下一个节点, 而最后一个节点的链接域则指向空值.
头部添加元素逻辑
指定位置添加元素
删除节点
代码实现
# –*– coding: utf-8 –*–
# @Time : 2019/4/5 21:58
# @Author : Damon_duanlei
# @FileName : singlelinklist.py
# @BlogsAddr : https://blog.csdn.net/Damon_duanlei
# 创建一个节点类
class Node(object):
def __init__(self, content):
self.__content = content
self.next = None
@property
def get_value(self):
return self.__content
# 创建一个单项列表类
class SingleLinkList(object):
def __init__(self):
self.head = None
# 判断链表为空
def is_empty(self):
if self.head is None:
return True
else:
return False
# 返回链表的长度
def length(self):
count = 0
# 设定游标节点
cur_node = self.head
# 游标节点指向空,则处于链表尾节点
while cur_node:
cur_node = cur_node.next
count += 1
return count
# 遍历链表
def travel(self):
cur_node = self.head
while cur_node:
print(cur_node.get_value)
cur_node = cur_node.next
# 从头加入
def add(self, item):
node = Node(item)
# 逻辑上不需要判断当前链表是否为空
node.next = self.head
self.head = node
# 从尾部加
def append(self, item):
node = Node(item)
# 判断当前链表是否为空
if self.is_empty():
self.head = node
else:
cur_node = self.head
while cur_node.next:
cur_node = cur_node.next
cur_node.next = node
def insert(self, index, item):
"""从任意位置插入"""
if index <= 0:
self.add(item)
elif index >= self.length():
self.append(item)
else:
node = Node(item)
cur_count = 0
cur_node = self.head
while cur_count < index - 1:
cur_node = cur_node.next
cur_count += 1
# 游标到达插入索引的前一个节点
node.next = cur_node.next
cur_node.next = node
def romove(self, item):
"""删除一个节点"""
cur_node = self.head
# 当前节点的前一个节点
front_node = None
while cur_node:
if cur_node.get_value == item and front_node is None:
# 判断头节点
self.head = cur_node.next
break
elif cur_node.get_value == item and front_node:
front_node.next = cur_node.next
break
front_node = cur_node
cur_node = cur_node.next
def search(self, item):
cur_node = self.head
while cur_node:
if cur_node.get_value == item:
return True
cur_node = cur_node.next
return False
if __name__ == '__main__':
single_link = SingleLinkList()
链表失去了顺序表随机读取的有点, 同时链表由于增加了结点的指针域, 空间开销比较大, 但对存存储空间的使用要相对灵活.
链表与顺序表的各种操作复杂度如下:
注意: 虽然表面上看起来复杂度都是 O(n), 但是链表和顺序表在插入和删除时进行的是完全不同的操作. 链表的主要耗时操作是遍历查找, 删除和插入操作本身的复杂度是 O(1). 顺序表查找很快, 主要耗时操作是拷贝覆盖. 因为除了目标元素在尾部的特殊情况, 顺序表进行插入和删除时需要对操作点之后的所有元素进行前后移位操作, 只能通过拷贝和覆盖的方法进行.
一种更复杂的链表是"双向链表"或 “双面链表”. 每个节点有两个链接: 一个指向前一个节点, 当此节点为第一个节点是, 指向空值; 而另一个指向想一个节点, 当此节点为最后一个节点时, 指向空值.
指定位置插入节点
删除元素
# –*– coding: utf-8 –*–
# @Time : 2019/4/6 16:10
# @Author : Damon_duanlei
# @FileName : doublelinklist.py
# @BlogsAddr : https://blog.csdn.net/Damon_duanlei
from singlelinklist import SingleLinkList
class DoubleNode(object):
def __init__(self, content):
self.__content = content
self.prev = None
self.next = None
@property
def get_value(self):
return self.__content
class DoubleLinkList(SingleLinkList):
"""判空, 遍历, 长度 , 搜索方法与单向链表一直,通过继承可直接使用"""
def add(self, item):
"""重写向表头添加add方法"""
node = DoubleNode(item)
if self.is_empty():
self.head = node
else:
# node 的next指向当前头节点
node.next = self.head
# 当前头节点的prev 指向node
self.head.prev = node
# 将链表的 head 指向node
self.head = node
def append(self, item):
"""重写向尾部追加的append方法"""
node = DoubleNode(item)
if self.is_empty():
self.head = node
else:
cur_node = self.head
# # 移动到链表尾部
while cur_node.next:
cur_node = cur_node.next
# 将尾节点cur的next指向node
cur_node.next = node
# 将node的prev指向cur
node.prev = cur_node
def insert(self, index, item):
"""重写向任意位置插入的insert方法"""
# 插入点索引小于0 在头部添加
if index <= 0:
self.add(item)
# 插入索引大于等于当前链表长度, 向尾部追加
elif index >= self.length():
self.append(item)
else:
node = DoubleNode(item)
count = 0
# 移动到插入节点的前一个节点
cur_node = self.head
while count < index - 1:
cur_node = cur_node.next
count += 1
node.next = cur_node.next
cur_node.next.prev = node
cur_node.next = node
node.prev = cur_node
def romove(self, item):
cur_node = self.head
while cur_node:
# 找到要删除的节点
if cur_node.get_value == item:
# 判断是否为头节点
if cur_node == self.head:
self.head = cur_node.next
# 判断链表是否只有⼀个结点
if cur_node.next:
cur_node.next.prev = None
else:
cur_node.prev.next = cur_node.next
if cur_node.next:
cur_node.next.prev = cur_node.prev
break
else:
cur_node = cur_node.next
if __name__ == '__main__':
double_link = DoubleLinkList()
double_link.append(4)
double_link.add(2)
double_link.add(1)
double_link.append(5)
double_link.insert(2, 3)
double_link.insert(0, 0)
double_link.insert(10, 100)
double_link.romove(1)
double_link.romove(3)
double_link.travel()
print(double_link.length())
print(double_link.search(618))
单链表的一个 是单向循环链表, 链表中最后一个节点的 next 域不再为 None, 而是指向链表的头节点.
栈 (stack) , 有些地方称为堆栈, 是一种容器, 可存入数据元素, 访问元素, 删除元素. 它的特点在于只能允许在容器的一端 (称为栈顶端指标, 英文: top) 进行加入数据 (英语: push) 和输出数据 (英语:pop) 的运算. 没有了位置概念, 保证任何时候可以访问, 删除的元素都是此前最后存入的那个元素, 确定了一种默认的访问顺序.
由于栈数据结构只允许在一端进行操作, 因而按照后进先出 (LIFO, Last In First Out) 的原理运作.
# –*– coding: utf-8 –*–
# @Time : 2019/4/6 19:59
# @Author : Damon_duanlei
# @FileName : my_stack.py
# @BlogsAddr : https://blog.csdn.net/Damon_duanlei
class MyStack(object):
"""栈"""
def __init__(self):
self.__items = []
# self.__items = SingleLinkList()
def push(self, item):
"""添加一个新的元素item到栈顶"""
self.__items.append(item) # O(1)
# self.__items.insert(0, item) #O(n)
def pop(self):
"""弹出栈顶元素"""
return self.__items.pop()
# self.__items.pop(0)
def peek(self):
"""返回栈顶元素"""
return self.__items[-1]
# return self.__items[len(self.__items)-1]
def is_empty(self):
"""判断栈是否为空"""
return self.__items == []
def size(self):
"""返回栈的元素个数"""
return len(self.__items)
队列 (queue) 是只允许在一端进行插入操作, 而在另一端进行删除操作的线性表.
队列是一种先进先出的 ( First In First Out ) 的线性表, 简称 FIFO . 允许插入的一端为队尾, 允许删除的一端为队头. 队列不允许在中间部位进行操作.
同栈一样, 队列也可以用顺序表或者链表实现.
# –*– coding: utf-8 –*–
# @Time : 2019/4/6 20:08
# @Author : Damon_duanlei
# @FileName : my_queue.py
# @BlogsAddr : https://blog.csdn.net/Damon_duanlei
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()