上一篇文章我们已经了解了队列的基本知识和应用 2023 跟我一起学算法:数据结构和算法-「队列」上 - 掘金 (juejin.cn) 这一篇我们继续介绍队列的相关内容。
不同类型的队列及其应用
介绍 :
队列是一种线性结构.遵循特定的操作执行顺序。顺序为先进先出 (FIFO)。队列的一个很好的例子是资源的任何消费者队列,其中先到达的消费者首先得到服务。在本文中,讨论了不同类型的队列。
队列类型:
有五种不同类型的队列,用于不同的场景。他们是:
-
输入受限队列(这是一个简单队列)
-
输出受限队列(这也是一个简单队列)
-
循环队列
-
双端队列(Deque)
-
优先队列
- 优先级升序队列
- 优先级降序队列

1、循环队列:
循环队列是一种线性数据结构,其操作基于 FIFO(先进先出)原则进行,最后一个位置连接回第一个位置以形成一个循环。它也被称为 "环形缓冲区" 。该队列主要用于以下情况:
- 内存管理:普通队列中未使用的内存位置可以在循环队列中利用。
- 交通系统:在计算机控制的交通系统中,采用循环队列,按照设定的时间一一重复地打开交通信号灯。
- CPU 调度:操作系统通常维护一个准备执行或等待特定事件发生的进程队列。
循环队列的时间复杂度为O(1)。
2、 输入受限队列:
在这种类型的队列中,只能从一侧(后侧)输入,并且可以从两侧(前侧和后侧)删除元素。这种队列不遵循 FIFO(先进先出)。该队列用于以下情况:数据消耗需要按照 FIFO 顺序,但由于某种原因需要删除最近插入的数据,这种情况可能是不相关的数据、性能问题等。

输入限制队列的优点:
- 通过限制添加的项目数量来防止队列溢出和过载
- 帮助保持系统的稳定性和可预测的性能
输入限制队列的缺点:
- 如果限制设置得太低,可能会导致资源浪费并且物品经常被丢弃
- 如果限制设置得太高并且队列已满,可能会导致等待或阻塞,从而阻止添加新项目。
3.输出受限队列:
在这种类型的队列中,可以从两侧(后侧和前侧)输入,并且只能从一侧(前侧)删除元素。该队列用于输入具有某种要执行的优先顺序并且输入甚至可以放置在第一位以便首先执行的情况。

4. 双端队列:
双端队列也是一种队列数据结构,其中插入和删除操作都在两端(前和后)进行。也就是说,我们可以在前后位置插入,也可以在前后位置删除。由于 Deque 同时支持堆栈和队列操作,因此它可以同时用作堆栈和队列操作。
Deque 数据结构支持 O(1) 时间内的顺时针和逆时针旋转,这在某些应用程序中非常有用。此外,使用双端队列可以有效解决需要删除和/或添加两端元素的问题。

双端队列
5. 优先级队列 :
优先级队列是一种特殊类型的队列,其中每个元素都与一个优先级相关联,并根据其优先级提供服务。有两种类型的优先级队列。他们是:
-
优先级升序队列: 可以任意插入元素,但只能删除最小的元素。
例如,假设有一个数组,其中元素 4、2、8 的顺序相同。所以,插入元素时,插入的顺序是相同的,而删除时,顺序是2、4、8。
-
优先级降序队列: 可以任意插入元素,但只能首先从给定队列中删除最大的元素。
例如,假设有一个数组,其中元素 4、2、8 的顺序相同。所以,插入元素时,插入的顺序是相同的,而删除时,顺序是8、4、2。
优先级队列的时间复杂度为O(logn)。
6. 队列的应用:
当事物不需要立即处理,但必须像广度优先搜索那样以先进先出的顺序处理时,就会使用队列。队列的这一属性使其在以下场景中也很有用。
- 当资源在多个消费者之间共享时。示例包括CPU 调度 、磁盘调度。
- 当数据在两个进程之间异步传输时(数据接收速率不一定与发送速率相同)。示例包括 IO 缓冲区、管道、文件 IO 等。
- 线性队列:线性队列是一种将数据元素添加到队列末尾并从队列前面删除的队列。线性队列用于需要按照接收顺序处理数据元素的应用程序。示例包括打印机队列和消息队列。
- 循环队列:循环队列与线性队列类似,但队列尾部与队列前端相连。这可以有效利用内存空间并提高性能。循环队列用于需要以循环方式处理数据元素的应用程序。示例包括 CPU 调度和内存管理。
- 优先级队列:优先级队列是一种队列,其中每个元素都分配有一个优先级。具有较高优先级的元素在具有较低优先级的元素之前被处理。优先级队列用于需要以更高优先级处理某些任务或数据元素的应用程序。示例包括操作系统任务调度和网络数据包调度。
- 双端队列:双端队列也称为双端队列,是一种队列类型,可以从队列的任一端添加或删除元素。这使得数据处理更加灵活,并且可以用于需要在多个方向上处理元素的应用程序。示例包括作业调度和搜索算法。
- 并发队列:并发队列是一种队列,旨在处理同时访问队列的多个线程。并发队列用于多线程应用程序,其中需要以线程安全的方式在线程之间共享数据。示例包括数据库事务和 Web 服务器请求。
7. 队列问题:
使用队列时可能出现的一些常见问题:
- 队列溢出:当队列达到其最大容量并且无法接受更多元素时,就会发生队列溢出。这可能会导致数据丢失并导致应用程序崩溃。
- 队列下溢:当尝试从空队列中删除元素时,就会发生队列下溢。这可能会导致错误和应用程序崩溃。
- 优先级反转:当低优先级任务占用高优先级任务所需的资源时,优先级队列中会发生优先级反转。这可能会导致处理延迟并影响系统性能。
- 死锁:当多个线程或进程互相等待对方释放资源时,就会出现死锁,导致所有线程都无法继续进行的情况。使用并发队列时可能会发生这种情况,并可能导致系统崩溃。
- 性能问题:队列性能可能会受到多种因素的影响,例如队列的大小、访问频率以及对队列执行的操作类型。队列性能不佳会导致系统性能变慢并降低用户体验。
- 同步问题:当多个线程同时访问同一队列时,可能会出现同步问题。这可能会导致数据损坏、竞争条件和其他错误。
- 内存管理问题:队列可能会消耗大量内存,尤其是在处理大型数据集时。可能会发生内存泄漏和其他内存管理问题,从而导致系统崩溃和其他错误。
使用队列实现 LRU 缓存实现
如何实现LRU缓存方案?应该使用什么数据结构?
我们给出了可以引用的总可能页码。我们还给出了缓存(或内存)大小(缓存一次可以容纳的页帧数)。LRU 缓存方案是当缓存已满并且引用缓存中不存在的新页面时删除最近最少使用的帧。
使用队列和散列的 LRU 缓存实现:
要解决该问题,需要遵循以下想法:
我们使用两种数据结构来实现 LRU Cache。
- 队列是使用双向链表实现的。队列的最大大小将等于可用帧的总数(缓存大小)。最近使用的页面将靠近前端,最近最少使用的页面将靠近后端。
- 以页码为键、对应队列节点的地址为值的哈希。
当一个页面被引用时,所需的页面可能在内存中。如果它在内存中,我们需要分离列表的节点并将其带到队列的前面。 如果所需的页面不在内存中,我们会将其放入内存中。简单来说,我们将一个新节点添加到队列的前面,并更新哈希中相应的节点地址。如果队列已满,即所有帧都已满,我们从队列的后面删除一个节点,并将新节点添加到队列的前面。
示例 -- 考虑以下参考字符串:1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5
使用 3 页框架的最近最少使用 (LRU) 页面替换算法查找页面错误数。
下面是上述方法的图示:


