• ​​​​【收录 Hello 算法】5.2 队列


    目录

    5.2   队列

    5.2.1   队列常用操作

    5.2.2   队列实现

    1.   基于链表的实现

    2.   基于数组的实现

    5.2.3   队列典型应用


    5.2   队列

    队列(queue)是一种遵循先入先出规则的线性数据结构。顾名思义,队列模拟了排队现象,即新来的人不断加入队列尾部,而位于队列头部的人逐个离开。

    如图 5-4 所示,我们将队列头部称为“队首”,尾部称为“队尾”,将把元素加入队尾的操作称为“入队”,删除队首元素的操作称为“出队”。

    队列的先入先出规则

    图 5-4   队列的先入先出规则

    5.2.1   队列常用操作

    队列的常见操作如表 5-2 所示。需要注意的是,不同编程语言的方法名称可能会有所不同。我们在此采用与栈相同的方法命名。

    表 5-2   队列操作效率

    方法名描述时间复杂度
    push()元素入队,即将元素添加至队尾𝑂(1)
    pop()队首元素出队𝑂(1)
    peek()访问队首元素𝑂(1)

    我们可以直接使用编程语言中现成的队列类:

    queue.cpp

    1. /* 初始化队列 */
    2. queue<int> queue;
    3. /* 元素入队 */
    4. queue.push(1);
    5. queue.push(3);
    6. queue.push(2);
    7. queue.push(5);
    8. queue.push(4);
    9. /* 访问队首元素 */
    10. int front = queue.front();
    11. /* 元素出队 */
    12. queue.pop();
    13. /* 获取队列的长度 */
    14. int size = queue.size();
    15. /* 判断队列是否为空 */
    16. bool empty = queue.empty();

    5.2.2   队列实现

    为了实现队列,我们需要一种数据结构,可以在一端添加元素,并在另一端删除元素,链表和数组都符合要求。

    1.   基于链表的实现

    如图 5-5 所示,我们可以将链表的“头节点”和“尾节点”分别视为“队首”和“队尾”,规定队尾仅可添加节点,队首仅可删除节点。

    LinkedListQueuepush()pop()

    基于链表实现队列的入队出队操作

    图 5-5   基于链表实现队列的入队出队操作

    以下是用链表实现队列的代码:

    linkedlist_queue.cpp

    1. /* 基于链表实现的队列 */
    2. class LinkedListQueue {
    3. private:
    4. ListNode *front, *rear; // 头节点 front ,尾节点 rear
    5. int queSize;
    6. public:
    7. LinkedListQueue() {
    8. front = nullptr;
    9. rear = nullptr;
    10. queSize = 0;
    11. }
    12. ~LinkedListQueue() {
    13. // 遍历链表删除节点,释放内存
    14. freeMemoryLinkedList(front);
    15. }
    16. /* 获取队列的长度 */
    17. int size() {
    18. return queSize;
    19. }
    20. /* 判断队列是否为空 */
    21. bool isEmpty() {
    22. return queSize == 0;
    23. }
    24. /* 入队 */
    25. void push(int num) {
    26. // 在尾节点后添加 num
    27. ListNode *node = new ListNode(num);
    28. // 如果队列为空,则令头、尾节点都指向该节点
    29. if (front == nullptr) {
    30. front = node;
    31. rear = node;
    32. }
    33. // 如果队列不为空,则将该节点添加到尾节点后
    34. else {
    35. rear->next = node;
    36. rear = node;
    37. }
    38. queSize++;
    39. }
    40. /* 出队 */
    41. int pop() {
    42. int num = peek();
    43. // 删除头节点
    44. ListNode *tmp = front;
    45. front = front->next;
    46. // 释放内存
    47. delete tmp;
    48. queSize--;
    49. return num;
    50. }
    51. /* 访问队首元素 */
    52. int peek() {
    53. if (size() == 0)
    54. throw out_of_range("队列为空");
    55. return front->val;
    56. }
    57. /* 将链表转化为 Vector 并返回 */
    58. vector<int> toVector() {
    59. ListNode *node = front;
    60. vector<int> res(size());
    61. for (int i = 0; i < res.size(); i++) {
    62. res[i] = node->val;
    63. node = node->next;
    64. }
    65. return res;
    66. }
    67. };

    2.   基于数组的实现

    在数组中删除首元素的时间复杂度为 𝑂(𝑛) ,这会导致出队操作效率较低。然而,我们可以采用以下巧妙方法来避免这个问题。

    我们可以使用一个变量 front 指向队首元素的索引,并维护一个变量 size 用于记录队列长度。定义 rear = front + size ,这个公式计算出的 rear 指向队尾元素之后的下一个位置。

    基于此设计,数组中包含元素的有效区间为 [front, rear - 1],各种操作的实现方法如图 5-6 所示。

    • 入队操作:将输入元素赋值给 rear 索引处,并将 size 增加 1 。
    • 出队操作:只需将 front 增加 1 ,并将 size 减少 1 。

    可以看到,入队和出队操作都只需进行一次操作,时间复杂度均为 𝑂(1) 。

    ArrayQueuepush()pop()

    基于数组实现队列的入队出队操作

    图 5-6   基于数组实现队列的入队出队操作

    你可能会发现一个问题:在不断进行入队和出队的过程中,front 和 rear 都在向右移动,当它们到达数组尾部时就无法继续移动了。为了解决此问题,我们可以将数组视为首尾相接的“环形数组”。

    对于环形数组,我们需要让 front 或 rear 在越过数组尾部时,直接回到数组头部继续遍历。这种周期性规律可以通过“取余操作”来实现,代码如下所示:

    array_queue.cpp

    1. /* 基于环形数组实现的队列 */
    2. class ArrayQueue {
    3. private:
    4. int *nums; // 用于存储队列元素的数组
    5. int front; // 队首指针,指向队首元素
    6. int queSize; // 队列长度
    7. int queCapacity; // 队列容量
    8. public:
    9. ArrayQueue(int capacity) {
    10. // 初始化数组
    11. nums = new int[capacity];
    12. queCapacity = capacity;
    13. front = queSize = 0;
    14. }
    15. ~ArrayQueue() {
    16. delete[] nums;
    17. }
    18. /* 获取队列的容量 */
    19. int capacity() {
    20. return queCapacity;
    21. }
    22. /* 获取队列的长度 */
    23. int size() {
    24. return queSize;
    25. }
    26. /* 判断队列是否为空 */
    27. bool isEmpty() {
    28. return size() == 0;
    29. }
    30. /* 入队 */
    31. void push(int num) {
    32. if (queSize == queCapacity) {
    33. cout << "队列已满" << endl;
    34. return;
    35. }
    36. // 计算队尾指针,指向队尾索引 + 1
    37. // 通过取余操作实现 rear 越过数组尾部后回到头部
    38. int rear = (front + queSize) % queCapacity;
    39. // 将 num 添加至队尾
    40. nums[rear] = num;
    41. queSize++;
    42. }
    43. /* 出队 */
    44. int pop() {
    45. int num = peek();
    46. // 队首指针向后移动一位,若越过尾部,则返回到数组头部
    47. front = (front + 1) % queCapacity;
    48. queSize--;
    49. return num;
    50. }
    51. /* 访问队首元素 */
    52. int peek() {
    53. if (isEmpty())
    54. throw out_of_range("队列为空");
    55. return nums[front];
    56. }
    57. /* 将数组转化为 Vector 并返回 */
    58. vector<int> toVector() {
    59. // 仅转换有效长度范围内的列表元素
    60. vector<int> arr(queSize);
    61. for (int i = 0, j = front; i < queSize; i++, j++) {
    62. arr[i] = nums[j % queCapacity];
    63. }
    64. return arr;
    65. }
    66. };

    以上实现的队列仍然具有局限性:其长度不可变。然而,这个问题不难解决,我们可以将数组替换为动态数组,从而引入扩容机制。有兴趣的读者可以尝试自行实现。

    两种实现的对比结论与栈一致,在此不再赘述。

    5.2.3   队列典型应用

    Permanent link

    • 淘宝订单。购物者下单后,订单将加入队列中,系统随后会根据顺序处理队列中的订单。在双十一期间,短时间内会产生海量订单,高并发成为工程师们需要重点攻克的问题。
    • 各类待办事项。任何需要实现“先来后到”功能的场景,例如打印机的任务队列、餐厅的出餐队列等,队列在这些场景中可以有效地维护处理顺序。
  • 相关阅读:
    《动手学深度学习(Pytorch版)》Task02:预备知识——4.25打卡
    CommunityToolkit.Mvvm8.1 IOC依赖注入控制反转(5)
    Linux驱动开发 驱动程序的具体编写及出口入口函数解析,printk打印内核信息
    优雅的使用String字符串处理各种类型转换
    MySQL update 是锁行还是锁表?
    如何从Android恢复出厂设置后的手机恢复数据
    Flutter 3.0 之 PlatformView :告别 VirtualDisplay ,拥抱 TextureLayer
    webpack5内部是如何处理模块化的?
    融合一致性正则与流形正则的半监督深度学习算法
    【计算机网络笔记】网络应用进程通信
  • 原文地址:https://blog.csdn.net/qq_42700289/article/details/138783549