数据结构和算法
简单的排序算法
如果 a+b+c=1000,且 a2+b2=c^2(a,b,c 为自然数),如何求出所有a、b、c可能的组合?
- 枚举法:三重循环
算法就是一种思想,和描述它的语言无关。输入、输出、有穷、确定性、可行性
时间离不开计算机的执行环境,如何在脱离计算机环境时候描述程序的优劣:时间复杂度
每台机器执行的总时间不同,但执行的基本运算数量大体相同。
T = 1000 * 1000 * 1000 * 2
T = 2000 * 2000 * 2000 * 2 (2000 规模问题)
T = N * N * N * 2 (N:跟问题规模相关的)
计算步骤 T(n) = n^3 * 2 称为时间复杂度,再简化
g(n) = n^3 数量级 ,大O表示法
- 最坏时间复杂度,是一种保证:在此步骤内,一定会执行完。
- 顺序、条件(分支)、循环:代表所有的事件。因此计算时间复杂度,要了解这三种。
- 顺序:加法
- 循环:乘法
- 分支:取复杂度最大值(最多的那个分支)
只保留最高次项,忽略常数和最高次
Python内置类型性能分析
使用 timeit 测试4种列表的生成优劣
import
li1 = [1,2]
li2 = [23,4]
li3 = li1+li2
li4 = [i for i in range(10000)]
li5 = list(range(10000))
def test1():
li = [i for i in range(10000)]
def test5():
li = []
for i in range(10000):
li.extend([i])
timer1 = Timer('test1()',"from __main__ import test1")
print("concat ",timer1.timeit(number=1000), "seconds")
使用 + 的效率最低:会生成一个新列表。这情况在字符串上也是一样。(%s要优于+)
使用extend()方法不会有新列表。
def t6():
li = []
for i in range(10000):
li.append(i) #尾部
def t6():
li = []
for i in range(10000):
li.insert(0,i) #头部
timer6 = Timer('t6()',"from __main__ import t6")
print 'append',timer6.timeit(1000)
上述append(i)
明显优于insert(0,i)
,这是由list的数据存储方式决定的。
x = range(10000)
x.pop() #尾部
x.pop(0) #头部
尾部速度优于头部
基本数据类型
list 和 dict 是不能算作基本数据类型的,是一个容器,是Python给封装好的高级数据结构。
基本数据类型:int/float/char/str/boolean。
数据的组织方式
一组数据如何保存:数据结构。
程序 = 数据结构 + 算法
总结:算法是为了解决实际问题而设计的,数据结构是算法需要处理的问题载体
抽象数据类型:一种面向对象的思想。
class Stus():
def add()
def del()
def sort()
抽象数据类型(ADT)的含义是指一个数学模型以及定义在此数学模型上的一组操作。即把数据类型和数据类型上的运算捆在一起,进行封装。引入抽象数据类型的目的是把数据类型的表示和数据类型上运算的实现与这些数据类型和运算在程序中的引用隔开,使它们相互独立。
最常用的数据运算有五种:插入/删除/修改/查找/排序
顺序表
顺序表的形式
- 顺序表,将元素顺序地存放在一块连续的存储区里,元素间的顺序关系由它们的存储顺序自然表示。
- 基本布局
- 元素外置的顺序表
- 链表,将元素存放在通过链接构造起来的一系列存储块中。
最小寻址单位 一个字节Byte(8位)。
当list存储不同数据类型时候,会选择元素外置的方式:原本list里放的是外置元素的地址,如图。
内存是一个连续的存储单元(字节)每个字节一个地址标识(如0x01)
int 1 转换成2进制 0000 0001 但是到内存中为 0*8*3 00000001 四个字节
顺序表的特征:
- 类型决定在内存中占用多少单元
- 0*8*3 00000001 如果按char取出,就不是 int 1 了
- list连续的存在内存中(索引很方便):l l+c l+2c (c为一个存储单元大小)
顺序表结构和实现
结构
在别的语言中的实现(Python中已经封装好了)
由表头信息+数据区构成
在开始时候预估到所有存储空间(容量8和元素个数4,那么空的为8-4)
基本实现方式
- 一体式 直接连续。读取方便。如果存不下了,重新申请(获得新地址+数据搬迁+原数据释放
- 分离式 表头+地址。间接读取,多一步。区别为:最后一步表头直接指向新地址;表头地址是不变的。
孰优孰劣?
扩充的两种策略
- 每次扩充增加固定数目的存储位置,如每次扩充增加10个元素位置,这种策略可称为线性增长。
特点:节省空间,但是扩充操作频繁,操作次数多。 - 每次扩充容量加倍,如每次扩充增加一倍存储空间。
特点:减少了扩充操作的执行次数,但可能会浪费空间资源。以空间换时间,推荐的方式。
链表
链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是不像顺序表一样连续存储数据,而是在每一个节点(数据存储单元)里存放下一个节点的位置信息(即地址)。
线性表的扩充。
- 表元素域elem用来存放具体的数据。
- 链接域next用来存放下一个节点的位置(python中的标识)
- 变量p指向链表的头节点(首节点)的位置,从p出发能找到表中的任意节点。
头节点 +...+ 尾节点
实现方式
is_empty() 链表是否为空
length() 链表长度
travel() 遍历整个链表
add(item) 链表头部添加元素
append(item) 链表尾部添加元素
insert(pos, item) 指定位置添加元素
remove(item) 删除节点
search(item) 查找节点是否存在
a b 赋值
a = 10
b = 20
a,b = b,a
a只是一个引用、变量(隐式声明,指向什么就是什么)。在别的语言中,在定义的时候 必须声明变量的类型。
Python变量的本质:
上图并不是node1包含了node2,而是next变量指向node2。
class SingleNode(object):
"""单链表的结点"""
def __init__(self,item):
# _item存放数据元素
self.item = item
# _next是下一个节点的标识
self.next = None
栈与队列
栈描述的是一种操作:先入后出。顺序表、链表是一种存储方式。
例:用栈解析带括号的计算式
队列:先进先出(深度遍历)
栈具体操作
Stack() 创建一个新的空栈
push(item) 添加一个新的元素item到栈顶 入栈、压栈
pop() 弹出栈顶元素
peek() 返回栈顶元素
is_empty() 判断栈是否为空
size() 返回栈的元素个数
队列具体操作
Queue() 创建一个空的队列
enqueue(item) 往队列中添加一个item元素
dequeue() 从队列头部删除一个元素
is_empty() 判断一个队列是否为空
size() 返回队列的大小
双端队列
双端队列(deque,全名double-ended queue),是一种具有队列和栈的性质的数据结构。
双端队列中的元素可以从两端弹出,其限定插入和删除操作在表的两端进行。双端队列可以在队列任意一端入队和出队。