队列是一种常见的线性表数据结构,它的典型特征是先进先出。即先入队列的先出队,后入队列的后出队。
队列有两个基本操作:入队(enqueue),即将一个数据放入到队列的尾部,出队(dequeque),即将一个数据从对头移除。

队列通常应用在资源有限的场景下,比如线程池、数据库连接池等。当线程池没有空闲线程时,新的任务请求线程资源时,将请求排队,等到有空闲线程时,取出排队的请求继续处理。

队列根据其中容纳的数据个数,可以分为无界队列(unbounded queue)和有界队列(bounded queue)。无界队列中可以存放任意多个数据,有界队列中可以放得数据个数是有限的。队列大小要设置的合理,队列太大导致等待的请求太多,队列太小会导致无法充分利用系统资源。

队列的数据结构可以用下图表示

android 有界队列 有界队列与无界队列_数据


队列可以用数组实现,也可以用链表实现。用数组实现的队列是顺序队列,用链表实现的队列是链式队列。

1.用Python列表实现队列

下面的代码是用Python的列表实现的队列。

class ArrayQueue:
    """
    用Python列表实现,有限长度的队列,capacity是队列长度。
    """
    def __init__(self, capacity: int):
        self._items = []
        self._capacity = capacity
        self._head = 0
        self._tail = 0

    def enqueue(self, data):
        if self._tail == self._capacity-1:  # tail到达最后面了
            if self._head == 0:  # 没有空间了
                return False
            else:  # 还有空间
                for i in range(0, self._tail - self._head):  
                    self._items[i] = self._items[i + self._head]  # 数据搬移
                self._tail = self._tail - self._head
                self._head = 0
        self._items.insert(self._tail, data)  # 插入数据
        self._tail += 1  # 移动tail指针
        return True

    def dequeue(self):
        if self._head != self._tail:  # 队列不为空
            item = self._items[self._head]
            self._head += 1
            return item
        else:
            return None

    def __repr__(self):
        return str(self._items[self._head: self._tail])

if __name__ == '__main__':
    aq = ArrayQueue(5)
    aq.enqueue("1")
    aq.enqueue("2")
    aq.enqueue("3")
    aq.dequeue()
    aq.enqueue("4")
    aq.enqueue("5")
    print(aq)
    aq.dequeue()
    print(aq)
    aq.dequeue()
    print(aq)
    aq.enqueue("6")
    aq.enqueue("7")
    aq.enqueue("8")
    aq.enqueue("9")
    print(aq)

2.深入理解队列的内部原理

下面我们看看队列的入队和出队的工作原理。

android 有界队列 有界队列与无界队列_数据_02


对队列的操作,需要两个指针,一个head指向队头,一个tail指向队尾。当入队的时候,tail指针往后移动一位,当出队时,head指针往后移动一位。随着不断地入队和出队,head指针和tail指针持续后移。对于有界队列,当tail移动到最右边,即使队列中还有空闲空间,此时也不能入队了。当队列tail到达最右边后,此时可以触发一次数据的搬移,将head和tail之间的数据都往左搬移到0~tail-head之间,再重置head和tail指针的指向。

android 有界队列 有界队列与无界队列_出队_03


下面代码描述了tail到达队尾时,入队操作前,进行数据搬移的过程可以参考上面一节入队的部分代码。

3. 用链表实现队列

用链表实现队列,也需要维护两个指针,一个是head指针指,一个是tail指针。head指针指向队列的队头元素,tail指针指向队列的队尾元素。

对于入队操作,需要先将新结点接到队尾(tail.next=new_node),再将队尾指针(tail=new_node)指向新的结点。

对于出队操作,返回队头结点的数据(head.data),再将队头指针往后移动一位(head=head.next)。

对于入队和出队操作,要考虑到队为空的情况。

下面是代码实现:

class Node:
    """链表的Node结点"""

    def __init__(self, data: int, next_node=None):
        """
        :param data: 存储的数据
        :param next_node: 下一个Node节点的地址
        """
        self.data = data
        self.next = next_node


