Python数据结构与算法_第4节_栈 & 队列 & 排序与搜索
栈(stack)
栈/堆栈(stack),是一种容器,可存入数据元素、访问元素、删除元素。是一种只允许在容器的一端(栈顶端top)加入数据(push)和输出数据(pop)的运算 。
后进先出(LIFO, Last In First Out):像只有一个出口的杯子一样,最先被添加的数据会在栈的底部,在读取时最后进入的数据先被读取。
线性表(顺序表、链表)的逻辑和栈一样;线性表描述的是数据怎么存放,栈描述的是操作。在栈中可以运用到链表。
栈结构实现
栈可以用顺序表(list)实现,也可以用链表实现。
栈的操作
Stack() 创建一个新的空栈
push(item) 添加一个新的元素item到栈顶
pop() 弹出栈顶元素
peek() 返回栈顶元素
is_empty() 判断栈是否为空
size() 返回栈的元素个数
栈的实现
# coding: utf-8
class Stack(object):
"""栈"""
def __init__(self):
# 用顺序表来定义栈
self.__list = []
def is_empty(self):
"""判断栈是否为空"""
return self.__list == []
# return not self.__list
def push(self, item):
"""加入一个新的元素item到栈顶"""
# 为了时间复杂度最低,在尾部添加item(O(1))
self.__list.append(item)
def pop(self):
"""弹出栈顶元素"""
return self.__list.pop()
def peek(self):
"""返回栈顶元素"""
# 如果列表不为空,则返回栈顶元素
if self.__list:
return self.__list[len(-1]
# 如果列表为空,则返回None
else:
return None
def size(self):
"""返回栈的元素的个数"""
return len(self.__list)
if __name__ == "__main__":
s = Stack()
s.push(1)
s.push(2)
s.push(3)
s.push(4)
print(s.pop())
print(s.pop())
print(s.pop())
print(s.pop())
# 后进先出
# 4
# 3
# 2
# 1
队列(queue)
队列(queue)是只允许在一端插入数据,在另一端删除数据的线性表 。
先进先出(FIFO):插入数据的一端为队尾,删除数据的一端为对头。队列不允许在中间部位进行操作。
队列结构实现
队列的操作
Queue() 创建一个空的队列
enqueue(item) 进入队列,往队列中添加一个item
dequeue() 退出队列,从队列头部删除一个元素
is_empty() 判断一个队列是否为空
size() 返回队列的大小
队列的实现
# coding: utf-8
class Queue(object):
"""队列"""
def __init__(self):
self.__list = []
def is_empty(self):
"""判断一个队列是否为空"""
return self.__list == []
# return not self.__list
def enqueue(self, item):
"""往队列中添加一个item"""
# 1. 在尾部添加O(1),头部删除O(n)
# 2. 在尾部删除O(1),头部添加O(n)
# 选择哪一种具体取决于在调用queue时,最常使用的是出队还是入队。
# 尾部添加元素
self.__list.append(item)
# 头部添加元素
# self.__list.insert(0, item)
def dequeue(self):
"""从队列头部删除一个item"""
# 头部删除元素
return self.__list.pop(0)
# 从尾部删除元素
# return self.__list.pop()
def size(self):
"""返回队列的大小"""
return len(self.__list)
if __name__ == "__main__":
s = Stack()
s.enqueue(1)
s.enqueue(2)
s.enqueue(3)
s.enqueue(4)
print(s.dequeue())
print(s.dequeue())
print(s.dequeue())
print(s.dequeue())
# 1
# 2
# 3
# 4
双端队列(deque)
双端队列(deque/ double-ended queue):具有队列和栈性质的数据结构。
双端队列中元素可以从两端弹出,其限定插入和删除操作在表的两端进行。双端队列可以在队列任意一端入队和出队。
相当于两个栈底部合在一起了。
操作
Deque() 创建一个空的双端队列
add_front(item) 从队头加入一个item元素
add_rear(item) 从队尾加入一个item元素
remove_front() 从队头删除一个item元素
remove_rear() 从队尾删除一个item元素
is_empty() 判断双端队列是否为空
size() 返回队列的大小
实现
# coding: utf-8
class Deque(object):
"""双端队列"""
def __init__(self):
self.__list = []
def is_empty(self):
"""判断队列是否为空"""
return self.__list == []
def add_front(self, item):
"""在队头添加元素"""
self.__list.insert(0,item)
def add_rear(self, item):
"""在队尾添加元素"""
self.__list.append(item)
def remove_front(self):
"""从队头删除元素"""
return self.__list.pop(0)
def remove_rear(self):
"""从队尾删除元素"""
return self.__list.pop()
def size(self):
"""返回队列大小"""
return len(self.__list)
排序与搜索(sorting algorithm)
排序算法(sorting algorithm)是一种能将一串数据依照特定顺序进行排列的算法。
排序算法的稳定性
稳定性:稳定的排序算法会让原本有相等键值的纪录维持相对次序。也就是如果一个排序算法是稳定的,当有两个相等键值的纪录R和S,且在原本的列表中R出现在S之前,在排序过的列表中R也将会是在S之前。
ex. 假设将要用以下元组的第一个数字们来排序:(4, 1) (3, 1) (3, 7)(5, 6)不稳定的排序算法得到的是b结果,稳定的排序算法得到的是a结果。 a. (3, 1) (3, 7) (4, 1) (5, 6) (维持次序) b. (3, 7) (3, 1) (4, 1) (5, 6) (次序被改变)
冒泡排序(bubble sorting)
冒泡排序(bubble sorting):它重复地遍历要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。越小的元素会经由交换慢慢“浮”到数列的顶端。
冒泡排序算法的思路:
比较相邻的元素。如果第一个比第二个大(升序),就交换他们两个。
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
针对所有的元素重复以上的步骤,除了最后一个。
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
冒泡排序的分析
每次选出前面部分数列的最大值,把它逐渐换置到队尾。
需要进行n-1次冒泡过程,每次对应的比较次数如下图所示:
冒泡排序的实现
先写内层循环再写外层循环
# 先写内层循环再写外层循环。
# coding:utf-8
def bubble_sort(alist):
# list的index是(0~n-1),我们需要遍历的index范围为(0~n-2)
n = len(alist)
# 一共循环n-1次
for j in range(0, n-1): # O(n)
# 单次冒泡过程
for i in range(0, n-1-j): # O(n)
if alist[i] > alist[i+1]:
alist[i], alist[i+1] = alist[i+1], alist[i]
优化:一旦在某次循环中,发现元素没有发生交换,则说明全部的序列都是有序的,则break。
优化前后,最坏时间复杂度无变化,最优时间复杂度减少。# 先写内层循环再写外层循环。
# coding:utf-8
def bubble_sort(alist):
# list的index是(0~n-1),我们需要遍历的index范围为(0~n-2)
n = len(alist)
# 一共循环n-1次
for j in range(0, n-1): # O(n)
count = 0
# 单次冒泡过程
for i in range(0, n-1-j): # O(n)
if alist[i] > alist[i+1]:
alist[i], alist[i+1] = alist[i+1], alist[i]
count += 1
if count == 0:
break
时间复杂度
最优时间复杂度:
O
(
n
)
O(n)
O(n) (表示遍历一次发现没有任何可以交换的元素,排序结束。)
最坏时间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
稳定性:稳定
选择排序(selection sorting)
选择排序(selection sorting):在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
对数据的移动比较少,如果该元素位于正确的位置上,则它不会被移动。因此,选择排序最多对n个元素的列表进行n-1次交换。
选择排序算法的思路:
遍历全部无序的元素,找到最小/最大值,把它放到开头/结尾。
重复n-1次。
选择排序的分析
从后面无序序列中选出最小/最大值,然后把它与开头/结尾的元素互换。重复n-1次。
选择排序的实现
# coding:utf-8
def selection_sort(alist):
n = len(alist)
# 需要进行n-1次选择操作
for i in range(n-1):
# 记录最小位置
min_index = i
# 从i+1位置到末尾选择出最小数据
for j in range(i+1, n):
if alist[j] < alist[min_index]:
min_index = j
# 如果选择出的数据不在正确位置,进行交换
if min_index != i:
alist[i], alist[min_index] = alist[min_index], alist[i]
时间复杂度
最优时间复杂度:O(n2)
最坏时间复杂度:O(n2)
稳定性:不稳定(考虑升序每次选择最大的情况)
插入排序(insertion sorting)
插入排序(insertion sorting):从后面的无须数据中拿出数据,在前面的已排序序列中,从后向前扫描,找到相应位置并插入。在从后向前扫描的过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
与选择排序的区别:
选择排序:**操作的是无序的部分。**前面有序序列的位置固定,从后面的无序序列中选出相应的元素放过去。
插入排序:**操作的是有序的部分。**后面无序序列的元素是固定的,挨个拿出来,来测试出该元素在前面的有序序列中应该放在哪个位置上。
插入排序算法的思路:
把序列中的第一个元素看作为有序序列
把无序序列的第一个元素(此处为列表的第二个元素)放到有序列表中合适的位置。
插入排序的分析
alist = {93, 54, 77, 31, 44, 55, 226}
alist = {54, 93, 77, 31, 44, 55, 226} # 54比93小
alist = {54, 93, 77, 31, 44, 55, 226} # 空格前面为有序序列,后面为无序序列
alist = {54, 77, 93, 31, 44, 55, 226}
......
选择排序的实现
# coding:utf-8
def selection_sort(alist):
"""插入排序"""
n = len(alist)
# 从右边的无序序列中取出多少个元素执行这样的过程
for j in range(1, n):
# i = [1, 2, 3, ..., n-1]
# i代表内层循环的起始值
i = j
# 执行从右边的无序序列中去除第一个元素,即i位置的元素,然后将其插入到前面的正确位置中。
while i > 0
if alist[i] < alist[i-1]:
alist[i], alist[i-1] = alist[i-1], alist[i]
i -= 1
如果i位置的元素在正确的位置上,则break
else:
break
时间复杂度
最优时间复杂度:O(n) (升序排列,序列已经处于升序状态)
最坏时间复杂度:O(n2)
稳定性:稳定