队列(实现基于python)

199 阅读5分钟

什么是队列

先进者先出,就是典型的”队列“。支持的操作也很有限,最基本的操作是:入队enqueue(),放一个数据到队列尾部;出队dequeue(),从队列头部取一个元素。所以,队列和栈一样,也是一种操作受限的线性表数据结构。

如何实现队列

队列可以用数组来实现,也可以用链表来实现。用数组实现的队列叫做顺序队列,用链表实现的队列叫作链式队列。

基于数组实现

队列需要两个指针:一个是head指针,指向队头;一个是tail指针,指向队尾。入队tail指针后移;出队head指针后移。

class Queue:
	def __init__(self, n):
		# 数组大小:n
		self.n = n
		self.queue = [None]*n
		# head表示队头下标,tail表示队尾下标
		self.head = 0
		self.tail = 0
		
	def enqueue(self, item):
		# tail == n 表示队列已经满了
		if self.tail == self.n: return False
		self.queue[self.tail] = item
		self.tail += 1
		return True

	def dequeue(self):
		# head == tail 表示队列为空
		if self.head == self.tail: return NULL
		tmp = self.queue[self.head]
		self.head += 1
		return tmp

但是随着不停地进行入队、出队操作,head和tail都会持续往后移动。当tail移到最右边,即使数组中还有空闲空间,也无法继续往队列中添加数据了。

我们可以用数据搬移来解决这个问题。但每次进行出队操作在删除完数组中下标为0的数据后都要搬移整个队列的数据,出队操作时间复杂度就会变成O(n)。那如何优化呢?

实际上,我们在出队时无需搬移数据。只有在没有空闲空间时,在入队操作时触发一次数据的搬移操作。

def enqueue(self, item):
	# tail == n 表示队列末尾没有空间了
	if self.tail == self.n: 
		# tail == n and head == 0 表示整个队列都占满了
		if self.head == 0:
			return False
		# 数据搬移
		for i in range(self.head, self.tail):
			self.queue[i-self.head] = self.queue[i]
		# 搬移完成后重新更新head 和 tail
		self.tail -= self.head
		self.head = 0
	self.queue[self.tail] = item
	self.tail += 1
	return True

基于链表实现

基于链表的实现,我们同样需要两个指针:head指针和tail指针。它们分别指向链表的第一个结点和最后一个结点。入队时:tail->next=new_node,tail = tail->next;出队时:head = head->next。

class Linked_Node:
	def __init__(self, val):
		self.val = val
		self.next = None

class Linked_Queue:
	def __init__(self, length):
		# length: 队列设置的长度。 size:队列实际的长度
		self.length = length
		self.size = 0
		self.head = None
		self.tail = None

	def enqueue(self, Node):
		# head,tail初始值指向第一个入队节点
		if self.head is None:
			self.head = Node
			self.tail = self.head
		# 实际长度小于队列长度,可以入队
		if self.size < self.length:
			self.tail.next = Node
			self.tail = self.tail.next
			self.size += 1
			print(self.size, self.length)
			return True
		else:
			return False

	def dequeue(self):
		# size为0,队列为空
		if self.size == 0:
			return False
		else:
			self.head = self.head.next
			self.size -= 1

循环队列

刚刚使用数组来实现队列的时候,在tail==n时候会出现数据搬移操作,这样入队操作性能会受到影响。那如何避免数据搬移呢?可以试试循环队列。循环队列,顾名思义,它长的像一个环。只要环里还有空位,即可按顺序入队。

循环队列的重点是确定好队空和队满的判定条件。队空的判断条件依旧是head == tail,但队满的判断条件会稍微有点复杂。总结一下就是(tail+1)%n=head,这个可以自己画图理解一下。这个判断方式tail指向的位置实际上是没有存储数据的。所以,循环队列会浪费一个数组的存储空间。也可以新定义一个size记录当前队列实际长度。

class Queue:
	def __init__(self, n):
		# 数组大小:n
		self.n = n
		self.queue = [None]*n
		# head表示队头下标,tail表示队尾下标
		self.head = 0
		self.tail = 0
		
	# 入队
	def enqueue(self, item):
		# 队列满了
		if (self.tail+1)%n == self.head: return False
		self.queue[self.tail] = item
		self.tail = (self.tail+1)%n
		return True

	# 出队
	def dequeue(self):
		# head == tail 表示队列为空
		if self.head == self.tail: return False
		tmp = self.queue[self.head]
		self.head = (self.head + 1)%n
		return tmp

队列的应用

阻塞队列

阻塞队列其实就是在队列的基础上增加了阻塞操作。简单来说,就是队列为空时,从队头取数据会被阻塞,因为此时还没有数据可取,直到队列中有了数据才能返回;如果队列已经满了,那么插入操作就会被阻塞,直到队列中有空闲位置后再插入数据,然后再返回。

并发队列

线程安全的队列我们叫作并发队列。最简单直接的实现方式是直接在enqueue()、dequeue()方法上加锁,但是锁粒度大并发度会比较低,同一时刻仅允许一个存或者取操作。

python队列用法

基本FIFO队列

FIFO即First in First Out,先进先出。queue提供了一个基本的FIFO容器,使用方法很简单,maxsize是个整数,指明了队列中能存放的数据个数的上限。一旦达到上限,插入会导致阻塞,直到队列中的数据被消费掉。如果maxsize小于或者等于0,队列大小没有限制。

import queue

# FIFO队列
# q = queue.Queue(maxsize = 5)
# LIFO队列
q = queue.LifoQueue(maxsize = 5)

for i in range(5):
    #将item放入队列中
	q.put(i)

#empty():如果队列为空,返回True,反之返回False
while not q.empty():
    #从队列中移除并返回一个数据
	print(q.get())

基本LIFO队列

LIFO即Last in First Out,后进先出。只需将上述queue.Queue类替换为queue.LifoQueue类即可。

优先级队列

按设置的优先级(由小到大)出队。

import queue

class Skill(object):
	def __init__(self, priority, description):
		self.priority = priority
		self.description = description
		
    #基类object提供了一系列可以用于实现同类对象进行“比较”的方法,可以用于同类对象的不同实例进行比较,包括`__lt__、__gt__、__le__、__ge__、__eq__和__ne__`六个方法。
	def __lt__(self, other):
		return self.priority < other.priority

    #返回一个对象的描述信息
	def __str__(self):
		return '(' + str(self.priority)+',\'' + self.description + '\')'

def PriorityQueue_class():
	q = queue.PriorityQueue()
	q.put(Skill(7, "proficient7"))
	q.put(Skill(5, "proficient5"))
	q.put(Skill(6, "proficient6"))
	q.put(Skill(10, "expert"))
	q.put(Skill(1, "novice"))

	while not q.empty():
		print(q.get())

PriorityQueue_class()

输出:

(1,'novice')
(5,'proficient5')
(6,'proficient6')
(7,'proficient7')
(10,'expert')

双向队列

双向队列操作很像list,但相比于list实现的队列,deque实现拥有更低的时间和空间复杂度。list实现在出队(pop)和插入(insert)时的空间复杂度大约为O(n),deque在出队(pop)和入队(append)时的时间复杂度是O(1)。

1959611-20200307215621460-1733987446.png

import collections

q = collections.deque()

for i in range(5):
	q.append(i)
	q.appendleft(i-1)

print(q)
print(q.popleft())
print(q)
print(q.pop())
print(q)

输出
deque([3, 2, 1, 0, -1, 0, 1, 2, 3, 4])
3
deque([2, 1, 0, -1, 0, 1, 2, 3, 4])
4
deque([2, 1, 0, -1, 0, 1, 2, 3])