阅读提示:文章大概字数15300,较详细的介绍了队列在js中的应用和作用,实现了几个常见算法用队列的代码
queue队列的应用与实现
前言
队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为空队列。
队列的数据元素又称为队列元素。在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。因为队列只允许在一端插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出(FIFO—first in first out)线性表。
提示:以下是本篇文章正文内容,下面案例可供参考
一、queue(队列)是什么?
1.生活中的队列
实际生活中排队的例子
2.程序中的队列
像是这样
或者是这样
观图有感:看完这几个图是不是感觉程序很贴生活,工艺设计的灵感大多来源于对生活的观察,通过反复多次的应用和验证之后再便利于生活中,就是这么奇妙。
二、各种类型的队列
1.基础队列
1.1 队列里的方法
- enqueue(element):向队列尾部添加新项。
- dequeue():移除队列的第一项,并返回被移除的元素。
- front():返回队列中第一个元素,队列不做任何变动。
- isEmpty():如果队列中不包含任何元素,返回 true,否则返回 false。
- size():返回队列包含的元素个数,与数组的 length 属性类似。
- print():打印队列中的元素。
- clear():清空整个队列。
1.2 队列类创建
提示:
javaScript里面没有像Java 、C#哪些强类型编程语言那样,有自己内置的队列数据结构类型, 在javaScript 中我们一般以Array 数组的封装队列类或者队列构造函数。
class Queue {
constructor() {
this.items = []
}
// 向队列尾部添加元素
enqueue(element) {
this.items.push(element)
}
// 移除队列的第一个元素,并返回被移除的元素
dequeue() {
return this.items.shift()
}
// 返回队列的第一个元素
front() {
return this.items[0]
}
// 判断是否为空队列
isEmpty() {
return this.items.length === 0
}
// 获取队列的长度
size() {
return this.items.length
}
// 清空队列
clear() {
this.items = []
}
// 打印队列里的元素
print() {
console.log(this.items.toString())
}
}
1.3 用队列来将十进制转化为二进制
function generatePrintBinary(n) {
let q = new Queue()
q.enqueue('1')
while (n-- > 0) {
let s1 = q.front()
q.dequeue()
console.log(s1)
let s2 = s1
q.enqueue(s1 + '0')
q.enqueue(s2 + '1')
}
}
generatePrintBinary(5) // => 1 10 11 100 101
1.4 构造函数的方式来创建队列
代码如下(示例):
// Queue类
function Queue() {
this.items = [];
// 向队列尾部添加元素
this.enqueue = function(element) {
this.items.push(element);
};
// 移除队列的第一个元素,并返回被移除的元素
this.dequeue = function() {
return this.items.shift();
};
// 返回队列的第一个元素
this.front = function() {
return this.items[0];
};
// 判断是否为空队列
this.isEmpty = function() {
return this.items.length === 0;
};
// 获取队列的长度
this.size = function() {
return this.items.length;
};
// 清空队列
this.clear = function() {
this.items = [];
};
// 打印队列里的元素
this.print = function() {
console.log(this.items.toString());
};
}
1.5 创建示例对象测试一下
说明:
以类的方式创建还是以构造函数的方式实现都可以用如下测试实例代码
// 创建Queue实例
let queue = new Queue();
console.log(queue.isEmpty()); // true
queue.enqueue('亚里士多德');
queue.enqueue('柏拉图');
queue.enqueue('苏格拉底');
queue.print(); // "亚里士多德,柏拉图,苏格拉底"
console.log(queue.size()); // 3
console.log(queue.isEmpty()); // false
queue.dequeue();
queue.dequeue();
queue.print(); // "苏格拉底"
queue.clear();
console.log(queue.size()); // 0
说明:
后面的代码我们以类的方式来实现
提示:创建队列不仅可以使用数组为数据结构基地还可以使用对象作为数据结构基地
- 像这样子,定义类,然后里面的方法想想这么实现
class Queue {
constructor() {
this.count = 0; // {1} 控制队列的大小
this.lowestCount = 0; // {2}踪第一个元素
this.items = {}; // {3} items 对象来存储的元素
}
}
2.最小优先队列
2.1 与基础队列的区别
2.2 代码实现
代码如下(示例):
class MinPriorityQueue {
constructor() {
this.items = [];
}
// 优先队列添加元素,要根据优先级判断在队列中的插入顺序
enqueue(element, priority) {
let queueElement = {
element: element,
priority: priority
};
if (this.isEmpty()) {
this.items.push(queueElement);
} else {
let added = false;
for (let i = 0; i < this.size(); i++) {
if (queueElement.priority < this.items[i].priority) {
this.items.splice(i, 0, queueElement);
added = true;
break;
}
}
if (!added) {
this.items.push(queueElement);
}
}
}
// 移除队列的第一个元素,并返回被移除的元素
dequeue() {
return this.items.shift();
};
// 返回队列的第一个元素
front() {
return this.items[0];
};
// 判断是否为空队列
isEmpty() {
return this.items.length === 0;
};
// 获取队列的长度
size() {
return this.items.length;
};
// 清空队列
clear() {
this.items = [];
};
// 打印队列里的元素
print() {
let strArr = [];
strArr = this.items.map(function (item) {
return `${item.element}->${item.priority}`;
});
console.log(strArr.toString());
}
}
上面入队方法添加了一下优先级的判断,用到js数组的splice 方法
关于splice用法的详细的教程
splice() 方法通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。此方法会改变原数组。
使用方法:
- 感受一下
const months = ['Jan', 'March', 'April', 'June'];
months.splice(1, 0, 'Feb');
// inserts at index 1
console.log(months);
// expected output: Array ["Jan", "Feb", "March", "April", "June"]
months.splice(4, 1, 'May');
// replaces 1 element at index 4
console.log(months);
// expected output: Array ["Jan", "Feb", "March", "April", "May"]
- 从索引 2 的位置开始删除 1 个元素,插入“trumpet”
var myFish = ['angel', 'clown', 'drum', 'sturgeon'];
var removed = myFish.splice(2, 1, "trumpet");
// 运算后的 myFish: ["angel", "clown", "trumpet", "sturgeon"]
// 被删除的元素: ["drum"]
- 从索引 0 的位置开始删除 2 个元素,插入"parrot"、“anemone"和"blue”
var myFish = ['angel', 'clown', 'trumpet', 'sturgeon'];
var removed = myFish.splice(0, 2, 'parrot', 'anemone', 'blue');
// 运算后的 myFish: ["parrot", "anemone", "blue", "trumpet", "sturgeon"]
// 被删除的元素: ["angel", "clown"]
2.3 实例测试示例
// 创建最小优先队列minPriorityQueue实例
let minPriorityQueue = new MinPriorityQueue();
console.log(minPriorityQueue.isEmpty()); // true
minPriorityQueue.enqueue("John", 1);
minPriorityQueue.enqueue("Jack", 3);
minPriorityQueue.enqueue("Camila", 2);
minPriorityQueue.enqueue("Tom", 3);
minPriorityQueue.print(); // John->1,Camila->2,Jack->3,Tom->3
console.log(minPriorityQueue.size()); // 4
console.log(minPriorityQueue.isEmpty()); // false
minPriorityQueue.dequeue();
minPriorityQueue.dequeue();
minPriorityQueue.print(); // Jack->3,Tom->3
minPriorityQueue.clear();
console.log(minPriorityQueue.size()); // 0
输出:
3.最大优先队列
3.1 与基础队列的区别
- 入队方法
enqueue(element, priority) {
let queueElement = {
element: element,
priority: priority
};
if (this.isEmpty()) {
this.items.push(queueElement);
} else {
let added = false;
for (let i = 0; i < this.size(); i++) {
if (queueElement.priority > this.items[i].priority) {
this.items.splice(i, 0, queueElement);
added = true;
break;
}
}
if (!added) {
this.items.push(queueElement);
}
}
}
- 打印队列的方法
print() {
let strArr = [];
strArr = this.items.map(function (item) {
return `${item.element}->${item.priority}`;
});
console.log(strArr.toString());
}
最大优先队列和最小优先队列的区别就只是优先级的判断不同
if (queueElement.priority < this.items[i].priority) //最小优先队列
if (queueElement.priority > this.items[i].priority) //最大优先队列
完整代码如下(示例):
// Queue类
class MaxPriorityQueue {
constructor() {
this.items = [];
}
// 优先队列添加元素,要根据优先级判断在队列中的插入顺序
enqueue(element, priority) {
let queueElement = {
element: element,
priority: priority
};
if (this.isEmpty()) {
this.items.push(queueElement);
} else {
let added = false;
for (let i = 0; i < this.size(); i++) {
if (queueElement.priority > this.items[i].priority) {
this.items.splice(i, 0, queueElement);
added = true;
break;
}
}
if (!added) {
this.items.push(queueElement);
}
}
}
// 移除队列的第一个元素,并返回被移除的元素
dequeue() {
return this.items.shift();
};
// 返回队列的第一个元素
front() {
return this.items[0];
};
// 判断是否为空队列
isEmpty() {
return this.items.length === 0;
};
// 获取队列的长度
size() {
return this.items.length;
};
// 清空队列
clear() {
this.items = [];
};
// 打印队列里的元素
print() {
let strArr = [];
strArr = this.items.map(function (item) {
return `${item.element}->${item.priority}`;
});
console.log(strArr.toString());
}
}
3.2 实例测试示例
// 创建最大优先队列maxPriorityQueue实例
let maxPriorityQueue = new MaxPriorityQueue();
console.log(maxPriorityQueue.isEmpty()); // true
maxPriorityQueue.enqueue("John", 1);
maxPriorityQueue.enqueue("Jack", 3);
maxPriorityQueue.enqueue("Camila", 2);
maxPriorityQueue.enqueue("Tom", 3);
maxPriorityQueue.print(); // "Jack->3,Tom->3,Camila->2,John->1"
console.log(maxPriorityQueue.size()); // 4
console.log(maxPriorityQueue.isEmpty()); // false
maxPriorityQueue.dequeue(); // {element: "Jack", priority: 3}
maxPriorityQueue.dequeue(); // {element: "Tom", priority: 3}
maxPriorityQueue.print(); // "Camila->2,John->1"
maxPriorityQueue.clear();
console.log(maxPriorityQueue.size()); // 0
输出:
4.循环队列
4.1 介绍
在实际使用队列时,为了使队列空间能重复使用,往往对队列的使用方法稍加改进:无论插入或删除,一旦rear指针增1或front指针增1 时超出了所分配的队列空间,就让它指向这片连续空间的起始位置。
自己真从 MaxSize-1 增 1 变到 0,可用取余运算 rear % MaxSize 和front % MaxSize 来实现。这实际上是把队列空间想象成一个环形空间,环形空间中的存储单元循环使用,用这种方法管理的队列也就称为循环队列。除了一些简单应用之外,真正实用的队列是循环队列。
4.2 循环队列之击鼓传花游戏 (Hot Potato)
- 下面用到的 new Queue() 类为我们上面定义的第一个基础队列类
// 实现击鼓传花
// 实现击鼓传花
function hotPotato (nameList, num) {
let queue = new Queue();
// 参与游戏的选手入队
for (let i = 0; i < nameList.length; i++) {
queue.enqueue(nameList[i]);
}
// 记录被淘汰的参与者
let eliminated = '';
let n = Math.floor(Math.random()*num + 1)
while (queue.size() > 1) {
// 循环 1 到 num 次,队首出来去到队尾
for (let i = 0; i < n; i++) {
queue.enqueue(queue.dequeue());
}
// 循环1 到 num 次过后,移除当前队首的元素
eliminated = queue.dequeue();
console.log(`${eliminated} 在击鼓传花中被淘汰!`);
}
// 最后只剩一个元素
return queue.dequeue();
}
- 测试数据
// 测试
let nameList = ["孙悟空", "唐僧", "沙僧", "白龙马", "猪八戒",'猪八戒他二姨'];
let winner = hotPotato(nameList, 10);
console.log(`最后的胜利者是:${winner}`);
- 结果
说明:这里入参为名称列表namelist和要循环的最高次数num
这样每次循环得到的就是一个随机的操作,不会存在暗箱操作的嫌疑。
5.双端队列数据结构
5.1 介绍和双端队列类代码
双端队列(deque,或称 double-ended queue)是一种允许我们同时从前端和后端添加和移除
元素的特殊队列。双端队列在现实生活中的例子有电影院、餐厅中排队的队伍等。举个例子,一个刚买了票的人如果只是还需要再问一些简单的信息,就可以直接回到队伍的头部。另外,在队伍末尾的人如果赶时间,他可以直接离开队伍。
在计算机科学中,双端队列的一个常见应用是存储一系列的撤销操作。每当用户在软件中进行了一个操作,该操作会被存在一个双端队列中(就像在一个栈里)。当用户点击撤销按钮时,该操作会被从双端队列中弹出,表示它被从后面移除了。在进行了预先定义的一定数量的操作后,最先进行的操作会被从双端队列的前端移除。由于双端队列同时遵守了先进先出和后进先出原则,可以说它是把队列和栈相结合的一种数据结构
介绍: 这里用上面说 的 以对象方式有存储元素,双端队列中在头部添加元素会多了一点逻辑,还有加上的了lowestCount 用来追踪(头部)第一个元素,和count 来计数,不是以数组长度的方式来计数,所以会看起来麻烦了一些。
- 双端队列类
class Deque {
constructor() {
this.items = {} // {1} 用一个对象来存储元素
this.count = 0 // {2} 控制队列的大小
this.lowestCount = 0 // {3} 用来追踪(头部)第一个元素
}
// 在队列头添加元素
addFront(ele) {
if (this.isEmpty()) { // {1} 第一种场景是这个双端队列是空的
this.items[this.count] = ele
} else if (this.lowestCount > 0) { // {2} 第二种场景是一个元素已经被从双端队列的前端移除
this.lowestCount -= 1
this.items[this.lowestCount] = ele
} else {
for (let i = this.count; i > 0; i--) { // {3} 将所有元素后移一位
this.items[i] = this.items[i - 1]
}
this.items[0] = ele // {4} 用需要添加的新元素来覆盖它
}
this.count++
return ele
}
// 移除队列头的元素
removeFront() {
if (this.isEmpty()) {
return
}
const delEle = this.items[this.lowestCount]
delete this.items[this.lowestCount] // delete为JavaScript 删除一个对象的关键字api
this.lowestCount++
return delEle
}
// 在队列尾部添加元素
addBack(ele) {
this.items[this.count] = ele
this.count++
}
// 移除队列尾部的元素
removeBack() {
if (this.isEmpty()) {
return
}
const delEle = this.items[this.count - 1]
delete this.items[this.count - 1]
this.count--
return delEle
}
// 获取双端队列头部的第一个元素
peekFront() {
if (this.isEmpty()) {
return
}
return this.items[this.lowestCount]
}
// 获取双端队列尾部的第一个元素
peekBack() {
if (this.isEmpty()) {
return
}
return this.items[this.count - 1]
}
size() {
return this.count - this.lowestCount
}
isEmpty() {
return this.size() === 0
}
clear() {
this.items = {}
this.count = 0
this.lowestCount = 0
}
toString() {
if (this.isEmpty()) {
return ''
}
let objString = `${this.items[this.lowestCount]}`
for (let i = this.lowestCount + 1; i < this.count; i++) {
objString = `${objString}, ${this.items[i]}`
}
return objString
}
}
5.2 相关算法与代码实现 -回文检查器
- 解释: 回文是正反都能读通的单词、词组、数或一系列字符的序列,例如madam或racecar。
有不同的算法可以检查一个词组或字符串是否为回文。最简单的方式是将字符串反向排列并检查它和原字符串是否相同。如果两者相同,那么它就是一个回文。我们也可以用栈来完成,但是利用数据结构来解决这个问题的最简单方法是使用双端队列。
- 回文检查器 代码
function palindromeChecker(str) {
// 判断输入条件是否为空、是否为字符串类型
if (!str || typeof str !== 'string' || !str.trim().length) {
return false
}
const deque = new Deque()
// 转化为小写
const lowerString = str.toLowerCase().split(' ').join('')
// 每个字符加入队列
for (let i = 0; i < lowerString.length; i++) {
deque.addBack(lowerString[i])
}
let isEqual = true
let firstChar = ''
let lastChar = ''
while (deque.size() > 1 && isEqual) {
firstChar = deque.removeFront()
lastChar = deque.removeBack()
// 两端一起往中间推,依次判断是否回文
if (firstChar != lastChar) {
isEqual = false
}
}
return isEqual
}
- 测试
let a = 'ABXBA'
let b = 'fddf'
let c = 'abcdefg'
console.log(palindromeChecker(a)) // true 当前为回文
console.log(palindromeChecker(b)) // true 当前为回文
console.log(palindromeChecker(c)) // false 当前不为回文
- 结果
6.并发队列
说明:
并发队列对计算机底层的程序设计比较重要,底层涉及到的并发队列比较多,前端的队列……
7.阻塞队列
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。
这两个附加的操作是:
- 在队列为空时,获取元素的线程会等待队列变为非空。
- 当队列满时,存储元素的线程会等待队列可用。
三、算法中的队列数据结构应用
1.LeetCode 算法题中的队列
1.1 933最近的请求次数
阅读理解
这个题晦涩
题目意思就是往队列里存放元素啊,每次ping就存一个数,然后存的时候看一下之前的数是否在t-3000之外了,是就删除;
比如题目给的例子:
输入:
[“RecentCounter”, “ping”, “ping”, “ping”, “ping”]
[[], [1], [100], [3001], [3002]]
输出:
[null, 1, 2, 3, 3]
就是第一次存个1,第二次存个100……, 然后每次存的时候,看看之前存的元素,有多少个在这次存的 t 到t-3000之间;
存1的时候,1在[-2999,1]之间,就这一个数,所以说返回数目1;
……
存3002的时候就删除1,因为1不在[2,3002]之间,而100,3001,3002都是在[2,3002]之间,就返回数目3;
解题思路
- 越早发出的请求越早不在最近3000ms 内的请求里
- 满足先进先出,用队列
解题步骤
- 有新请求就入队, 3000ms 前发出的请求出队
- 队列长度就是最近发请求次数
Coding Part
var RecentCounter = function() {
// 给构造函数绑定一个队列
this.queue = [];
}
/**
* @param {number} t
* @return {number}
*/
RecentCounter.prototype.ping = function(t) {
this.queue.push(t)
while (this.queue < t - 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)
*/
let obj = new RecentCounter()
let param_1 = obj.ping(3001)
console.log(param_1);
复杂度分析
- 时间复杂度:O(Q)O(Q),其中 QQ 是 ping 的次数。
- 空间复杂度:O(W)O(W),其中 W = 3000W=3000 是队列中最多存储的 ping 的记录数目。
2.其他面试题中的队列
2.1 异步面试题(任务队列)
思考下面的打印结果:
setTimeout(() => {
console.log(1)
}, 0);
console.log(2);
复制代码之间放到浏览器按F12打开控制台,查看一下吧!
- 像这样子
- 按 Enter健查看结果
2.2 异步队列和事件循环的原理
来看看js异步队列先吧
- 程序代码在底层中的执行逻辑,如图:
- 更具体化美化的表现形式:
- 有时候看图会更后理解,相互之间的关系,
这里是Google 搜索 Event Loop 之后的图片结果,看看图,谷歌访问不了就会打不开链接噢!
解析说明
对上图,分为(A)左边的调用栈,(B)右边 异步解析 (C)底边的任务队列
- (C)任务队列中的任务会排队到(A)调用栈中去执行
- (A)调用栈处理任务时遇到同步的程序代码就把它给编译执行了,遇到异步的代码程序,就把他扔给了(B)进行异步解析
- B)异步解析出异步的回调、计时器、HTTP等异步的操作,
- 又回到了(C)任务队列中,这个事件循环异步队列过程中就像是被一层一层的脱去,异步的衣服,所以上面2.1 异步面试题会输出 2、1,因为setTimeout是异步的给仍一边去了。
- 同理在复杂的异步程序里也都遵循这个原理,要理清顺序一层一层的脱,就能脱的明白。执行也就直观了。
总结
提示:这里对文章进行总结:
例如:以上就是今天要讲的内容,本文简单介绍了队列在JS中的使用和实际程序中的应用。