队列知识及编程练习总结

目录

一、背景知识 

二、队列的应用

(一)在Spring中的应用

(二)在其他框架中的应用

(三)在实际开发中的应用

三、相关编程练习

(一)用队列实现栈

(二)使用栈实现队列

(三)设计循环队列

(四)滑动窗口最大值 

(五)课程表

(六)队列的最大值


一、背景知识 

队列是一种常见的数据结构,其特点是先进先出(First-In-First-Out,FIFO),也就是最先入队的元素最先出队。队列可以用来实现任务调度、缓存、消息传递等应用场景。

队列通常有两个基本操作:入队(enqueue)和出队(dequeue)。入队操作将一个元素加入队列的尾部,出队操作将队列头部的元素移除并返回其值。队列还有一个获取队头元素但不移除的操作(peek)。

队列有多种实现方式,包括数组实现和链表实现。数组实现的队列通常需要指定一个固定的容量,而链表实现的队列可以动态增长。

队列还有一些变种,如双端队列(deque)和优先队列(priority queue)。双端队列支持在队头和队尾进行插入和删除操作,优先队列中元素按照一定的优先级顺序出队。

总的来说,队列的基本操作包括:

  1. 入队(enqueue):将一个元素加入队列的尾部。
  2. 出队(dequeue):将队列头部的元素移除并返回其值。
  3. 获取队头元素但不移除(peek):返回队列头部的元素值,但不将其移除。
  4. 判断队列是否为空(isEmpty):如果队列中没有元素则返回 true,否则返回 false。
  5. 判断队列是否已满(isFull):如果队列已满则返回 true,否则返回 false。但是对于链表实现的队列来说,不需要判断队列是否已满,因为链表的长度是动态变化的。(对于数组实现的队列而言才有这个操作)

除了这些基本操作之外,队列的具体实现还可以包括其他的一些操作,例如扩容、缩容等。

按照以上的背景,展示举例:队列的大致流程就像是往管道中一个一个地放入小球,然后小球从管道前头一个个出来。(图来自图灵社区)

队列知识及编程练习总结_第1张图片

二、队列的应用

(一)在Spring中的应用

在 Spring 框架中,队列主要用于异步消息处理,其中的应用包括:

  1. Spring JMS(Java Message Service):Spring JMS 可以使用 JMS 队列实现异步消息处理。消息生产者将消息发送到队列中,消息消费者从队列中获取消息并进行处理。
  2. Spring AMQP(Advanced Message Queuing Protocol):Spring AMQP 是 Spring 对 AMQP 消息协议的支持,可以使用 AMQP 的队列实现异步消息处理。与 JMS 相比,AMQP 具有更强的消息路由和交换机管理能力。
  3. Spring Integration:Spring Integration 提供了丰富的消息通道和消息处理器,可以使用队列实现异步消息处理。
  4. Spring Batch:Spring Batch 是 Spring 提供的批处理框架,其中的 JobLauncher 实现了异步任务处理,使用了内部的任务队列进行任务调度。

总的来说,Spring 框架中队列的应用主要涉及异步消息处理,其背后的思想是将任务分发给多个线程或者处理器,提高任务的处理效率和并发能力

(二)在其他框架中的应用

除了 Spring 框架,队列在其他一些框架中也有广泛的应用,例如:

  1. Apache Kafka:Apache Kafka 是一个分布式消息队列,被广泛用于大规模数据流处理和实时数据管道。它提供高可用、高可靠的消息传输服务,支持多个消费者和分区,同时还具有高性能和可扩展性。
  2. RabbitMQ:RabbitMQ 是一个 AMQP 实现,支持多种消息队列模式,包括点对点、发布/订阅和路由等。它具有高可用、高可靠性和高性能,支持多种客户端语言和多种平台。
  3. Redis:Redis 是一个内存中的数据结构存储系统,可以使用 Redis List 实现队列。它支持数据持久化、集群和复制,同时还支持多种数据结构操作和多种客户端语言。
  4. ActiveMQ:ActiveMQ 是一个开源的 JMS 实现,支持多种消息队列模式,包括点对点、发布/订阅和通配符等。它具有高可用、高可靠性和高性能,同时还支持多种客户端语言和多种平台。

总的来说,队列在分布式系统和大规模数据处理中扮演着重要的角色,被广泛应用于消息传输、任务调度、数据流处理等场景

(三)在实际开发中的应用

