系列文章导引
开源项目
本系列所有文章都将会收录到GitHub
中统一收藏与管理,欢迎ISSUE
和Star
。
GitHub传送门:Kiner算法算题记
概念
有一个连续的存储区存储任意结构,有头指针和尾指针,尾指针一般指向最后一个元素的下一位
先入先出(FIFO)
基本操作
一个最简单的队列结构至少要支持以下两种操作:
入队(push
)
尾指针向后移动一步,并插入元素
出队(pop
)
- 逻辑出栈:头指针向后移动一步
- 真实出栈:如果使用数组模拟队列的话,就是调用数组的
shift
方法,将数组第一个元素弹出
队列的常见变种
循环队列
由于在大部分的语言中,都是采用头尾指针的方式对队列进行操作的,那么这样就可能导致一个问题:
# 加入有一个长度为10的空队列,下面队列中的*代表该为为空
[*,*,*,*,*,*,*,*,*,*]
# 先后执行多次入队和出队操作
push 1
push 2
pop
push 3
# 经过上述的操作后,我们的队列长成这样,其中头指针指向的位置是2所在的位置,尾指针指向的位置是3的下一位,也就是3后面的*
# 由于队列是先进先出的结构,上述执行了一次pop操作,因此1被弹出了队列,用<1>标识1倍逻辑删除了。
[<1>,2,3,*,*,*,*,*,*,*]
# 从上面的操作我们不难联想到,我们这个队列的大小是有限的,仅有10位,而每当我们执行pop操作时,就会出现很多个被逻辑删除的元素,
# 虽然我们已经用不上这个元素了,但是因为是逻辑删除,并没有真正的删除这个元素,所以还是会占了一个坑,就像上面的<1>,这样,就有可能会出现一个“假溢出”的情况,如:
[<1>,<2>,<3>,4,5,6,7,8,9,10]
# 如上面的这个队列,看起来这个队列好像是满了,没办法在放置下一个元素了,但是,我们可以发现,前面的<1>,<2>,<3>都是已经被逻辑删除的,对我们来说是没有用的元素,其实我们这个队列并没有真正溢出,仅仅是因为这几个家伙占着茅坑不拉屎导致的假溢出“假溢出”。
那么,如果要解决队列假溢出
的问题,我们就引申出来一个队列的变种,叫做循环队列
。循环队列
就是为了有效的利用队列的空间,当队列的尾指针到了最后的时候,如果还要插入元素,那么尾指针会回到队列的第一位,也就是上面<1>所在的位置,只要头尾指针不相遇,我们就可以继续网队列里面插入元素,如上述队列,如果使用循环队列
实现,最终可能是这样:
# 以下队列便是真正的队满队列,其中队首是4,队尾是13
[11,12,13,4,5,6,7,8,9,10]
循环队列的Typescript
版本实现
// leetcode [622] 设计循环队列
// 这里为了模拟大部分语言中的实现,因此除了使用数组用来存储数据外,没有使用数户组的一些方法,如pop何push等,直接使用头尾指针实现
class MyCircularQueue {
// 用于记录队列中实际存储了多少个元素
private count: number = 0;
// js中使用数组模拟队列
private queue: number[];
// 头指针
private head: number = 0;
// 尾指针
private tail: number = 0;
// 初始化时初始化一个长度为k的数组控件
constructor(private k: number) {
this.queue = new Array<number>(k);
}
// 入队
enQueue(value: number): boolean {
// 若队列满了则直接返回false
if(this.isFull()) return false;
// console.log(this.isFull(), this.count, this.k);
// 将元素赋值给尾指针指向的位置
this.queue[this.tail] = value;
// 入队操作,需要将元素数量加一
this.count++;
// 尾指针向后移动一位,由于当前队列是循环队列,如果刚好向后移动一位超出了数组的长度,就会出现异常
// 这里可以使用一个技巧,尾指针向后移动一位之后,再跟初始化时的数组长度取余就可以获得真实的尾指针位置了
// 如:k=10,当前的tail指针指向9,那么,尾指针向后移动一位就是:9+1%10=0,尾指针应该指向我们数组的第一位元素了
this.tail = (this.tail + 1) % this.k;
return true;
}
// 出队操作
deQueue(): boolean {
// 当队列为空时,返回false
if(this.isEmpty()) return false;
// 出队操作,元素数量减一
this.count--;
// 头指针向后移动一位,为了防止超过数组长度,因此进行取余操作,具体想看入队操作的解释
this.head = (this.head + 1) % this.k;
return true;
}
// 返回队首袁术
Front(): number {
if(this.isEmpty()) return -1;
return this.queue[this.head];
}
// 返回队尾元素
Rear(): number {
if(this.isEmpty()) return -1;
// 因为尾指针始终指向的是队列最后一个元素的下一位,如果tail刚好为0时,tail-1就会出现负数的情况
// 为了解决这种情况,我们可以尾指针减一后,先加上一个数组的长度,然后再对接过与数组长度取余,
// 如:k=10, (0 - 1 + 10) % 10 = 9,因此,最后一个元素在数组里面的索引就是9
const idx = (this.tail - 1 + this.k) % this.k;
return this.queue[idx];
}
// 判断队列是否为空
isEmpty(): boolean {
return this.count === 0;
}
// 判断队列是否已满
isFull(): boolean {
return this.count === this.k;
}
}
双向循环队列
双向循环队列
就是在循环队列
的基础上支持既可以在头部或尾部入队
,也可以在头部或尾部出队
的特殊队列
双向循环队列的Typescript
版本
// leetcode [641] 设计循环双端队列
class MyCircularDeque {
private head: number = 0;
private tail: number = 0;
private count: number = 0;
private queue: number[];
constructor(private k: number) {
this.queue = new Array<number>(k);
}
// 在队首插入元素
insertFront(value: number): boolean {
if(this.isFull()) return false;
// 由于队首可能是有元素的,而队尾是没有元素的,所以
// 如果要在队首插入元素的话,需要让head左移一位(注意head为0的情况)
this.head = (this.head - 1 + this.k) % this.k;
this.queue[this.head] = value;
this.count++;
return true;
}
// 在队尾插入元素
insertLast(value: number): boolean {
if(this.isFull()) return false;
this.queue[this.tail] = value;
this.tail = (this.tail + 1) % this.k;
this.count++;
return true;
}
// 在队首删除元素
deleteFront(): boolean {
if(this.isEmpty()) return false;
this.head = (this.head + 1) % this.k;
this.count--;
return true;
}
// 在队尾删除元素
deleteLast(): boolean {
if(this.isEmpty()) return false;
this.tail = (this.tail - 1 + this.k) % this.k;
this.count--;
return true;
}
// 获取队首元素
getFront(): number {
if(this.isEmpty()) return -1;
return this.queue[this.head];
}
// 获取队尾与安娜苏
getRear(): number {
if(this.isEmpty()) return -1;
const idx = (this.tail - 1 + this.k) % this.k;
return this.queue[idx];
}
// 判断队列是否为空
isEmpty(): boolean {
return this.count === 0;
}
// 判断队列是否已满
isFull(): boolean {
return this.count === this.k;
}
}
前中后队列
前中后循环队列双向循环队列的基础上,再加了一个,可以从队列中间入队和出队
前中后队列的Typescript
版本
// leetcode: [1670] 设计前中后队列
// 使用双向链表的形式实现前中后队列
// 如:1->2->3->4
// 可以看成是:1->2 ---> 3->4
// 这两个链表串在一起,这样,我们想要往中间插入时,就只需要考虑到底是在链表1后面插入还是在链表2前面插入即可
// 双向链表链表节点对象
class Node {
constructor(public val=0,public prev: Node=null, public next: Node=null){}
// 在当前节点之前插入一个节点
insertPrev(node: Node) {
node.prev = this.prev;
node.next = this;
this.prev && (this.prev.next = node);
this.prev = node;
}
// 在当前节点后插入一个节点
insertNext(node: Node) {
node.next = this.next;
this.next && (this.next.prev = node)
this.next = node;
node.prev = this;
}
// 弹出当前节点的上一个节点
popPrev(): void {
if(!this.prev) return;
let p = this.prev;
this.prev = p.prev;
this.prev && (this.prev.next = this);
}
// 弹出当前节点的下一个节点
popNext(): void {
if(!this.next) return;
let p = this.next;
this.next = p.next;
this.next && (this.next.prev = this);
}
}
// 使用双向链表实现一个循环双端队列
class MyQueue {
// 因为是双端队列,可以从头部添加和删除元素,也可以从尾部添加或删除元素
// 因此需要定义头尾两个虚拟节点辅助我们操作这个链表
private head: Node = new Node();
private tail: Node = new Node();
private count: number = 0;// 用于记录队列中实际的元素数量,循环队列的关键
constructor(){
// 初始时,我们让头尾虚拟头相连即可
// head -> tail
// 我们需要从前面插入元素是,只需要在head节点后面插入元素
// 我们需要从后面插入元素时,只需要从tail节点前面插入元素
this.head.next = this.tail;
this.head.prev = null;
this.tail.next = null;
this.tail.prev = this.head;
}
// 在队列尾部插入元素
public pushBack(val: number) {
this.tail.insertPrev(new Node(val));
this.count++;
}
// 在队列头部插入元素
public pushFront(val: number) {
this.head.insertNext(new Node(val));
this.count++;
}
// 在队列的尾部删除元素
public popBack(): number {
if(this.isEmpty()) return -1;
let res = this.tail.prev.val;
this.tail.popPrev();
this.count--;
return res;
}
// 在队列的首部删除元素
public popFront(): number {
if(this.isEmpty()) return -1;
let res = this.head.next.val;
this.head.popNext();
this.count--;
return res;
}
// 获取队首元素
public front(): number{
return this.head.next.val;
}
// 获取对队尾元素
public back(): number{
return this.tail.prev.val;
}
// 队列元素珊瑚粮
public size(): number{
return this.count;
}
// 队列是否为空
public isEmpty(): boolean{
return this.head.next === this.tail;
}
}
class FrontMiddleBackQueue {
private q1: MyQueue;
private q2: MyQueue;
constructor() {
this.q1 = new MyQueue();
this.q2 = new MyQueue();
}
// 每一次添加或删除元素操作后,为了始终保持q1的元素数量始终大于或等于q2的元素数量,调用此方法进行修正
update(): void {
// 1 -> 2 -> 3 -> 4
// 始终确保q1的长度大于或等于q2,并且两者节点数量的差值最大为1
// 当q1数量小于q2时,从q2头部取出一个节点放在q1尾部
if(this.q1.size() < this.q2.size()) {
this.q1.pushBack(this.q2.popFront());
}
// 如果q2的数量比q1数量少两个时,从q1尾部拿一个出来放在q2头部
if(this.q2.size() === this.q1.size() - 2) {
this.q2.pushFront(this.q1.popBack());
}
}
// 在队首添加元素,就直接在q1上添加元素,然后修正两个队列即可
pushFront(val: number): void {
this.q1.pushFront(val);
this.update();
}
// 在队列中间添加元素,首先判断如果q1内的元素数量大于q2的话,就把元素放在q2头部,否则放在q1尾部
pushMiddle(val: number): void {
if(this.q1.size() > this.q2.size()) {
this.q2.pushFront(this.q1.popBack());
}
this.q1.pushBack(val);
this.update();
}
// 在队尾添加元素,直接在q2尾部添加元素即可,然后修正两个队列
pushBack(val: number): void {
this.q2.pushBack(val);
this.update();
}
// 从队首删除元素,在q1头部删除并修正两个队列接口
popFront(): number {
if(this.isEmpty()) return -1;
let res = this.q1.popFront();
this.update();
return res;
}
// 从队列中间删除元素,由于q1数量永远大于或等于q2数量,因此,我们只需要把q1的队尾元素删除即可
popMiddle(): number {
if(this.isEmpty()) return -1;
let res = this.q1.popBack();
this.update();
return res;
}
// 从队尾删除元素,由于有可能长出现q2为空的情况,因此,如果q2为空,则从q1的队首删除元素,否则从q2的队首删除
popBack(): number {
if(this.isEmpty()) return -1;
let res;
if(this.q2.isEmpty()){
res = this.q1.popBack();
} else{
res = this.q2.popBack();
}
this.update();
return res;
}
// 判断队列是否为空
isEmpty(): boolean {
return this.q1.size() === 0;
}
}
优先队列
我们都知道,普通队列
是一个严格遵循先进先出(FIFO)原则的数据结构,但是,在某些特殊场景,比如说我们的任务队列中,有一个优先级相当高的任务需要被优先执行,那么,这个时候就要插队
了,而支持这种插队操作的队列,我们把它叫做优先队列
,即:支持优先级的队列
队列的典型应用场景
CPU的超线程技术
CPU通过指令队列不断的处理输入的指令
虚拟四核本质上只有两个核心,只是增加了两个指令队列
1个CPU包含多个计算核心
线程池的任务队列
相当于任务的缓冲区,一般当前没空处理的时候,先放在队列里面等一会,有空了再从队列里面取
进程可以理解为是一个人
线程则是这个人要做的一些事
一个人可以同时做多件事,所以一个进程可以包含若干个线程
LeetCode刷题
LeetCode 933 最近的请求次数
解题思路
这题就是利用了队列的先进先出的原理实现的,这题比较简单,我们直接来看具体实现吧。
代码实现
/*
* @lc app=leetcode.cn id=933 lang=typescript
*
* [933] 最近的请求次数
*/
// @lc code=start
class RecentCounter {
// 使用一个数组模拟栈
private queue: number[];
constructor() {
// 初始化数组
this.queue = new Array<number>();
}
ping(t: number): number {
// 每次请求时将t加入队列
this.queue.push(t);
// 将所有时间大于3000的元素弹出队列
while(t - this.queue[0] > 3000) this.queue.shift();
// 最后剩下的数组的长度就是我们最近的请求次数
return this.queue.length;
}
}
/**
* Your RecentCounter object will be instantiated and called as such:
* var obj = new RecentCounter()
* var param_1 = obj.ping(t)
*/
// @lc code=end
leetcode 622 设计循环队列
代码解析请参考上面队列常见变种中的代码详解
leetcode 641 设计循环双端队列
代码解析请参考上面队列常见变种中的代码详解
leetcode: 1670 设计前中后队列
代码解析请参考上面队列常见变种中的代码详解