1.队列的基础定义
官方定义:队列是只允许在一端进行插入操作,而另一端进行删除操作的线性表。
解释:队列也是一种线性结构,相比数组,队列对应的操作是数组的子集,只能从一端(队尾)添加元素,只能从另一端(队首)取出元素。
队列是一种先进先出(First in First out,FIFO)的数据结构。
2.队列的实现
基本需求:
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("my")
q.enqueue("friend")
print(q.size())
print(q.items)
print(q.dequeue())
print(q.items)
print(q.dequeue())
print(q.items)
print(q.dequeue())
print(q.items)
3
['friend', 'my', 'hello']
hello
['friend', 'my']
my
['friend']
friend
[]
应该ok了。
3.数组队列的问题
我们来看如果删除队首元素的话,队列是怎么实现的。假设下列例子中,队首是左侧,队尾是右侧。
当我们删除队首元素时,a即会出队。然后bcde依次向前挪一个位置,时间复杂度是O(n)。
但是如果a出队后,剩余元素不去挪动位置,毕竟bcde还是保持我们队列中的样子(b是队首,e是队尾)。
所以,我们可以在索引为1的位置记录为队首(front),队尾(tail)其实就是原来的size,记录下一次元素入队的位置。这样的好处是,下一次删除队首元素的操作,只需要维护front即可,不需要所有元素都向前移动一个单位(详细见下个部分‘循环队列’)。
4.循环队列
当我们队列为空时(即没有元素在队列里),front和tail指的都是索引为0的位置,照理说front指的是第一个元素,而tail指的是最后一个元素的后一个位置。所以,当front == tail时,意味着队列为空。
当有一个元素a入队了,我们只需要维护tail即可,把tail挪到索引为1的位置,指向下一次元素入队的位置。
依此类推,当我们入队5个元素后,tail需要维护,即把它挪到索引为5的位置。
这时我们如果要把队首元素a出队,只需要维护front即可,把front向后移动一个单位。这样的时间复杂度是O(1)。
到现在来看,好像跟循环队列没什么关系啊?
我们来看这个情况,此时加了新元素h到索引为7的位置,tail无法维护了,而且我们注意到前面空出来的位置是没办法被后面元素利用到的。因此,前面还有利用的空间,所以我们可以把队列看成一个‘环状’首尾相接的形状。
那么我们怎么实现位置7之后是位置0的呢?可以想一下,7+1=8,8/8取余是0,则我们把tail维护到索引为0的位置。
此时又进来一个新元素i,则tail再+1即可。
这时,我们看到还有一个空位置在索引1上。如果我们此时再引入一个新元素,tail接下来就要挪到索引2上,跟front是同一个位置了,可此时我们队列不为空(recall:front == tail意味着队列为空)。所以,我们会定义tail + 1 == front为队列列满了(我们是有意识地浪费掉一个空间)。严谨来说,我们是**(tail + 1)% c == front为队列列满**了(c是队列的长度),因为当front指的是索引0,而tail指的是索引7的时候,也是满的情况。
循环队列的实现如下:
class LoopQueue(object):
def __init__(self, n=2): #先默认capacity为2,方便后面测试
self.arr = [None] * (n+1) # 由于特意浪费了一个空间,所以arr的实际大小应该是用户传入的容量+1
self.front = 0
self.tail = 0
self.size = 0
def __str__(self):
return str(self.arr)
def __len__(self):
return len(self.arr)
def __iter__(self):
return iter(self.arr)
def get_size(self):
# 获取队列元素个数
return self.size
def get_capaticty(self):
# 获取队列容积(实际可存储元素个数)
return self.__len__() - 1
def is_full(self):
# 判断队列是否为满
return (self.tail+1) % len(self.arr) == self.front
def is_empty(self):
# 判断队列是否为空
return self.size == 0
def get_front(self):
# 获取队首
return self.arr[self.front]
def enqueue(self, e):
# 入队
if self.is_full():
self.resize(self.get_capaticty() * 2) # 如果队列满,以当前队列容积的2倍进行扩容
self.arr[self.tail] = e
self.tail = (self.tail+1) % len(self.arr)
self.size += 1
def dequeue(self):
# 出队
if self.is_empty():
raise Exception("Cannot dequeue from en empty queue")
result = self.arr[self.front]
self.arr[self.front] = None
self.front = (self.front+1) % len(self.arr)
self.size -= 1
# 如果元素个数少于容积的1/4并且元素个数
if self.size < self.get_capaticty() // 4 and self.get_capaticty() > 1:
self.resize(self.get_capaticty() // 2)
return result
def resize(self, new_capacity):
new_arr = [None] * (new_capacity+1)
for i in range(self.size):
new_arr[i] = self.arr[(i+self.front) % len(self.arr)]
self.arr = new_arr
self.front = 0
self.tail = self.size
注意此时enqueue和dequeue的均摊复杂度都是O(1)。
检验一下,
if __name__ == '__main__':
loop_queue = LoopQueue()
loop_queue.enqueue("hello")
loop_queue.enqueue("my")
print(loop_queue.arr)
loop_queue.enqueue("friend")
print(loop_queue.get_size())
print(loop_queue.arr)
print(loop_queue.dequeue())
print(loop_queue.arr)
print(loop_queue.dequeue())
print(loop_queue.arr)
loop_queue.enqueue("hello")
print(loop_queue.arr)
loop_queue.enqueue("my")
print(loop_queue.arr)
loop_queue.enqueue("love")
print(loop_queue.arr)
print(loop_queue.dequeue())
print(loop_queue.dequeue())
print(loop_queue.dequeue())
print(loop_queue.arr)
print(loop_queue.dequeue())
print(loop_queue.arr)
['hello', 'my', None]
3
['hello', 'my', 'friend', None, None]
hello
[None, 'my', 'friend', None, None]
my
[None, None, 'friend', None, None]
[None, None, 'friend', 'hello', None]
[None, None, 'friend', 'hello', 'my']
['love', None, 'friend', 'hello', 'my']
friend
hello
my
['love', None, None, None, None]
love
[None, None, None]
我们在延申一下,双端队列(double-ended queue, deque)是一种具有队列和栈的性质的数据结构。双端队列中的元素可以从两端弹出,其限定插入和删除操作在表的两端进行。双端队列可以在队列任意一端入队和出队。
我们看看实现双端队列的需求:
具体实现:
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)
print(deque.items)
deque.add_rear(3)
deque.add_rear(4)
print(deque.items)
print(deque.size())
print(deque.remove_front())
print(deque.remove_front())
print(deque.remove_rear())
print(deque.remove_rear())
[2, 1]
[2, 1, 3, 4]
4
2
1
4
3
5.队列的应用
5.1回文词检测
有两种思路,一种思路是因为是对称的,我们可以判断正序和逆序遍历的结果是否一样,当然这种方法只需要两边循环一下就可以了;另一种思路是从两段同时读取,判断是否一样,一样的话就继续读取,直到遍历完了或还剩最中间那个元素。后者采用的思想就是前面双端队列的原理。
def palchecker(aString):
deque = Deque()
# 回文词入队
for i in aString:
deque.add_front(i)
state = True
# 双向读取并比较
while deque.size() > 1 and state:
df = deque.remove_front()
dr = deque.remove_rear()
# print(df,dr)
if df != dr:
state = False
return state
检验一下呗:
palchecker('abaa')
>>False
5.2用栈实现队列(232)
Implement a first in first out (FIFO) queue using only two stacks. The implemented queue should support all the functions of a normal queue (push
, peek
, pop
, and empty
).
Implement the MyQueue
class:
void push(int x)
Pushes element x to the back of the queue.int pop()
Removes the element from the front of the queue and returns
it.int peek()
Returns the element at the front of the queue.boolean empty()
Returnstrue
if the queue is empty,false
otherwise.
Notes:
- You must use only standard operations of a stack, which means only
push to top
,peek/pop from top
,size
, andis empty
operations are valid. - Depending on your language, the stack may not be supported natively. You may simulate a stack using a list or deque (double-ended queue) as long as you use only a stack’s standard operations.
先思考一下,队列是先入先出的,而栈是后入先出的,如果要用栈实现队列的效果,需要两个栈来实现。
第一种思想:在一个栈中维持所有元素的出队顺序。具体来说,所有的元素在入队操作完成后只会保存在一个栈中,且其出栈的顺序和出队的顺序是一致的。
#leetcode 232
class MyQueue:
def __init__(self):
"""
Initialize your data structure here.
"""
self._s1, self._s2 = [], []
def push(self, x):
"""
Push element x to the back of queue.
:type x: int
:rtype: void
"""
while self._s1:
self._s2.append(self._s1.pop())
self._s1.append(x)
while self._s2:
self._s1.append(self._s2.pop())
def pop(self):
"""
Removes the element from in front of queue and returns that element.
:rtype: int
"""
return self._s1.pop()
def peek(self):
"""
Get the front element.
:rtype: int
"""
return self._s1[-1]
def empty(self):
"""
Returns whether the queue is empty.
:rtype: bool
"""
return not self._s1
第二种思想:将两个栈一个用于入队,一个用于出队。假设栈inStack 用于实现入队操作,栈 outStack 用于实现出队操作。
#leetcode 232 方法2
class MyQueue:
def __init__(self):
"""
Initialize your data structure here.
"""
self._in_stack, self._out_stack, self._front = [], [], None
def push(self, x):
"""
Push element x to the back of queue.
:type x: int
:rtype: void
"""
if not self._in_stack:
self._front = x
self._in_stack.append(x)
def pop(self):
"""
Removes the element from in front of queue and returns that element.
:rtype: int
"""
if self.empty():
rasie Exception("[ERROR] The queue is empty!")
if not self._out_stack:
while self._in_stack:
self._out_stack.append(self._in_stack.pop())
return self._out_stack.pop()
def peek(self):
"""
Get the front element.
:rtype: int
"""
if self.empty():
rasie.Exception("[ERROR] The queue is empty!")
if not self._out_stack:
return self_front
else:
return self._out_stack[-1]
def empty(self):
"""
Returns whether the queue is empty.
:rtype: bool
"""
return not self._in_stack and not self._out_stack
5.3生产窗口最大值数组(程序员面试指南)
这题似曾相识,发现跟之前数组Leetcode总结的固定滑动窗口差不多。但我们这里用队列的思想来思考(主要难点是怎么用双向队列实现最大值的更新)。
现有一个双向队列名为qmax。
当我们遍历到数组的第i个元素的时候,有如下规则:
如果qmax为空,直接插入i。
如果qmax不为空,则获取qmax队尾的坐标,记为j。
如果a[j]>a[i],说明队尾的值比他数组大,则直接插入队列。
如果a[j]<=a[i],需要把j从qmax弹出,然后会到第二步,再决定插入的策略。
#生成窗口最大数组
def getMaxWindow(arr, w):
if arr == None or w < 1 or len(arr) < w:
return
qmax = []
res = []
for i in range(len(arr)):
while qmax and arr[qmax[-1]] <= arr[i]:
qmax.pop()
qmax.append(i)
if qmax[0] <= i - w:
qmax.pop(0)
if i-w+1 >= 0:
res.append(arr[qmax[0]])
return res