在实际开发工作中,队列可以应用于多个场景,例如:

  1. 异步任务处理:当系统需要执行大量耗时的任务时,可以将任务放入队列中,由多个工作者线程异步地处理任务,从而提高系统的并发能力和性能。
  2. 消息队列:当系统需要在多个进程或者机器之间传递消息时,可以使用消息队列。生产者将消息放入队列中,消费者从队列中获取消息并进行处理,从而实现不同进程或者机器之间的异步通信。
  3. 数据流处理:当系统需要处理大规模的数据流时,可以使用队列将数据分配给多个处理节点进行处理。例如,将用户行为日志放入队列中,由多个处理节点进行数据分析和挖掘。
  4. 任务调度:当系统需要执行周期性的任务时,可以使用队列进行任务调度。将任务放入队列中,由任务调度器定时执行任务,并将执行结果返回给系统。
  5. 网络流量控制:当系统需要控制网络流量时,可以使用队列进行流量控制。例如,将网络请求放入队列中,限制同时处理请求的数量,从而避免系统崩溃。

总的来说,队列在实际开发工作中应用广泛,涉及任务调度、消息传输、数据处理等多个场景,可以提高系统的并发能力和性能,提高系统的可靠性和稳定性

三、相关编程练习

(一)用队列实现栈

与栈知识及编程练习总结_张彦峰ZYF的博客-CSDN博客中练习三(五)相同,请直接跳转阅读。

(二)使用栈实现队列

与栈知识及编程练习总结_张彦峰ZYF的博客-CSDN博客中练习三(四)相同,请直接跳转阅读。

(三)设计循环队列

题目描述:设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。

循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。

你的实现应该支持如下操作:

  • MyCircularQueue(k): 构造器,设置队列长度为 k 。
  • Front: 从队首获取元素。如果队列为空,返回 -1 。
  • Rear: 获取队尾元素。如果队列为空,返回 -1 。
  • enQueue(value): 向循环队列插入一个元素。如果成功插入则返回真。
  • deQueue(): 从循环队列中删除一个元素。如果成功删除则返回真。
  • isEmpty(): 检查循环队列是否为空。
  • isFull(): 检查循环队列是否已满。

示例:

MyCircularQueue circularQueue = new MyCircularQueue(3); // 设置长度为 3
circularQueue.enQueue(1);  // 返回 true
circularQueue.enQueue(2);  // 返回 true
circularQueue.enQueue(3);  // 返回 true
circularQueue.enQueue(4);  // 返回 false,队列已满
circularQueue.Rear();  // 返回 3
circularQueue.isFull();  // 返回 true
circularQueue.deQueue();  // 返回 true
circularQueue.enQueue(4);  // 返回 true
circularQueue.Rear();  // 返回 4
 

提示:

  • 所有的值都在 0 至 1000 的范围内;
  • 操作数将在 1 至 1000 的范围内;
  • 请不要使用内置的队列库。

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/design-circular-queue

解题思路

比较常见的实现方式,使用一个数组和两个指针 frontrear 分别指向队列的头部和尾部,同时使用一个变量 size 维护队列中元素的个数,在 enQueue()deQueue() 方法中,需要将指针向后移动一位,并使用取模操作保证指针不会越界。在 enQueue() 方法中,还需要注意特殊情况,即当队列为空时,需要将 front 指向插入的元素。

这个实现方式的时间复杂度为O(1)。

队列知识及编程练习总结_第2张图片

 具体代码展示

package org.zyf.javabasic.letcode.queue;