class LinkedListQueue:
    """
    https://www.geeksforgeeks.org/queue-linked-list-implementation/
    """

    def __init__(self):
        self._head = self._tail = None  # 队头指针,队尾指针初始化为None

    def enqueue(self, value):
        new_node = Node(value)
        if self._tail:  # 队不是空
            self._tail.next = new_node  # 队尾元素的next指向新结点
        else:  # 队是空
            self._head = new_node  # 队头指向新结点
        self._tail = new_node  # 移动尾指针

    def dequeue(self):
        if self._head:  # 队不为空
            value = self._head.data
            self._head = self._head.next
            if not self._head:
                self._tail = None
            return value

    def __repr__(self):
        values = []
        p = self._head
        if not p:
            return "->"
        while p:
            values.append(p.data)
            p = p.next
        return "->".join(value for value in values)

if __name__ == '__main__':
    aq = LinkedListQueue()
    aq.enqueue("1")
    aq.enqueue("2")
    aq.enqueue("3")
    aq.dequeue()
    aq.enqueue("4")
    aq.enqueue("5")
    print(aq)
    aq.dequeue()
    print(aq)
    aq.dequeue()
    print(aq)
    aq.enqueue("6")
    aq.enqueue("7")
    aq.enqueue("8")
    aq.enqueue("9")
    print(aq)

4. 循环队列

用数组来实现队列的时候,在 tail==n 时,会有数据搬移操作,这样入队操作性能就会受到影响。那有没有办法能够避免数据搬移呢?循环队列应用而生。

循环队列长成下面的这个样子。

android 有界队列 有界队列与无界队列_出队_04


图中这个队列的大小为 8,当前 head=4,tail=7。如果这时,依次将在 a,b 入队,循环队列中的元素就变成了下面的样子:

android 有界队列 有界队列与无界队列_android 有界队列_05


可见,tail指针是在环上移动。当tail到达size-1的下标时,tail指针移动到下标为0的位置。通过这样的方法,我们成功避免了数据搬移操作。

在用数组实现的非循环队列中,队满的判断条件是 tail==capacity-1 并且head=0, 队空的判断条件时head=tail。那针对循环队列,如何判断队空和队满呢?

下面是一张队满的图。

android 有界队列 有界队列与无界队列_数据_06


可以看到队满的时候,(tail+1)%size=head。而队空的判断条件,肯容易想到是head = tail = None。

class CircularQueue:

    def __init__(self, capacity):
        self._capacity = capacity + 1
        self._items = [None] * self._capacity  # 用None占位
        self._head = 0
        self._tail = 0

    def enqueue(self, item: str) -> bool:
        if (self._tail + 1) % self._capacity == self._head:
            return False
        self._items[self._tail] = item
        self._tail = (self._tail + 1) % self._capacity
        return True

    def dequeue(self) -> Optional[str]:
        if self._head != self._tail:
            item = self._items[self._head]
            self._head = (self._head + 1) % self._capacity
            return item

    def __repr__(self) -> str:
        if self._tail >= self._head:
            return " ".join(item for item in self._items[self._head: self._tail])
        else:
            return " ".join(item for item in chain(self._items[self._head:], self._items[:self._tail]))


if __name__ == '__main__':
    aq = CircularQueue1(5)
    aq.enqueue("1")
    aq.enqueue("2")
    aq.enqueue("3")
    p = aq.dequeue()
    print(aq)
    aq.enqueue("4")
    aq.enqueue("5")
    print(aq)
    aq.dequeue()
    print(aq)
    aq.dequeue()
    print(aq)
    aq.enqueue("6")
    aq.enqueue("7")
    aq.enqueue("8")
    aq.enqueue("9")
    print(aq)

5.Python中的queue模块

平时的业务开发不大可能从零实现一个队列,而是使用编程语言中线程的队列库来解决实际问题。
Python提供了现成的模块queue,这个模块提供了三种常用的队列,分别是:

  • 先进先出(FIFO)队列Queue
  • 后进先出(LIFO)队列LifoQueue
  • 优先级队列PriorityQueue

