1. 队列基础知识
1.1 队列简介
队列(Queue):简称队,一种线性表数据结构,是一种只允许在表的一端进行插入操作(称为【队尾】),而在表的另一端进行删除操作操作(称为【队头】);
队列有两种基本操作:「插入操作」 和 「删除操作」。
- 队列的插入操作又称为「入队」。
- 队列的删除操作又称为「出队」。
简单来说,队列是一种 「先进先出(First In First Out)」 的线性表,简称为 「FIFO 结构」。
队列的相关解释:
【线性表】:队列首先是一个线性表,队列中元素具有前驱后继的线性关系。队列中元素按照 a1,a2,…an的次序一次入队。队头元素为a1,队尾的元素an。
【先进先出原则】:根据队列的定义,最先进入队列的元素在队头,最后进入队列的元素在队尾。每次删除的总是队列中的队头元素,即最先进入队列的元素。也就是说,元素进入队列或者退出队列是按照「先进先出(First In First Out)」的原则进行的。
1.2 队列的顺序存储与链式存储
「顺序存储的队列」:利用一组地址连续的存储单元依次存放队列中从队头到队尾的元素,同时使用指针 front 指向队头元素在队列中的位置,使用指针 rear 指示队尾元素在队列中的位置。「链式存储的队列」:利用单链表的方式来实现队列。队列中元素按照插入顺序依次插入到链表的第一个节点之后,并使用队头指针 front 指向链表头节点位置,也就是队头元素,rear 指向链表尾部位置,也就是队尾元素。
1.2.1 队列的顺序存储实现
队列最简单的实现方式就是借助于一个数组来描述队列的顺序存储结构。在 Python 中我们可以借助列表 list 来实现。
为了算法设计上的方便以及算法本身的简单,我们约定:队头指针 self.front 指向队头元素所在位置的前一个位置,而队尾指针 self.rear 指向队尾元素所在位置。
- 初始化时:创建一个空队列self.queue,定义队列大小self.size。令队头指针self.front和队尾指针self.rear都指向-1。即使 self.front = self.rear = -1。
- 判断队列为空:根据 self.front 和 self.rear 的指向位置关系进行判断。如果对头指针 self.front 和队尾指针 self.rear 相等,则说明队列为空。
- 判断队列为满:如果 self.rear 指向队列最后一个位置,即 self.rear == self.size - 1,则说明队列已满。
- 获取队头元素:先判断队列是否为空,为空直接抛出异常。如果不为空,因为 self.front 指向队头元素所在位置的前一个位置,所以队头元素在 self.front 后面一个位置上,返回 self.queue[self.front + 1]。
- 获取队尾元素:先判断队列是否为空,为空直接抛出异常。如果不为空,因为 self.rear 指向队尾元素所在位置,所以直接返回 self.queue[self.rear]。
- 入队操作:判断队列是否已满,已满直接抛出异常。如果不满,则将队尾指针 self.rear 向右移动一位,并进行赋值操作。此时 self.rear 指向队尾元素。
- 出队操作:先判断队列是否为空,为空直接抛出异常。如果不为空,则将队头指针 self.front 指向元素赋值为 None,并将 self.front 向右移动一位。
实现代码
class Queue:
# 初始化空队列
def __init__(self, size=100):
self.size = size
self.queue = [None for _ in range(size)]
self.front = -1
self.rear = -1
# 判断队列是否为空
def is_empty(self):
return self.front == self.rear
# 判断队列是否已满
def is_full(self):
return self.rear + 1 == self.size
# 入队操作
def enqueue(self, value):
if self.is_full():
raise Exception('Queue is full')
else:
self.rear += 1
self.queue[self.rear] = value
# 出队操作
def dequeue(self):
if self.is_empty():
raise Exception('Queue is empty')
else:
self.front += 1
return self.queue[self.front]
# 获取队头元素
def front_value(self):
if self.is_empty():
raise Exception('Queue is empty')
else:
return self.queue[self.front + 1]
# 获取队尾元素
def rear_value(self):
if self.is_empty():
raise Exception('Queue is empty')
else:
return self.queue[self.rear]
1.2.2 循环队列的顺序存储实现
队列的顺序存储 会造成假溢出的问题,原因是:由于出队操作总是删除当前的队头元素,将 self.front 进行右移,而插入操作又总是在队尾进行。经过不断的出队、入队操作,队列的变化就像是使队列整体向右移动。当队尾指针 self.rear == self.size - 1 时,此时再进行入队操作就又抛出队列已满的异常。而之前因为出队操作而产生空余位置也没有利用上,这就造成了假溢出问题。
为了解决「假溢出」问题,有两种做法:
- 第一种:每一次删除队头元素之后,就将整个队列往前移动 1 个位置。其代码如下所示:
# 出队操作
def dequeue(self):
if self.is_empty():
raise Exception('Queue is empty')
else:
value = self.queue[0]
for i in range(self.rear):
self.queue[i] = self.queue[i + 1]
return value
这样可以不用头指针了,因为头指针总是在队列的第0个位置。但是每次出队,整个队的元素都会移动,使时间复杂度从O(1)—>O(n),这种方式代价有点大,不建议。
- 第二种:将队列想象成为头尾相连的循环表,利用数学中的求模运算,使得空间得以重复利用,这样就解决了问题。这样在进行插入操作时,如果队列的第 self.size - 1 个位置被占用之后,只要队列前面还有可用空间,新的元素加入队列时就可以从第 0 个位置开始继续插入。
假设: self.size 为循环队列的最大元素个数。队头指针 self.front 指向队头元素所在位置的前一个位置,而队尾指针 self.rear 指向队尾元素所在位置。则:
- 入队时,队尾指针循环前进 1 个位置,即 self.rear = (self.rear + 1) % self.size。
- 出队时,队头指针循环前进 1 个位置,即 self.front = (self.front + 1) % self.size。
注意:循环队列在一开始初始化,队列为空时,self.front 等于 self.rear。而当充满队列后,self.front 还是等于 self.rear。这种情况下就无法判断「队列为空」还是「队列为满」了。
解决**「队列为空」OR「队列已满」**的情况,有多种处理方式:
- 增加表示队列中元素个数的变量: self.count,用来以区分队列已满还是队列为空。在入队、出队过程中不断更新元素个数 self.count 的值。队列已满:self.count == self.size;队列为空:self.count == 0。
- 特意空出来一个位置用于区分队列已满还是队列为空入队时少用一个队列单元,即约定以「队头指针在队尾指针的下一位置」作为队满的标志。
队列已满条件为:队头指针在队尾指针的下一位置,即 (self.rear + 1) % self.size == self.front。
队列为空条件为:队头指针等于队尾指针,即 self.front == self.rear。
代码:
class Queue:
# 初始化空队列
def __init__(self, size=100):
self.size = size + 1
self.queue