/**
 * @author yanfengzhang
 * @description 设计你的循环队列实现。
 * 循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。
 * 

* 循环队列的一个好处是我们可以利用这个队列之前用过的空间。 * 在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。 * 但是使用循环队列,我们能使用这些空间去存储新的值。 * @date 2023/4/9 14:58 */ public class MyCircularQueue { /*存储队列元素的数组*/ private int[] data; /*队头指针,指向队头元素*/ private int front; /*队尾指针,指向队尾元素的下一个位置*/ private int rear; /*队列中元素的个数*/ private int size; public MyCircularQueue(int k) { /*初始化数组*/ data = new int[k]; /*初始时队头指针指向 -1*/ front = -1; /*初始时队尾指针指向 -1*/ rear = -1; /*初始时队列中元素个数为 0*/ size = 0; } public boolean enQueue(int value) { /*队列已满,插入失败*/ if (isFull()) { return false; } /*队列为空,需要将 front 指向插入的元素*/ if (isEmpty()) { front = 0; } /*队尾指针向后移动一位,并使用取模操作保证指针不会越界*/ rear = (rear + 1) % data.length; /*在队尾插入元素*/ data[rear] = value; /*队列中元素个数加一*/ size++; /*插入成功*/ return true; } public boolean deQueue() { /*队列为空,删除失败*/ if (isEmpty()) { return false; } /*队列中只有一个元素,需要将 front 和 rear 指向 -1*/ if (front == rear) { front = -1; rear = -1; } /*队列中有多个元素,需要将 front 指针向后移动一位*/ else { /*队头指针向后移动一位,并使用取模操作保证指针不会越界*/ front = (front + 1) % data.length; } /*队列中元素个数减一*/ size--; /*删除成功*/ return true; } public int Front() { if (isEmpty()) { /*队列为空,返回 -1*/ return -1; } /*返回队头元素*/ return data[front]; } public int Rear() { /*队列为空,返回 -1*/ if (isEmpty()) { return -1; } /*返回队尾元素*/ return data[rear]; } public boolean isEmpty() { /*判断队列是否为空*/ return size == 0; } public boolean isFull() { /*判断队列是否已满*/ return size == data.length; } /** * 在该测试代码中,我们创建了一个容量为 3 的循环队列, * 然后进行了一系列操作来测试该类的实现是否正确。 * 最后输出的结果符合预期,说明该循环队列的实现是正确的。 */ public static void main(String[] args) { /*创建容量为 3 的循环队列*/ MyCircularQueue circularQueue = new MyCircularQueue(3); /*输出 true,队列变为 [1]*/ System.out.println(circularQueue.enQueue(1)); /*输出 true,队列变为 [1, 2]*/ System.out.println(circularQueue.enQueue(2)); /*输出 true,队列变为 [1, 2, 3]*/ System.out.println(circularQueue.enQueue(3)); /*输出 false,队列已满,插入失败*/ System.out.println(circularQueue.enQueue(4)); /*输出 3,队尾元素为 3*/ System.out.println(circularQueue.Rear()); /*输出 true,队列已满*/ System.out.println(circularQueue.isFull()); /*输出 true,队列变为 [2, 3]*/ System.out.println(circularQueue.deQueue()); /*输出 true,队列变为 [2, 3, 4]*/ System.out.println(circularQueue.enQueue(4)); /*输出 4,队尾元素为 4*/ System.out.println(circularQueue.Rear()); } }

(四)滑动窗口最大值 

题目描述:给定一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回滑动窗口中的最大值。

示例:输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3       输出: [3,3,5,5,6,7]

解释:滑动窗口的位置 最大值

  • [1 3 -1] -3 5 3 6 7 3
  • 1 [3 -1 -3] 5 3 6 7 3
  • 1 3 [-1 -3 5] 3 6 7 5
  • 1 3 -1 [-3 5 3] 6 7 5
  • 1 3 -1 -3 [5 3 6] 7 6
  • 1 3 -1 -3 5 [3 6 7] 7

提示:你可以假设 k 总是有效的,在输入数组不为空的情况下,1 ≤ k ≤ 输入数组的大小。

进阶:你能在线性时间复杂度内解决此题吗?

解题思路

滑动窗口最大值问题可以使用单调队列来解决,具体思路如下:

  1. 我们使用双端队列 deque,存储数组的下标。deque 中的元素按照从大到小的顺序排列,即 deque 中的第一个元素是滑动窗口中的最大值。
  2. 我们遍历数组,依次将元素放入 deque 中。当元素个数大于 k 时,我们需要将 deque 的第一个元素弹出,因为它不在滑动窗口中了。
  3. 对于新加入 deque 的元素,从队尾开始比较,如果队尾的元素比当前元素小,则说明队尾的元素不可能是滑动窗口中的最大值,将其弹出。直到队尾的元素比当前元素大,或者 deque 为空,我们再将当前元素插入队尾。
  4. 每次队列中的最大值即为 deque 的第一个元素,我们将其存入结果数组中。

由于每个元素最多进 deque 和出 deque 一次,因此时间复杂度为 O(n)。

需要注意的是,deque 中存储的是数组中元素的下标,而不是元素本身。因此在计算 deque 中的最大值时,需要根据下标从原数组中取出对应的元素。

具体代码展示

package org.zyf.javabasic.letcode.queue;

import java.util.Arrays;
import java.util.Deque;
import java.util.LinkedList;

/**
 * @author yanfengzhang
 * @description 给定一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。
 * 你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
 * 返回滑动窗口中的最大值。
 * @date 2023/4/9  15:23
 */
public class MaxSlidingWindow {

