Python数据结构与算法
概念
- 数据是一个抽象的概念,将其进行分类后得到程序设计语言中的基础类型
算法与数据结构的区别
- 数据结构只是静态的描述了数据元素之间的关系
- 高效的程序需要在数据结构的基础上设计和选择算法
- **程序 = 数据结构 + 算法 **
- 总结:算法是为了解决实际问题而设计的,数据结构是算法需要处理的问题载体
抽象数据类型(Abstract Data Type)
抽象数据类型(ADT)的含义是指一个数据模型以及定义在此数学模型上的一组操作。即把数据类型和数据类型上的运算捆在一起,进行封装。引入抽象数据类型的目的是把数据类型的表示和数据类型上运算的实现与这些数据类型和运算在程序中的引用隔开,使他们相互独立
常用的数据运算有五种:
- 插入
- 删除
- 修改
- 查找
- 排查
顺序表 -- 列表和元组的底层都是顺序表实现
列表的顺序表结构是采用分离式结构,并且是元素外置方式
- 顺序表,将元素顺序地存放在一块连续的存储区里,元素间的顺序关系由他们的存储顺序自然表示
- 链表,将元素存放在通过链接构造起来的一系列存储块中
顺序表的存储方式--顺序表的结构
- 顺序表的结构:表头+表数据
- 表头:存储本元素表的容量信息(数据有多少该数据表大小就会被分配多少)和本元素的个数信息
- 表数据:其中存入的就是列表中的实际数据
列表下标为什么以0开始计算? 列表是以顺序表的结构存储的,时间复杂度是O(1)
答:方便内存地址计算和数据寻址
/%E6%88%AA%E5%9B%BE/%E9%A1%BA%E5%BA%8F%E8%A1%A8%E7%BB%93%E6%9E%84.png)
注意:列表保存数据的方式是使用顺序表来存数据,上图是列表中存入整数类型的数据时内存地址中存入的就是整数本身,而当列表中存入的数据是字符时,内存地址中存入的是该字符的内存地址,如果要取数据时,就利用顺序表的下标取出字符的内存,然后根据内存地址找到字符数据
/%E6%88%AA%E5%9B%BE/%E5%88%97%E8%A1%A8%E4%BF%9D%E5%AD%98%E6%95%B0%E6%8D%AE%E6%96%B9%E5%BC%8F.png)
顺序表的两种基本实现方式
- 一体式:一体式就是列表顺序表的表头和列表顺序表的数据没有分离,是一体的
- 分体式:分体式就是顺序表中的表头和表数据分别存储,表头中存储的表数据的地址指向
- 一般列表可变数据类型采用分体式(内存地址不变),元组不可变类型采用一体式(内存地址改变)
/%E6%88%AA%E5%9B%BE/%E9%A1%BA%E5%BA%8F%E8%A1%A8%E7%9A%84%E4%B8%A4%E7%A7%8D%E5%9F%BA%E6%9C%AC%E5%AE%9E%E7%8E%B0%E6%96%B9%E5%BC%8F.png)
当两种不同的顺序表实现方式出现新增数据时
原来申请的列表数据存储的内存地址大小就不够了,append 添加列表数据时就需要重新申请内存地址,将原数据搬到新的内存地址中存储
一体式和分体式进行数据添加时的区别:
- 一体式 结构添加数据时 列表的内存地址会改变
- 一体式结构由于顺序表信息区与数据区连续存储在一起,所以若想更换数据区,则只能整体搬迁,即整个顺序表对象(指存储顺序表的结构信息的区域)改变了。
- 分体式 结构添加数据时 列表的内存地址不会改变(因为分体式的表头地址没变,只是表头中数据指向发生改变)
- 分离式结构若想更换数据区,只需将表信息区中的数据区链接地址更新即可,而该顺序表对象不变。
/%E6%88%AA%E5%9B%BE/%E5%88%97%E8%A1%A8%E6%89%A9%E5%AE%B9.png)
添加数据时,元素存储区扩充的两种策略:
- 每次扩充增加固定数目的存储位置,如每次扩充增加10个元素位置,这种策略可以称为线性增长
- 特点:节省空间,但是扩充操作频繁,操作次数多
- 每次扩充容量加倍,如每次扩充增加一倍存储空间
- 特点:减少了扩充操作的执行次数,但可能会浪费空间资源,以空间换时间 推荐的方式
/%E6%88%AA%E5%9B%BE/%E5%88%97%E8%A1%A8%E6%93%8D%E4%BD%9C%E7%9A%84%E6%97%B6%E9%97%B4%E5%A4%8D%E6%9D%82%E5%BA%A6.png)
链表 -- python 没有内置这种数据结构,需要自己定义
- 为什么需要链表?
- 答:顺序表的构建需要预先知道数据大小来申请连续的存储空间,而在进行扩充时又需要进行数据的搬迁,所以使用起来并不是很灵活;链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理
- 链表的定义:
- 链表(Linked list)是一种常见的基础数据结构,是一种线性表,但不想顺序表一样连续存储数据,而是在每一个节点(数据存储单元)里存放下一个节点的位置信息(即地址)
/%E6%88%AA%E5%9B%BE/%E9%93%BE%E8%A1%A8%E7%9A%84%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.png)
python中的顺序表
python中的list和tuple两种类型采用了顺序表的实现技术,具有前面讨论的顺序表的所有性质;tuple 元组是不可变类型,即不变得顺序表,因此不支持改变其内部状态的任何操作,而其他方面,则与list的性质类似
-
- 也就是说列表和元组都是顺序表来实现的
-
- 元组除了不能被修改,其他特性与列表相同
-
- 那列表和元组都有一体式或分体式结构和元素内置和元素外置的方式
-
- 列表 -- 顺序表--分离式结构
1.单向链表
- 单向链表也叫做单链表,是链表中最简单的一种形式,它的每个节点包括两个域,一个信息域(元素域)和一个链接域。这个链接指向链表中的下一个节点,而最后一个节点的链接或则指向一个空值
/%E6%88%AA%E5%9B%BE/%E5%8D%95%E5%90%91%E9%93%BE%E8%A1%A8.png)
- 表元素域elem用来存放具体的数据。
- 链接域next用来存放下一个节点的位置(python中的标识)
- 变量p指向链表的头节点(首节点)的位置,从p出发能找到表中的任意节点
-
字典的底层实现--也是特殊的列表存储--叫做哈希列表
字典的key做hash运算 ----> 得到唯一值 index----> 唯一值形成hash列表 ----> 列表中存入字典的 value
/%E6%88%AA%E5%9B%BE/%E5%AD%97%E5%85%B8%E5%BA%95%E5%B1%82%E5%AE%9E%E7%8E%B0-%E5%93%88%E5%B8%8C%E5%88%97%E8%A1%A8.png)
单链表的操作 --- 操作链表封装的操作函数
- 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
class SingleLinkList(object):
"""单向链表类"""
def __init__(self, node=None):
self.__head = node
def is_empty(self):
"""链表是否为空"""
return self.__head == None
def length(self):
"""链表长度"""
# 记录列表长度的count
count = 0
# 游标
cur = self.__head
while cur != None:
# 游标右移
cur = cur.next
# 累加
count += 1
# cur == None 表示指向到最后一个节点的next区域
return count
def travel(self):
"""遍历整个链表"""
# 游标
cur = self.__head
while cur != None:
print(cur.item, end=" ")
# 在游标移动之前需要打印
cur = cur.next
print("")
def add(self, item):
"""链表头部添加元素"""
# 创建新节点
node = Node(item)
# 新节点的next指向旧的头节点
node.next = self.__head
# 头指针指向新的节点
self.__head = node
def append(self, item):
"""链表尾部添加数据"""
node = Node(item)
# 如果链表为空
if self.is_empty():
self.__head = node
# 链表不为空时
else:
cur = self.__head
while cur.next != None:
cur = cur.next
# 跳出循环,cur.next() == None cur 指向最后一个节点
# 最后一个节点next指向新的节点
cur.next = node
node.next = None
def insert(self, pos, item):
"""指定位置添加元素"""
# 头部添加
if pos <= 0:
self.add(item)
# 尾部添加
elif pos >= self.length():
self.append(item)
# 指定pos位置添加元素
else:
node = Node(item)
count = 0
cur = self.__head
while count < (pos - 1 ):
cur = cur.next
count += 1
# count == pos - 1 cur游标指向pos节点的前一个节点
node.next = cur.next
cur.next = node
def remove(self, item):
"""链表删除指定item的元素操作"""
# 游标
cur = self.__head
# cur的前一个游标
per = None
while cur != None:
# 找到了要删除的元素
if cur.item == item:
# 1.要求删除的元素是头节点
if cur == self.__head:
# 头指针指向1号节点
self.__head = cur.next
# 2.要删除的元素不是头节点
else:
per.next = cur.next
return
else:
# 在cur移动之前将值赋值给pre 形成一前一后
per = cur
# 游标右移动
cur = cur.next
def search(self, item):
"""判断传入的元素是否在链表内 bool"""
cur = self.__head
while cur != None:
# 表示元素已经找到了
if cur.item == item:
return True
else:
# 循环未退出 继续移动比较
cur = cur.next
# 循环退出表示元素未找到
return False
if __name__ == '__main__':
ll = SingleLinkList()
ll.add('curry')
ll.add('james')
ll.append("harden")
ll.insert(2, 'ray-all')
print("链表长度:%d" % ll.length())
ll.travel()
ll.remove('harden')
print("链表长度:%d" % ll.length())
ll.travel()
print(ll.search('curry'))
"""
链表长度:4
james curry ray-all harden
链表长度:3
james curry ray-all
True
"""
链表与顺序表的对比
链表失去了顺序表随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大,但对存储空间的使用要相对灵活
链表与顺序表的各种操作复杂度如下所示:
| 操作 | 链表 | 顺序表 |
|---|---|---|
| 访问元素 | O(n) | O(1) |
| 在头部插入/删除 | O(1) | O(n) |
| 在尾部插入/删除 | O(n) | O(1) |
| 在中间插入/删除 | O(n) | O(n) |
2.双向链表
/%E6%88%AA%E5%9B%BE/%E5%8F%8C%E5%90%91%E9%93%BE%E8%A1%A8.png)
class Node(object):
"""单链表节点类"""
def __init__(self, item):
# 保存节点的真实值
self.item = item
# 保存下一个节点的地址
self.next = None
# 保存上一个节点的地址
self.pre = None
class DoubleLinkList(object):
"""双向链表类"""
def __init__(self, node=None):
self.__head = node
def is_empty(self):
"""链表是否为空"""
return self.__head == None
def length(self):
"""链表长度"""
# 记录列表长度的count
count = 0
# 游标
cur = self.__head
while cur != None:
# 游标右移
cur = cur.next
# 累加
count += 1
# cur == None 表示指向到最后一个节点的next区域
return count
def travel(self):
"""遍历整个链表"""
# 游标
cur = self.__head
while cur != None:
print(cur.item, end=" ")
# 在游标移动之前需要打印
cur = cur.next
print("")
def search(self, item):
"""判断传入的元素是否在链表内 bool"""
cur = self.__head
while cur != None:
# 表示元素已经找到了
if cur.item == item:
return True
else:
# 循环未退出 继续移动比较
cur = cur.next
# 循环退出表示元素未找到
return False
def add(self, item):
"""链表头部添加元素"""
# 创建新节点
node = Node(item)
# 链表为空
if self.is_empty():
# 头指针指向新的结点
self.__head = node
else:
node.next = self.__head
self.__head.pre = node
self.__head = node
def append(self, item):
"""链表尾部添加数据"""
node = Node(item)
# 如果链表为空
if self.is_empty():
self.__head = node
# 链表不为空时
else:
cur = self.__head
while cur.next != None:
cur = cur.next
# 跳出循环,cur.next() == None cur 指向最后一个节点
# 最后一个节点next指向新的节点
cur.next = node
# 给新节点的pre指向,之前的最后一个节点
node.pre = cur
def insert(self, pos, item):
"""指定位置添加元素"""
# 头部添加
if pos <= 0:
self.add(item)
# 尾部添加
elif pos >= self.length():
self.append(item)
# 指定pos位置添加元素
else:
node = Node(item)
# 计数
count = 0
# 游标
cur = self.__head
# 寻找pos前一个节点
while count < pos:
cur = cur.next
count += 1
# count == pos - 1 cur游标指向pos节点的前一个节点
node.next = cur
node.pre = cur.pre
cur.pre.next = node
cur.pre = node
def remove(self, item):
"""链表删除指定item的元素操作"""
# 游标
cur = self.__head
# cur的前一个游标
per = None
while cur != None:
# 找到了要删除的元素
if cur.item == item:
# 1.要求删除的元素是头节点
if cur == self.__head:
# 头指针指向1号节点
self.__head = cur.next
if cur.next:
cur.next.pre = None
# 2.要删除的元素不是头节点
else:
cur.pre.next = cur.next
if cur.next:
cur.next.pre = cur.pre
return
else:
# 游标右移动
cur = cur.next
if __name__ == '__main__':
ll = DoubleLinkList()
ll.add('curry')
ll.add('james')
ll.append("harden")
ll.insert(2, 'ray-all')
print("链表长度:%d" % ll.length())
ll.travel()
ll.remove('harden')
print("链表长度:%d" % ll.length())
ll.travel()
print(ll.search('curry'))
3.单向循环链表
/%E6%88%AA%E5%9B%BE/%E5%8D%95%E5%90%91%E5%BE%AA%E7%8E%AF%E5%88%97%E8%A1%A8.png)
栈 和 队列
栈 --- 栈的数据类型在python中是没有定义的,需要自定义栈实现
- 栈 --- 类似于羽毛球桶,只有一个出口和一个进口
栈(stack),有些地方称为堆栈,是一种容器,可存入数据元素、访问元素、删除元素,它的特点在于只能允许在容器的一端(称为栈顶端指标,英语:top)进行加入数据(英语:push)和输出数据(英语:pop)的运算。没有了位置概念,保证任何时候可以访问、删除的元素都是此前最后存入的那个元素,确定了一种默认的访问顺序。
由于栈数据结构只允许在一端进行操作,因而按照后进先出(LIFO, Last In First Out)的原理运作。
/%E6%88%AA%E5%9B%BE/%E6%A0%88%E7%9A%84%E7%BB%93%E6%9E%84%E6%A8%A1%E5%9E%8B.png)
- 栈的两种实现方式
- 列表形式
- 列表头部作为栈顶
- 列表尾部作为栈顶
- 单链表形式
- 列表形式
/%E6%88%AA%E5%9B%BE/%E6%A0%88%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%96%B9%E5%BC%8F-%E5%88%97%E8%A1%A8%E5%92%8C%E5%8D%95%E9%93%BE%E8%A1%A8.png)
- 栈的代码实现
"""
Stack() 创建一个新的空栈
push(item) 添加一个新的元素item到栈顶
pop() 弹出栈顶元素
peek() 返回栈顶元素
is_empty() 判断栈是否为空
size() 返回栈的元素个数
"""
class Stack(object):
"""栈的数据结构 LIFO """
def __init__(self):
"""顺序表的数据结构实现"""
self.items = []
def push(self, item):
"""添加一个新的元素到栈顶"""
# 以列表的头部为栈顶时
# self.items.insert(0, item) # O(N)
# 以列表的尾部为栈顶时
self.items.append(item) # O(1)
def pop(self):
"""弹出栈顶元素"""
# 列表头部
# return self.items.pop(0) # O(N)
# 列表尾部
return self.items.pop() # O(1)
def peek(self):
"""返回栈顶元素"""
return self.items[len(self.items) -1]
def is_empty(self):
"""判断是否为空"""
return self.items == []
def size(self):
"""返回栈的元素个数"""
return len(self.items)
if __name__ == "__main__":
stack = Stack()
stack.push("hello")
stack.push("world")
stack.push("itcast")
print("栈的大小", stack.size())
print("栈的顶部元素", stack.peek())
print("删除栈的元素", stack.pop())
print("删除栈的元素", stack.pop())
print("删除栈的元素", stack.pop())
队列 -- 先进先出结构
-
队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
队列是一种先进先出的(First In First Out)的线性表,简称FIFO。允许插入的一端为队尾,允许删除的一端为队头。队列不允许在中间部位进行操作!假设队列是q=(a1,a2,……,an),那么a1就是队头元素,而an是队尾元素。这样我们就可以删除时,总是从a1开始,而插入时,总是在队列最后。这也比较符合我们通常生活中的习惯,排在第一个的优先出列,最后来的当然排在队伍最后。
-
队列的实现:和栈相同,队列可以用顺序表或者链表实现
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("itcast")
print q.size()
print q.dequeue()
print q.dequeue()
print q.dequeue()
-
双端队列:类似两个栈的底部互相连接,两头都可以进出数据
-
双端队列(deque,全名double-ended queue),是一种具有队列和栈的性质的数据结构。
双端队列中的元素可以从两端弹出,其限定插入和删除操作在表的两端进行。双端队列可以在队列任意一端入队和出队。
-
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()
冒泡排序
- 主要思想:
-
- 列表中的前一个数和后一个数比较,大则交换位置
- for 循环次数是列表长度n-1 次
- 内循环的
-
def bubble_sort(list):
n = len(list)
# 控制总的趟数 n-1 趟, 第一趟n-1个元素
for j in range(n - 1):
count = 0
# 一趟比较交换操作,将最大的值交换到后面, 第二趟是n-2个元素,因为最大的元素已经在最后面了
for i in range(0, n - 1 - j):
# 前一元素比后一个元素大,需要交换位置
if list[i] > list[i + 1]:
# 解包,交换
list[i], list[i + 1] = list[i + 1], list[i]
# 累加
count += 1
# 说明一趟交换下来都没有发生数据交换,则该列表是有序的列表
if count == 0:
print('有序列表')
break
if __name__ == '__main__':
list = [1, 3, 2, 66, 33, 77, 22, 99, 4]
bubble_sort(list)
print(list)
选择排序
-
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对n个元素的表进行排序总共进行至多n-1次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。
-
排序步骤:
-
- 寻找无序列表中的最小(大)值
- 将最小(大)值放到最前(后)面
- 再在无序列表中寻找最小(大)值,重复2
-
def selection_sort(list):
"""选择排序(最小值)"""
n = len(list)
for i in range(n-1):
min_num = i
# 第一步:找到无序列表中最小的一个值
for j in range(1+i, n):
if list[j] < list[min_num]:
min_num = j
# 第二步:将最下的值换到最前面去
if min_num != i:
list[i], list[min_num] = list[min_num], list[i]
if __name__ == '__main__':
list = [1, 3, 2, 66, 33, 77, 22, 99, 4]
selection_sort(list)
print(list)
插入排序
- 插入排序(英语:Insertion Sort)是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
快速排序
希尔排序
归并排序
搜索
- 搜索是在一个项目集合中找到一个特定项目的算法过程。搜索通常的答案是真的或假的,因为该项目是否存在。 搜索的几种常见方法:顺序查找、二分法查找、二叉树查找、哈希查找
- 二分查找:二分查找又称折半查找,优点是比较次数少,查找速度快,平均性能好;其缺点是要求待查表为有序表,且插入删除困难。因此,折半查找方法适用于不经常变动而查找频繁的有序列表。首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。
/%E6%88%AA%E5%9B%BE/%E6%90%9C%E7%B4%A2%E7%AE%97%E6%B3%95-%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E6%B3%95.png)
- 递归实现方法 if..elif..else + 循环调原函数
def binary_search(list, item):
"""
搜索的二分查找方法实现
:param list: 需要查找的列表
:param item: 查找列表中是否存在该元素
:return: true则表示元素在列表中,false表示元素不在该列表中
"""
n = len(list)
# 元素没有找到的递归出口
if n == 0:
return False
min_num = n // 2
# 元素找到了,正好是列表中间值
if item == list[min_num]:
return True, min_num
# 元素在列表左半部分,递归调用
elif item < list[min_num]:
return binary_search(list[:min_num], item)
# 元素在列表右半部分,递归调用
else:
return binary_search(list[min_num+1:], item)
if __name__ == '__main__':
list = [1, 2, 3, 6, 33, 77, 223, 993]
print(binary_search(list, 3))
print(binary_search(list, 33))
- 非递归实现方法 -- while 死循环+循环赋值给第一个下标个最后一个下标
def binary_search(alist, item):
first = 0
last = len(alist)-1
while first<=last:
midpoint = (first + last)/2
if alist[midpoint] == item:
return True
elif item < alist[midpoint]:
last = midpoint-1
else:
first = midpoint+1
return False
testlist = [0, 1, 2, 8, 13, 17, 19, 32, 42,]
print(binary_search(testlist, 3))
print(binary_search(testlist, 13))

浙公网安备 33010602011771号