什么是队列
先进者先出,就是典型的”队列“。支持的操作也很有限,最基本的操作是:入队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)。
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])