另外,collections模块提供了双端队列deque。可以在队列两端进行入队和出队操作。

下面,我们简单看一下这四种队列的用法。

5.1 先进先出(FIFO)队列Queue
import queue

q=queue.Queue(5)
q.put(1)
q.put(2)
q.put(3)
q.put(4)
q.put(5)
# q.put(6)  # 将会阻塞住
print(q.maxsize)
print(q.get())
print(q.get())
print(q.get())
print(q.get())
print(q.get())
# print(q.get())  # 将会阻塞线程

这里的put方法和get方法,是阻塞操作,简单来说,如果队列已经满了,那么插入数据的操作就会被阻塞,直到队列中有空闲位置后再插入数据,然后再返回。在队列为空的时候,从队头取数据会被阻塞,因为此时还没有数据可取,直到队列中有了数据才能返回。

常见的阻塞队列的应用场景是“生产者 - 消费者模型”。阻塞队列可以有效地协调生产和消费的速度。当“生产者”生产数据的速度过快,“消费者”来不及消费时,存储数据的队列很快就会满了。这个时候,生产者就阻塞等待,直到“消费者”消费了数据,“生产者”才会被唤醒继续“生产”。

android 有界队列 有界队列与无界队列_ci_07


在一个脚本中,模拟生产者-消费者模型,需要用到两个线程,一个是生产者线程,一个是消费者线程。一个简单的利用阻塞队列实现生产者-消费者模型的代码如下:

import queue
import random
import threading
import time


class Producer(threading.Thread):
    def __init__(self, q):
        super(Producer, self).__init__()
        self.q = q

    def run(self) -> None:  # 定义线程要运行的函数,线程被cpu调度后自动执行
        while True:
            item = random.randint(1, 10)
            self.q.put(item)  # 入队,队列满时阻塞
            print("Produce " + str(item) + ": " + str(self.q.qsize()) + " items in queue")
            time.sleep(random.randint(1, 2))


class Consumer(threading.Thread):
    def __init__(self, q):
        super(Consumer, self).__init__()
        self.q = q

    def run(self) -> None:  # 定义线程要运行的函数,线程被cpu调度后自动执行
        while True:
            item = self.q.get()  # 出队,队列为空时阻塞
            print("Consume " + str(item) + ": " + str(self.q.qsize()) + " items in queue")
            time.sleep(random.randint(1, 2))


if __name__ == '__main__':
    q = queue.Queue(4)
    c = Consumer(q)
    p = Producer(q)
    p2 = Producer(q)
    c.start()
    p.start()  # 启动线程
    p2.start()  # 启动线程

这段代码中,生产者线程时刻监控队列情况,队列没有满时,使用put方法在随机的时间间隔往队列中放入数据,消费者线程也时刻监控队列情况,队列不为空时,使用get方法从队列中取数据。从上面程序运行的结果看,队列满后生产者停止生产,等待消费者消费之后再生产。

Queue类还提供了两个非阻塞的操作方法,分别是put_nowait和get_nowait。这两个方法不会阻塞线程的执行,当队列已经满了,在调用put_nowait进行入队操作时会立即抛出queue.Full异常,而不是等待队列中有空闲。当队列为空时,在调用get_nowait进行出队操作时会立即抛出_queue.Empty异常,而不是等待有数据入队。

import queue

q=queue.Queue(5)
q.put_nowait(1)
q.put_nowait(2)
q.put_nowait(3)
q.put_nowait(4)
q.put_nowait(5)
# q.put_nowait(6)  # 抛出queue.Full异常
print(q.maxsize)
print(q.get_nowait())
print(q.get_nowait())
print(q.get_nowait())
print(q.get_nowait())
print(q.get_nowait())
print(q.get_nowait())  # 抛出_queue.Empty异常

Queue类除了上面基础的入队和出队操作外,更多的方法可以参考:https://docs.python.org/3.8/library/asyncio-queue.html?highlight=queue#queue
其中有两个比较难理解的方法task_done和join。

  • task_done()

