Leetcode部分队列相关练习
一、队列
1. 队列的定义
队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为空队列。
队列的数据元素又称为队列元素。在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。因为队列只允许在一端插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出(FIFO—first in first out)线性表。
队列分为顺序存储和链式存储两种方式。
顺序存储就是用数组实现,比如有一个n个元素的队列,数组下标0的一端是队头,入队操作就是通过数组下标一个个顺序追加,不需要移动元素,但是如果删除队头元素,后面的元素就要往前移动,对应的时间复杂度就是O(n),性能自然不高。
为了提高出队的性能,就有了循环队列,什么是循环队列呢?就是有两个指针,front指向队头,rear指向对尾元素的下一个位置,元素出队时front往后移动,如果到了对尾则转到头部,同理入队时rear后移,如果到了对尾则转到头部,这样通过下标front出队时,就不需要移动元素了。
同时规定,当队列为空时,front和rear相等,那么队列什么时候判断为满呢?按照循环操作rear依次后移,然后再从头开始,也是出现rear和front相等时,队列满。这样跟队列空的情况就相同了,为了区分这种情况,规定数组还有一个空闲单元时,就表示队列已满,因为rear 可能在front后面,也可能循环到front前面,所以队列满的条件就变成了(rear+1)%maxsize = front ,同时队列元素个数的计算就是(rear -front+maxsize)%maxsize。
循环队列要事先申请好空间,整个过程都不能释放,而且要有固定的长度,如果长度事先无法估计,这种方式显然不够灵活;所以就引入了链式存储队列,其实就是线性表的单链表,只是它只能对尾进,队头出。并且规定队头指针指向链队列的头结点,对尾指针指向终端节点,当队列为空时,front和rear都指向头结点。
入队操作,就是在链表尾部插入结点;出队操作就是头结点的后继结点出队,然后将头结点的后继后移。如果最后除了头结点外,只剩一个元素了,就把rear也指向头结点。
2. 队列的实现
2.1 用数组实现一个顺序队列
class QueueUnderflow(ValueError):
pass
class SQueue():
def __init__(self, init_len = 8):
self._len = init_len # 存储区长度
self._elems = [0]*init_len # 元素存储
self._head = 0 # 表头元素下标
self._num = 0 # 元素个数
def is_empty(self): # 判断队列是否为空
return self._num == 0
def peek(self):
if self._num ==0:
raise QueueUnderflow
return self._elems[self._head]
def dequeue(self): # 删除队首元素
if self._num == 0:
raise QueueUnderflow
e = self._elems[self._head]
self._head = (self._head+1) % self._len
self._num -= 1
return e
def enqueue(self, e): # 在队尾插入元素
if self._num == self._len:
self.__extend()
self._elems[(self._head+self._num) % self._len] = e
self._num += 1
def __extend(self): # 扩展队列
old_len = self._len
self._len *=2
new_elems = [0]*self._len
for i in range(old_len):
new_elems[i] = self._elems[(self._head+1)%old_len]
self._elems, self._head = new_elems, 0
2.2 用链表实现一个链式队列
class Head(object):
def __init__(self):
self.left = None
self.right = None
class Node(object):
def __init__(self, value):
self.value = value
self.next = None
class Queue(object):
def __init__(self):
#初始化节点
self.head = Head()
def enqueue(self, value):
#插入一个元素
newnode = Node(value)
p = self.head
if p.right:
#如果head节点的右边不为None
#说明队列中已经有元素了
#就执行下列的操作
temp = p.right
p.right = newnode
temp.next = newnode
else:
#这说明队列为空,插入第一个元素
p.right = newnode
p.left = newnode
def dequeue(self):
#取出一个元素
p = self.head
if p.left and (p.left == p.right):
#说明队列中已经有元素
#但是这是最后一个元素
temp = p.left
p.left = p.right = None
return temp.value
elif p.left and (p.left != p.right):
#说明队列中有元素,而且不止一个
temp = p.left
p.left = temp.next
return temp.value
else:
#说明队列为空
#抛出查询错误
raise LookupError('queue is empty!')
def is_empty(self):
if self.head.left:
return False
else:
return True
def top(self):
#查询目前队列中最早入队的元素
if self.head.left:
return self.head.left.value
else:
raise LookupError('queue is empty!')
2.3 实现一个循环队列
class LoopQueue:
def __init__(self, capacity=10):
"""
构造函数
:param capacity: 循环队列的初始容量,默认为10。
"""
self._capacity = capacity + 1 # 对于用户来说,其容量为capacity。而对于内部实现来说,需要满足判空与判满的奇异性,
# 当self._tail + 1 = self._front是,此时判定为满,此时还剩余一个空间,所以真实容量是用户指定容量加一!
self._front = 0 # 队首的索引(闭区间)
self._tail = 0 # 队尾的索引(开区间,就像C++的.end()迭代器那样)self._front=self._tail=0,即初始化为空
self._data = [float('nan')] * self._capacity # 初始化为nan * self._capacity这么多容量
def isEmpty(self):
"""
判断循环队列是否为空
:return: bool值,空为True
"""
return self._front == self._tail # self._tail和self._tail相等时表示空。
def getCapacity(self):
"""
获取循环队列当前的容量
:return: 循环队列的容量
"""
return self._capacity - 1 # 对于用户来说得到的容量需要减一哦
def getSize(self):
"""
获得循环队列内有效元素的个数
:return: 有效元素的个数
"""
retSize = None # 要返回的size
if self._tail >= self._front: # self._tail在self._front后面(包括等于),就和普通队列一样
retSize = self._tail - self._front
else: # 此时self._front > self._tail
retSize = self._capacity - (self._front - self._tail) # 讲过啦,很简单
return retSize
def enqueue(self, elem):
"""
将元素elem入队
时间复杂度:O(1)
:param elem: 要入队的元素
"""
if (self._tail + 1) % self._capacity == self._front: # 满了
self._resize(self.getCapacity() * 2) # 扩大为getCapacity()的两倍
# 解释一下这里为什么不是self._capacity * 2。首先self._capacity和self.getCapacity()之间差一个1,
# 其次此时真实可容纳元素的空间是self.getCapacity(),扩大为它的二倍,也就是此时真实可容纳元素的空间
# 变为原先的两倍,self._capacity也容易维护,只需加一即可。
self._data[self._tail] = elem # 将self._tail的位置的元素置为elem
self._tail = (self._tail + 1) % self._capacity # 维护self._tail,注意是循环队列哦,要对全体空间取余的!
def dequeue(self):
"""
循环队列的出队操作
时间复杂度:O(1)
:return: 出队元素的值
"""
if self.isEmpty(): # 队列此时没有元素
raise Exception('Error.The loop queue is empty, can not make dequeue operation.') # 抛出异常
ret_val = self._data[self._front] # 记录一下队首的元素,方便返回
self._data[self._front] = None # 手动回收self._front处的元素
self._front = (self._front + 1) % self._capacity # 维护self._front,直接加一就好,注意循环队列的性质,要对
# 全体空间取余
if self.getSize() and self.getCapacity() // self.getSize() == 4: # 队列不为空且有效元素个数为可容纳元素的四分之一时,缩容
self._resize(self.getCapacity() // 2) # 缩容为原先的二分之一
return ret_val # 返回队首元素
def getFront(self):
"""
获取队首的元素的值(队列一般只关心队首)
:return: 队首元素的值
"""
if self.isEmpty(): # 空队列抛异常就完事了
raise Exception('Error. The loop queue is empty, can not get any mumber.')
return self._data[self._front] # 获得self._front索引处的元素
def printLoopQueue(self):
"""对循环队列内的有效元素进行打印操作"""
print('LoopQueue: Front--- ', end='') # 队首
index = self._front # 从队首开始
while index != self._tail: # 没到达队尾就一直打印
if index + 1 != self._tail: # 没到最后一次的打印。为了对称,强迫症。。
print(self._data[index], end=' ') # 打印当前元素
index = (index + 1) % self._capacity # index向后推进,注意是循环队列,到self._data的尾部就要返回到0索引处哦,
# 所以要对真实的存储空间取余,而不是对self.getCapacity()取余!这么做就错了!
else:
print(self._data[index], end=' ')
break # 最后一次打印操作,完事直接退出循环就好
print('---Tail') # 队尾
print('Size: %d, Capacity: %d' % (self.getSize(), self.getCapacity())) # 有效元素个数以及当前容量的打印
# private
def _resize(self, capacity):
"""
扩/缩容操作,将容量扩/缩至capacity(这里的capacity是面向用户,所以真正的self._capaciry应该是capacity+1,才能容纳capacity这么多元素呀)
:param capacity: 新的容量(基于用户的角度)
"""
# 此时千万不能先做self._capacity = capacity + 1 。因为一会儿要将当前队列中的元素全部取出来,一旦
# self._tail在self._front的前面,而self._capacity已经改变,就不能全部取出来了!好好想一下~以前就进过坑
tmp_list = [float('nan')] * (capacity + 1) # 建立一个新的list,真实容量为capacity+1,原因你懂得
index = self._front # 准备开始遍历原先的self._data,转移元素!从self._front开始
while index != self._tail: # 若index一直没到self._tail,就继续往后撸
tmp_list[index - self._front] = self._data[index] # 注意在这里我把原先的元素都放到新list的以索引零开始的地方顺序的放置元素
index = (index + 1) % self._capacity # index往后撸,注意循环队列的性质。
self._data = tmp_list # 更新self._data为新的那个数据,tmp_list会被自动垃圾回收的,不用担心它。
self._tail = self.getSize() # 维护self._tail。就是 0 + self.getSize()。因为转移元素并不改变size呀。
self._front = 0 # 维护self._front,因为我是从零开始放的,所以置零。
# 上面这两句话的顺序一定不能变!变了对扩容没有影响,但是缩容就会出错!虽然我们心里知道是从零开始放的,但是应该先安排self._tail。因为
# 一旦先把self._front置零,geiSize()方法瞬间出错!随后调用self.getSize()就出现问题了!导致self._tail出现在缩容后数组
# 索引的overflow位置,打印的话就会无线循环打印!因为self._front始终不能等于self._tail呀!所以一定要先安排self._tail!
self._capacity = capacity + 1 # 最后再维护self._capacity!
3. 练习
641. Design Circular Deque(设计一个双端队列)
[https://leetcode-cn.com/problems/design-circular-deque/]
题目描述
设计实现双端队列。
你的实现需要支持以下操作:
MyCircularDeque(k):构造函数,双端队列的大小为k。
insertFront():将一个元素添加到双端队列头部。 如果操作成功返回 true。
insertLast():将一个元素添加到双端队列尾部。如果操作成功返回 true。
deleteFront():从双端队列头部删除一个元素。 如果操作成功返回 true。
deleteLast():从双端队列尾部删除一个元素。如果操作成功返回 true。
getFront():从双端队列头部获得一个元素。如果双端队列为空,返回 -1。
getRear():获得双端队列的最后一个元素。 如果双端队列为空,返回 -1。
isEmpty():检查双端队列是否为空。
isFull():检查双端队列是否满了。
示例:
MyCircularDeque circularDeque = new MycircularDeque(3); // 设置容量大小为3
circularDeque.insertLast(1); // 返回 true
circularDeque.insertLast(2); // 返回 true
circularDeque.insertFront(3); // 返回 true
circularDeque.insertFront(4); // 已经满了,返回 false
circularDeque.getRear(); // 返回 2
circularDeque.isFull(); // 返回 true
circularDeque.deleteLast(); // 返回 true
circularDeque.insertFront(4); // 返回 true
circularDeque.getFront(); // 返回 4
提示:
所有值的范围为 [1, 1000]
操作次数的范围为 [1, 1000]
请不要使用内置的双端队列库。
代码实现
class MyCircularDeque:
def __init__(self, k: int):
"""
Initialize your data structure here. Set the size of the deque to be k.
"""
self.head, self.tail = -1, -1
self.vec = [None for _ in range(k)]
def insertFront(self, value: int) -> bool:
"""
Adds an item at the front of Deque. Return true if the operation is successful.
"""
if self.isFull(): return False
self.head -= 1
if self.head < 0:
self.head = len(self.vec)-1
self.vec[self.head] = value
return True
def insertLast(self, value: int) -> bool:
"""
Adds an item at the rear of Deque. Return true if the operation is successful.
"""
if self.isFull():
return False
self.tail += 1
if self.tail == len(self.vec):
self.tail = 0
self.vec[self.tail] = value
return True
def deleteFront(self) -> bool:
"""
Deletes an item from the front of Deque. Return true if the operation is successful.
"""
if self.isEmpty():
return False
self.vec[self.head] = None
self.head += 1
return True
def deleteLast(self) -> bool:
"""
Deletes an item from the rear of Deque. Return true if the operation is successful.
"""
if self.isEmpty(): return False
self.vec[self.tail] = None
self.tail -= 1
return True
def getFront(self) -> int:
"""
Get the front item from the deque.
"""
print(self.head, len(self.vec))
return self.vec[self.head]
def getRear(self) -> int:
"""
Get the last item from the deque.
"""
return self.vec[self.tail]
def isEmpty(self) -> bool:
"""
Checks whether the circular deque is empty or not.
"""
for x in self.vec:
if x != None:
return False
return True
def isFull(self) -> bool:
"""
Checks whether the circular deque is full or not.
"""
for x in self.vec:
if x == None:
return False
return True
# Your MyCircularDeque object will be instantiated and called as such:
# obj = MyCircularDeque(k)
# param_1 = obj.insertFront(value)
# param_2 = obj.insertLast(value)
# param_3 = obj.deleteFront()
# param_4 = obj.deleteLast()
# param_5 = obj.getFront()
# param_6 = obj.getRear()
# param_7 = obj.isEmpty()
# param_8 = obj.isFull()
239. 滑动窗口最大值
[https://leetcode-cn.com/problems/sliding-window-maximum/]
题目描述
给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口 k 内的数字。滑动窗口每次只向右移动一位。
返回滑动窗口最大值。
示例:
输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
注意:
你可以假设 k 总是有效的,1 ≤ k ≤ 输入数组的大小,且输入数组不为空。
线性时间复杂度
代码实现
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
# solution1 每次用大顶堆取最大,注意heapq没有实现大顶堆,要用取反的方式来完成大顶堆功能
# 代码参考为gymer的评论
# 同时要注意,不能使用remove来去除滑出元素(remove为O(n)),使用一个字典记住返回的最大数字的次数来规避这个问题
# 滑动窗口初始化时为k,每次将滑动窗口加大1,对窗口内的数字做堆排序,获取(不是取出)其中最大元素,如果字典中该元素计数值大于0,表示这个元素应该是一个不属于当前k滑动窗口的数字且之后加入堆的数字都小于该数字(因为没有减少滑动窗口),此时应将改数字从堆删除并再取出一个新的堆顶,重复此操作直到取出的数字在字典中对应出现次数为0,表示为当前k滑动窗口真实最大值,取出真实最大值之后要把应滑出k范围窗口的数值在计数器中+1,以便后期取到该值时进行抑制。
# 这个方法的时间复杂度会比其他语言直接使用大顶堆稍慢,因为大顶堆排序时元素个数大于k,最坏情况可达n,所以复杂度为O(nlogn),其他语言直接用大顶堆为O(nlogk),使用remove的话复杂度为O(n^2)
# 空间复杂度也比其他语言大,为O(n),其他语言为O(1)
# import heapq
# from collections import defaultdict
# if not nums:
# return []
# nums = [-1 * x for x in nums]
# counter = defaultdict(int)
# windows = nums[:k]
# res = []
# heapq.heapify(windows)
# res.append(-1 * windows[0])
# counter[nums[0]] += 1
# for i in range(len(nums) - k):
# heapq.heappush(windows, nums[i + k])
# min_num = windows[0]
# while counter[min_num] > 0:
# counter[min_num] -= 1
# heapq.heappop(windows)
# min_num = windows[0]
# res.append(-1 * min_num)
# counter[nums[i + 1]] += 1
# return res
# solution2 使用双端队列,双端队列的作用是过滤那些,存在于一个滑动窗口中,被夹在两个大于它的数中间的数字,如3,1,2中的1,双端队列的维护规则是,在保证对头与队尾长度差不超过一个滑动窗口k的情况下,降序排列队列中的元素,对小于新加元素的对中元素进行pop操作,即将不可能进结果的数字pop掉
# 时间复杂度为O(n*1),其中1的原因是因为每个数字仅会在双端队列中存在一次
total = len(nums)
re = []
if total==0:
return re
dequeue = []
for i,x in enumerate(nums):
if i>=k and i-dequeue[0]>=k:#表示超出k滑动窗口,需要从前pop一个元素
dequeue.pop(0)
while dequeue and x>=nums[dequeue[-1]]:#将该元素加到队尾,并保证整个双端队列降序,如存在比该元素小的数字存在于队列中,将其删除
dequeue.pop()
dequeue.append(i)
if i>=k-1:
re.append(nums[dequeue[0]])
return re
# 超过时间复杂度O(n)
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
if not nums:
return []
res = []
for i in range(len(nums)-k+1):
window = nums[i:i+k]
m_num = max(window)
res.append(m_num)
return res