    /**
     * 1 我们使用双端队列 deque,存储数组的下标。deque 中的元素按照从大到小的顺序排列,即 deque 中的第一个元素是滑动窗口中的最大值。
     * 2 我们遍历数组,依次将元素放入 deque 中。当元素个数大于 k 时,我们需要将 deque 的第一个元素弹出,因为它不在滑动窗口中了。
     * 3 对于新加入 deque 的元素,从队尾开始比较,如果队尾的元素比当前元素小,
     * 则说明队尾的元素不可能是滑动窗口中的最大值,将其弹出。
     * 直到队尾的元素比当前元素大,或者 deque 为空,我们再将当前元素插入队尾。
     * 4 每次队列中的最大值即为 deque 的第一个元素,我们将其存入结果数组中。
     */
    public int[] maxSlidingWindow(int[] nums, int k) {
        int n = nums.length;
        if (n == 0) {
            return new int[0];
        }
        if (k == 1) {
            return nums;
        }

        /*双端队列,存储数组下标*/
        Deque deque = new LinkedList<>();

        /*初始化队列,从 0 到 k - 1,把队列中比 nums[i] 小的元素全部弹出*/
        for (int i = 0; i < k; i++) {
            while (!deque.isEmpty() && nums[i] >= nums[deque.peekLast()]) {
                deque.pollLast();
            }
            deque.offerLast(i);
        }

        int[] ans = new int[n - k + 1];
        ans[0] = nums[deque.peekFirst()];

        /*遍历数组,维护队列中的元素*/
        for (int i = k; i < n; i++) {
            /*判断队首是否在滑动窗口中*/
            if (deque.peekFirst() <= i - k) {
                deque.pollFirst();
            }

            /*把队列中比 nums[i] 小的元素全部弹出*/
            while (!deque.isEmpty() && nums[i] >= nums[deque.peekLast()]) {
                deque.pollLast();
            }

            /*将当前元素插入队尾*/
            deque.offerLast(i);

            ans[i - k + 1] = nums[deque.peekFirst()];
        }

        return ans;
    }

    public static void main(String[] args) {
        int[] nums = {1, 3, -1, -3, 5, 3, 6, 7};
        int k = 3;
        int[] result = new MaxSlidingWindow().maxSlidingWindow(nums, k);
        /*输出:[3, 3, 5, 5, 6, 7]*/
        System.out.println(Arrays.toString(result));

    }
}

(五)课程表

题目描述:你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。

在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程  bi 。

例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。
请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。

队列知识及编程练习总结_第3张图片

 提示:

  • 1 <= numCourses <= 105
  • 0 <= prerequisites.length <= 5000
  • prerequisites[i].length == 2
  • 0 <= ai, bi < numCourses
  • prerequisites[i] 中的所有课程对 互不相同

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/course-schedule

解题思路

该问题可以抽象成一个有向图,课程之间的先修关系可以表示为图中的边。因此,该问题就转化为在有向图中判断是否存在环。若存在环,那么就无法完成所有课程的学习,否则就可以完成。

使用拓扑排序算法可以解决该问题。拓扑排序可以输出图的节点,满足节点的前驱节点已经全部输出。对于本题,我们可以使用拓扑排序来判断有向图中是否存在环。具体做法是,每次选择一个入度为 0 的节点,并移除该节点以及以该节点为起点的所有边,重复这个过程直到图为空,若过程中存在没有入度为 0 的节点的情况,那么就说明图中存在环。如果最终能够将所有节点输出,那么就说明没有环,可以完成所有课程的学习。

也可以使用队列实现拓扑排序算法。具体实现方法是:

  1. 统计所有节点的入度,将入度为 0 的节点加入队列。
  2. 取出队首节点,输出该节点,并将以该节点为起点的所有边移除,更新它们的入度。如果某个节点入度变为 0,则将该节点加入队列。
  3. 重复第 2 步,直到队列为空或者无法再取出入度为 0 的节点。

如果最终能够输出所有节点,说明不存在环;否则,说明存在环。

具体代码展示

package org.zyf.javabasic.letcode.queue;

import java.util.LinkedList;
import java.util.Queue;