注意: 最初内存中没有任何元素。
请按照以下步骤解决问题:
-
创建一个 LRUCache 类,声明一个 int 类型的列表、一个 <int, list> 类型的无序映射以及一个用于存储缓存最大大小的变量
-
在LRUCache的refer函数中
- 如果队列中不存在该值,则将该值推入队列前面,如果队列已满,则删除最后一个值
- 如果该值已经存在,则将其从队列中删除并将其推入队列的前面
-
在显示函数print中,LRUCache使用从前面开始的队列
javascript 代码示例:
js
// JavaScript程序实现LRU缓存
// 使用Set和LinkedList
class LRUCache {
constructor(capacity) {
this.cache = new Set();
this.capacity = capacity;
}
// 此函数如果缓存中不存在键,则返回false。否则,它会通过先移除再添加的方式将键移到前面,并返回true。
get(key) {
if (!this.cache.has(key)) {
return false;
}
this.cache.delete(key);
this.cache.add(key);
return true;
}
/* 在LRU缓存中引用键x */
refer(key) {
if (!this.get(key)) {
this.put(key);
}
}
// 以相反的顺序显示缓存内容
display() {
const list = [...this.cache];// The reverse() method of
// Array类用于反转数组中的元素
list.reverse();
let ans="";
for (const key of list) {
ans = ans +key + " ";
}
console.log(ans);
}
put(key) {
if (this.cache.size === this.capacity) {
const firstKey = this.cache.values().next().value;
this.cache.delete(firstKey);
}
this.cache.add(key);
}
}
const ca = new LRUCache(4);
ca.refer(1);
ca.refer(2);
ca.refer(3);
ca.refer(1);
ca.refer(4);
ca.refer(5);
ca.display();
golang 代码示例:
go
package main
import (
"fmt"
"testing"
)
type LRUCache struct {
list []int
csize int
ma map[int]int
}
func (lru *LRUCache) refer(x int) {
if index, ok := lru.ma[x]; !ok {
// 如果存在,比较当前的容量是否已达上限
if len(lru.list) == lru.csize {
// 如果已达上限,则删除栈顶元素
lru.list = lru.list[:lru.csize-1]
}
} else {
// 如果存在, 则删除对应 index 位置的值, 并将期追加到队尾
lru.list = append(lru.list[:index-1], lru.list[index+1:]...)
}
lru.list = append(lru.list, x)
lru.ma[x] = len(lru.list)
}
func (lru *LRUCache) Display() {
for i := len(lru.list) - 1; i >= 0; i-- {
fmt.Println(lru.list[i])
}
}
func NewLRUCache(size int) *LRUCache {
ma := make(map[int]int)
return &LRUCache{
list: []int{},
csize: size,
ma: ma,
}
}
func Test_NewLRUCache(t *testing.T) {
cache := NewLRUCache(4)
cache.refer(1)
cache.refer(2)
cache.refer(3)
cache.refer(1)
cache.refer(4)
cache.refer(5)
cache.Display()
}
复杂度分析
时间复杂度: O(1),我们使用Linked HashSet数据结构来实现缓存。
Linked HashSet 为添加元素和检索元素提供恒定的时间复杂度。
辅助空间: O(n),我们需要在缓存中存储n个元素,所以空间复杂度为O(n)。