队列是遵循先进先出(FIFO,也称为先来先服务)原则的一组有序的项。
队列在尾部添加新元素,并从顶部移除元素。最新添加的元素必须排在队列的末尾。
class Queue {
constructor() {
this.count = 0; // {1}
this.lowestCount = 0; // {2}
this.items = {}; // {3}
}
}
为了写出一个在获取元素时更高效的数据结构,我们将使用一个对象来存储我们 的元素(行{3})。
你会发现 Queue 类和 Stack 类(栈)非常类似,只是添加和移除元素的原则不同。
队列的常用方法:
enqueue(element(s)):向队列尾部添加一个(或多个)新的项。
dequeue():移除队列的第一项(即排在队列最前面的项)并返回被移除的元素。
peek():返回队列中第一个元素——最先被添加,也将是最先被移除的元素。队列不做
任何变动(不移除元素,只返回元素信息——与 Stack 类的 peek 方法非常类似)。
该方 0 法在其他语言中也可以叫作 front 方 法。
isEmpty():如果队列中不包含任何元素,返回 true,否则返回 false。
size():返回队列包含的元素个数,与数组的 length 属性类似。
具体实现方法这里不再实现,具体参考《学习javascript数据结构和算法》
这里介绍一个移除方法dequeue()
dequeue() {
if (this.isEmpty()) {
return undefined;
}
const result = this.items[this.lowestCount]; // {1}
delete this.items[this.lowestCount]; // {2}
this.lowestCount++; // {3}
return result; // {4}
}
由于queue采用count作为key值,所以要访问到每一个元素就要知道其key值,对于队列最前端的元素每次移除后,lowestCount要指向下一个key值,所以要+1
双端队列(deque,或称 double-ended queue)是一种允许我们同时从前端和后端添加和移除元素的特殊队列。
在计算机科学中,双端队列的一个常见应用是存储一系列的撤销操作。每当用户在软件中进 行了一个操作,该操作会被存在一个双端队列中(就像在一个栈里)。当用户点击撤销按钮时, 该操作会被从双端队列中弹出,表示它被从后面移除了。在进行了预先定义的一定数量的操作后, 最先进行的操作会被从双端队列的前端移除。由于双端队列同时遵守了先进先出和后进先出原 则,可以说它是把队列和栈相结合的一种数据结构。
class Deque {
constructor() {
this.count = 0;
this.lowestCount = 0;
this.items = {};
}
}
既然双端队列是一种特殊的队列,我们可以看到其构造函数中的部分代码和队列相同,包括 相同的内部属性和以下方法:isEmpty、clear、size 和 toString。
由于双端队列允许在两端添加和移除元素,还会有下面几个方法。
addFront(element):该方法在双端队列前端添加新的元素。
addBack(element):该方法在双端队列后端添加新的元素(实现方法和 Queue 类中的 enqueue 方法相同)。
removeFront():该方法会从双端队列前端移除第一个元素(实现方法和 Queue 类中的 dequeue 方法相同)。
removeBack():该方法会从双端队列后端移除第一个元素(实现方法和 Stack 类中的 pop 方法一样)。
peekFront():该方法返回双端队列前端的第一个元素(实现方法和 Queue 类中的 peek 方法一样)。
peekBack():该方法返回双端队列后端的第一个元素(实现方法和 Stack 类中的 peek 方法一样)。
这里介绍一个双端队列添加元素方法:
addFront(element) {
if (this.isEmpty()) { // {1}
this.addBack(element);
} else if (this.lowestCount > 0) { // {2}
this.lowestCount--;
this.items[this.lowestCount] = element;
} else {
for (let i = this.count; i > 0; i--) { // {3}
this.items[i] = this.items[i - 1];
}
this.count++;
this.lowestCount = 0;
this.items[0] = element; // {4}
}
}
说明:要将一个元素添加到双端队列的前端,存在三种场景。
第一种场景是这个双端队列是空的(行{1})。在这种情况下,我们可以执行 addBack 方法。 元素会被添加到双端队列的后端,在本例中也是双端队列的前端。addBack 方法已经有了增加 count 属性值的逻辑,因此我们可以复用它来避免重复编写代码。
第二种场景是一个元素已经被从双端队列的前端移除(行{2}),也就是说 lowestCount 属 性会大于等于 1。这种情况下,我们只需要将 lowestCount 属性减 1 并将新元素的值放在这个
第三种也是最后一种场景是 lowestCount 为 0 的情况。我们可以设置一个负值的键,同时 更新用于计算双端队列长度的逻辑,使其也能包含负键值。这种情况下,添加一个新元素的操作 仍然能保持最低的计算成本。为了便于演示,我们把本场景看作使用数组。要在第一位添加一个 新元素,我们需要将所有元素后移一位(行{3})来空出第一个位置。由于我们不想丢失任何已 有的值,需要从最后一位开始迭代所有的值,并为元素赋上索引值减 1 位置的值。在所有的元素 7 都完成移动后,第一位将是空闲状态,这样就可以用需要添加的新元素来覆盖它了(行{4})
实例:
循环队列——击鼓传花游戏
由于队列经常被应用在计算机领域和我们的现实生活中,就出现了一些队列的修改版本,我 们会在本章实现它们。这其中的一种叫作循环队列。循环队列的一个例子就是击鼓传花游戏(hot potato)。在这个游戏中,孩子们围成一个圆圈,把花尽快地传递给旁边的人。某一时刻传花停止, 这个时候花在谁手里,谁就退出圆圈、结束游戏。重复这个过程,直到只剩一个孩子(胜者)。
function hotPotato(elementsList, num) {
const queue = new Queue(); // {1}
const elimitatedList = [];
for (let i = 0; i < elementsList.length; i++) {
queue.enqueue(elementsList[i]); // {2}
}
while (queue.size() > 1) {
for (let i = 0; i < num; i++) {
queue.enqueue(queue.dequeue()); // {3}
}
elimitatedList.push(queue.dequeue()); // {4}
}
return {
eliminated: elimitatedList,
winner: queue.dequeue() // {5}
};
}
还有一种实例是回文判断,原理是使用双端队列,判断从队首和队尾移除的元素是否相等
当我们在浏览器中打开新标签时,就会创建一个任务队列。
这是因为每个标签都是单线程处 理所有的任务,称为事件循环。
浏览器要负责多个任务,如渲染 HTML、执行 JavaScript 代码、 处理用户交互(用户输入、鼠标点击等)、执行和处理异步请求。如果想更多地了解事件循环, 可以访问 https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/。