/**
 * @author yanfengzhang
 * @description 你这个学期必须选修 numCourses 门课程,记为0到numCourses - 1 。
 * 在选修某些课程之前需要一些先修课程。
 * 先修课程按数组prerequisites 给出,其中prerequisites[i] = [ai, bi] ,表示如果要学习课程ai 则 必须 先学习课程bi 。
 * 

* 例如,先修课程对[0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。 * 请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。 * @date 2023/4/9 15:55 */ public class CanFinishCourses { /** * 使用队列实现拓扑排序算法。具体实现方法是: * 统计所有节点的入度,将入度为 0 的节点加入队列。 * 取出队首节点,输出该节点,并将以该节点为起点的所有边移除,更新它们的入度。如果某个节点入度变为 0,则将该节点加入队列。 * 重复第 2 步,直到队列为空或者无法再取出入度为 0 的节点。 * 如果最终能够输出所有节点,说明不存在环;否则,说明存在环。 */ public boolean canFinish(int numCourses, int[][] prerequisites) { /*记录每个节点的入度*/ int[] inDegree = new int[numCourses]; for (int[] pre : prerequisites) { inDegree[pre[0]]++; } Queue queue = new LinkedList<>(); /*将入度为 0 的节点加入队列*/ for (int i = 0; i < numCourses; i++) { if (inDegree[i] == 0) { queue.offer(i); } } int count = 0; /*取出队首节点,输出该节点,并将以该节点为起点的所有边移除,更新它们的入度*/ while (!queue.isEmpty()) { int curr = queue.poll(); count++; for (int[] pre : prerequisites) { if (pre[1] == curr) { inDegree[pre[0]]--; if (inDegree[pre[0]] == 0) { queue.offer(pre[0]); } } } } /*如果最终能够输出所有节点,说明不存在环;否则,说明存在环*/ return count == numCourses; } }

(六)队列的最大值

题目描述:请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_value、push_back 和 pop_front 的均摊时间复杂度都是O(1)。

若队列为空,pop_front 和 max_value 需要返回 -1

队列知识及编程练习总结_第4张图片

 限制:

  • 1 <= push_back,pop_front,max_value的总操作数 <= 10000
  • 1 <= value <= 10^5

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/dui-lie-de-zui-da-zhi-lcof

解题思路

可以使用两个队列来实现,一个队列用来存储插入的元素,另一个队列用来存储当前队列中的最大值。

具体实现时,我们可以使用两个 LinkedList 来实现双端队列,一个队列用来存储插入的元素,另一个队列用来存储当前队列中的最大值。

每次插入元素时,先将该元素插入到存储元素的队列中,然后将最大值队列中小于等于该元素的元素全部弹出,保证最大值队列中的元素始终是递减的。然后将该元素插入到最大值队列中。

当需要弹出队首元素时,如果队首元素等于最大值队列中的第一个元素,则同时弹出该元素。

由于每个元素最多只会被弹入和弹出一次,因此总时间复杂度为O(n),均摊时间复杂度为 O(1)。

具体代码展示

package org.zyf.javabasic.letcode.queue;

import java.util.LinkedList;

/**
 * @author yanfengzhang
 * @description 请定义一个队列并实现函数 max_value 得到队列里的最大值,
 * 要求函数max_value、push_back 和 pop_front 的均摊时间复杂度都是O(1)。
 * 

* 若队列为空,pop_front 和 max_value需要返回 -1 * @date 2023/4/9 16:19 */ public class MaxQueue { /*存储正常数据的队列*/ private LinkedList queue; /*存储当前队列中最大值的队列*/ private LinkedList maxQueue; public MaxQueue() { queue = new LinkedList<>(); maxQueue = new LinkedList<>(); } public int max_value() { if (maxQueue.isEmpty()) { return -1; } /*返回最大值队列的队首元素*/ return maxQueue.peekFirst(); } public void push_back(int value) { /*将元素加入正常队列*/ queue.offerLast(value); while (!maxQueue.isEmpty() && maxQueue.peekLast() < value) { /*将小于当前元素的元素从最大值队列中弹出*/ maxQueue.pollLast(); } /*将当前元素加入最大值队列*/ maxQueue.offerLast(value); } public int pop_front() { if (queue.isEmpty()) { return -1; } /*弹出正常队列的队首元素*/ int front = queue.pollFirst(); if (front == maxQueue.peekFirst()) { /*如果该元素是最大值队列的队首元素,则同时弹出*/ maxQueue.pollFirst(); } return front; } public static void main(String[] args) { MaxQueue q = new MaxQueue(); /*输出 -1*/ System.out.println(q.max_value()); q.push_back(1); q.push_back(3); q.push_back(2); /*输出 3*/ System.out.println(q.max_value()); /*输出 1*/ System.out.println(q.pop_front()); /*输出 3*/ System.out.println(q.max_value()); /*输出 3*/ System.out.println(q.pop_front()); /*输出 2*/ System.out.println(q.max_value()); /*输出 2*/ System.out.println(q.pop_front()); /*输出 -1*/ System.out.println(q.max_value()); } }

你可能感兴趣的:(算法相关内容,java,数据结构,leetcode)