目录
一、背景知识
二、队列的应用
(一)在Spring中的应用
(二)在其他框架中的应用
(三)在实际开发中的应用
三、相关编程练习
(一)用队列实现栈
(二)使用栈实现队列
(三)设计循环队列
(四)滑动窗口最大值
(五)课程表
(六)队列的最大值
队列是一种常见的数据结构,其特点是先进先出(First-In-First-Out,FIFO),也就是最先入队的元素最先出队。队列可以用来实现任务调度、缓存、消息传递等应用场景。
队列通常有两个基本操作:入队(enqueue)和出队(dequeue)。入队操作将一个元素加入队列的尾部,出队操作将队列头部的元素移除并返回其值。队列还有一个获取队头元素但不移除的操作(peek)。
队列有多种实现方式,包括数组实现和链表实现。数组实现的队列通常需要指定一个固定的容量,而链表实现的队列可以动态增长。
队列还有一些变种,如双端队列(deque)和优先队列(priority queue)。双端队列支持在队头和队尾进行插入和删除操作,优先队列中元素按照一定的优先级顺序出队。
总的来说,队列的基本操作包括:
除了这些基本操作之外,队列的具体实现还可以包括其他的一些操作,例如扩容、缩容等。
按照以上的背景,展示举例:队列的大致流程就像是往管道中一个一个地放入小球,然后小球从管道前头一个个出来。(图来自图灵社区)
在 Spring 框架中,队列主要用于异步消息处理,其中的应用包括:
总的来说,Spring 框架中队列的应用主要涉及异步消息处理,其背后的思想是将任务分发给多个线程或者处理器,提高任务的处理效率和并发能力。
除了 Spring 框架,队列在其他一些框架中也有广泛的应用,例如:
总的来说,队列在分布式系统和大规模数据处理中扮演着重要的角色,被广泛应用于消息传输、任务调度、数据流处理等场景。
在实际开发工作中,队列可以应用于多个场景,例如:
总的来说,队列在实际开发工作中应用广泛,涉及任务调度、消息传输、数据处理等多个场景,可以提高系统的并发能力和性能,提高系统的可靠性和稳定性。
与栈知识及编程练习总结_张彦峰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
解题思路
比较常见的实现方式,使用一个数组和两个指针 front 和 rear 分别指向队列的头部和尾部,同时使用一个变量 size 维护队列中元素的个数,在 enQueue() 和 deQueue() 方法中,需要将指针向后移动一位,并使用取模操作保证指针不会越界。在 enQueue() 方法中,还需要注意特殊情况,即当队列为空时,需要将 front 指向插入的元素。
这个实现方式的时间复杂度为O(1)。
具体代码展示
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 ≤ 输入数组的大小。
进阶:你能在线性时间复杂度内解决此题吗?
解题思路
滑动窗口最大值问题可以使用单调队列来解决,具体思路如下:
由于每个元素最多进 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 。提示:
- 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 的节点的情况,那么就说明图中存在环。如果最终能够将所有节点输出,那么就说明没有环,可以完成所有课程的学习。
也可以使用队列实现拓扑排序算法。具体实现方法是:
如果最终能够输出所有节点,说明不存在环;否则,说明存在环。
具体代码展示
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
限制:
- 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());
}
}