意味着之前入队的一个任务已经完成。由队列的消费者线程调用。每一个get()调用得到一个任务,接下来的task_done()调用告诉队列该任务已经处理完毕。
如果当前一个join()正在阻塞,它将在队列中的所有任务都处理完时恢复执行(即每一个由put()调用入队的任务都有一个对应的task_done()调用)。

  • join()

阻塞调用线程,直到队列中的所有任务被处理掉。
只要有数据被加入队列,未完成的任务数就会增加。当消费者线程调用task_done()(意味着有消费者取得任务并完成任务),未完成的任务数就会减少。当未完成的任务数降到0,join()解除阻塞。

5.2. Python中的优先级队列PriorityQueue

优先队列是根据优先级判定谁先出来,如果优先级一样,则按数据的ASCII码输出。

优先级队列的一个常见应用场景是,电商网站在进行抢购时,用户访问下单提交量很大,服务器后端收到订单后,并不是直接进行扣库存处理,而是将请求放到队列,异步消费处理,用普通队列是FIFO的。但是如果需求是用户会员级别高的,可以优先抢购到商品,这个时候使用优先队列代替普通队列FIFO队列,就能满足我们的需求。

Python中queue模块中,有一个优先级队列的类PriorityQueue。

q = queue.PriorityQueue()
q.put((10, 'b'))
q.put((30, 'a'))
q.put((13, 'k'))
q.put((13, 'f'))
q.put((-3.5, 'c'))
q.put((-7.5, 'h'))
print(q.get())
print(q.get())
print(q.get())
print(q.get())
print(q.get())
print(q.get())
p = queue.PriorityQueue()
p.put(19)
p.put(1)
p.put(13)
p.put(5)
p.put(10)
print(p.get())
print(p.get())
print(p.get())
print(p.get())
print(p.get())

在将数据入优先级队列时,put方法的参数可以使单个数值和字符,也可以采用(priority_number, data)元组结构,元组第一个值代表优先级,值越小优先级越高,元组第二个值表示真正的数据。在出队列时,数值越小,或者priority_number越小的优先出队列。因此上面的代码出队列的顺序是:

(-7.5, 'h')
(-3.5, 'c')
(10, 'b')
(13, 'f')
(13, 'k')
(30, 'a')
1
5
10
13
19

与Queue一样,优先级队列的put和get方法也是阻塞方法,但是也提供非阻塞方法put_nowait和get_nowait。

利用优先级队列出队的顺序,可以利用优先级队列求出数据流中的第k大元素。题目来自leetcode703:https://leetcode-cn.com/problems/kth-largest-element-in-a-stream/
思路是维护一个长度为k的优先级队列,队首就是第k大元素。

具体算法如下,我增加了比较多的注释,方便理解:

import queue
class KthLargest:
    def __init__(self, k: int, nums: List[int]):
        self.q = queue.PriorityQueue()
        self.k = k
        for num in nums:  # 初始时将所有元素依次插入优先级队列
            self.q.put(num)
        while self.q.qsize() > k: # 保持队列长度是k
            self.q.get()

    def add(self, val: int) -> int:
        if self.q.qsize() == self.k:  # 队列长度已经是k
            head = max(self.q.get(), val)  # 获取队首和待add元素之间的大值
            self.q.put(head) 
        else:
            self.q.put(val)  # 队列长度不是k直接加入队列
        kth = self.q.get() # 获取队首,也就是第k大元素
        self.q.put(kth) # 还要放回去保持队列长度为k
        return kth  # 返回队首的元素,也就是第k大元素

利用优先级队列,通过上面的算法,能够始终保持队列中保持前k大元素。对于需要求topk的场景,非常好用。

5.3. Python中的后进先出队列LifoQueue

后进先出队列其实是与栈的效果类似。参考下面的代码,后入队的先出队。

q = queue.LifoQueue()
q.put(1)
q.put(2)
q.put(3)
q.put(4)
print(q.get())
print(q.get())
print(q.get())
print(q.get())