队列基本概念
维基百科:队列,又称为伫列(queue),计算机科学中的一种抽象资料型别,是先进先出(FIFO, First-In-First-Out)的线性表。在具体应用中通常用链表或者数组来实现。队列只允许在后端(称为rear)进行插入操作,在前端(称为front)进行删除操作。队列的操作方式和堆栈类似,唯一的区别在于队列只允许新数据在后端进行添加。
(单向)队列原理说明图
(单向)队列的基本操作
| 操作名 | 定义方法 | 方法说明 |
|---|---|---|
| 入队列 | enqueue(element) | 向队列尾部添加一个(或多个)新的元素 |
| 出队列 | dequeue() | 移除排在最前面的一个元素 |
| 查看队列头 | peek() | 查看最前面一个元素 |
| 是否为空 | isEmpty() | 检查队列是否还有元素 |
| 清空队列 | clear() | 清空队列 |
| 元素个数 | size() | 返回队列中有几个元素 |
用对象实现队列
class Queue {
constructor() {
this.front = 0;
this.rear = 0;
this.items = {};
}
enqueue(element) {
this.items[this.rear] = element;
this.rear++
}
dequeue() {
if (this.isEmpty()) {
return undefined;
}
let result = this.items[this.front];
delete this.items[this.front];
this.front++;
return result;
}
isEmpty() {
return this.rear - this.front === 0;
}
peek() {
return this.items[this.front];
}
size() {
return this.rear - this.front;
}
clear() {
this.front = 0;
this.rear = 0;
this.items = {};
}
}
队列的工作过程
在实现队列的时候,用于存储队列中元素的数据结构,可以使用数组,就像上一章的 Stack 类那样。但是,为了写出一个在获取元素时更高效的数据结构,上面用的是items = {}对象来存储队列的元素。(JavaScript中的对象是散列表的数据结构,散列表具有一步查找的优势,而数组的查找是O(n),数组只是一步访问)。当然,在功能实现上,数组和对象都是可以的。在实现操作队列的时候,有几点需要注意:
- 设置队列头和尾:front,rear
- 当front与rear重合,rear-front即表示队列元素的个数,队列就是空的,
- 入队的时候,入一个东西,front不变,rear后移一位
- 出队的手,移除一个东西,front后移一位,rear不变
进队、出队例子
let queue = new Queue();
console.log(queue.isEmpty()); //新建立的队列为空
queue.enqueue('John');
queue.enqueue('Jack');
queue.enqueue('Camila');
console.log(queue.size()); // 输出 3
// John,Jack,Camila
console.log(queue.isEmpty()); // 输出 false
queue.dequeue(); // 移除 John
//Jack,Camila
queue.dequeue(); // 移除 Jack
//Camila
console.log(queue.peek()); // Camila
双向队列基本概念
双端队列(deque,或称 double-ended queue)是一种允许我们同时从前端和后端添加和移除 元素的特殊队列。
在计算机科学中,双端队列的一个常见应用是存储一系列的撤销操作。每当用户在软件中进 行了一个操作,该操作会被存在一个双端队列中(就像在一个栈里)。当用户点击撤销按钮时, 该操作会被从双端队列中弹出,表示它被从后面移除了。在进行了预先定义的一定数量的操作后, 最先进行的操作会被从双端队列的前端移除。由于双端队列同时遵守了先进先出和后进先出原 则,可以说它是把队列和栈相结合的一种数据结构
双向队列原理说明图
双向队列的基本操作
由于双向队列的头和尾都可以插入和移除元素,所以在入队和出队的时候,头和尾要分开,但基本操作和单向对象是类似的。
| 操作名 | 定义方法 | 方法说明 |
|---|---|---|
| 头部入队列 | addFront(element) | 该方法在双端队列前端添加新的元素 |
| 尾部入队列 | addRear(element) | 实现方法和 Queue 类中的enqueue 方法相同 |
| 头部出队列 | removeFront() | 实现方法和 Queue 类中的dequeue 方法相同 |
| 尾部出队列 | removeRear() | 实现方法和 Stack 类中的pop 方法一样 |
| 查看队列头 | peekFront() | 实现方法和 Queue 类中的 peek方法一样 |
| 查看队列尾 | peekRear() | 实现方法和 Stack 类中的 peek方法一样 |
| 是否为空 | isEmpty() | 检查队列是否还有元素 |
| 清空队列 | clear() | 清空队列 |
| 元素个数 | size() | 返回队列中有几个元素 |
(双向)队列的基本操作
双向队列的实现
class Deque {
constructor() {
this.front = 0;
this.rear = 0;
this.items = {};
}
addFront(element) {
this.front--;
this.items[this.front] = element;
}
addRear(element) {
this.items[this.rear] = element;
this.rear++;
}
removeFront() {
if (this.isEmpty()) {
return undefined;
}
let result = this.items[this.front];
delete this.items[this.front];
this.front++;
return result;
}
removeRear() {
if (this.isEmpty()) {
return undefined;
}
let result = this.items[this.rear - 1];
delete this.items[this.rear - 1];
this.rear--;
return result;
}
isEmpty() {
return this.rear - this.front === 0;
}
peekFront() {
return this.isEmpty() ? undefined : this.items[this.front];
}
peekRear() {
return this.isEmpty() ? undefined : this.items[this.rear - 1];
}
clear() {
this.front = 0;
this.rear = 0;
this.items = 0;
}
size() {
return this.rear - this.front;
}
//另外加一个toSting(),这样就能看到队列里有哪些元素,这个操作不是必须的,因为封装好的队列应该是看不到里面的元素的。
toString() {
let result = "队列元素";
for (let i = this.front; i < this.rear; i++) {
result = result + "," + this.items[i];
}
return result;
}
}
在实例化 Deque 类后,我们可以执行下面的方法:
双向队列的操作过程
const deque = new Deque();
console.log(deque.isEmpty()); // 输出 true
deque.addRear('John');
deque.addRear('Jack');
console.log(deque.toString()); // John, Jack
deque.addRear('Camila');
console.log(deque.toString()); // John, Jack, Camila
console.log(deque.size()); // 输出 3
console.log(deque.isEmpty()); // 输出 false
deque.removeFront(); // 移除 John
console.log(deque.toString()); // Jack, Camila
deque.removeRear(); // Camila 决定离开
console.log(deque.toString()); // Jack
deque.addFront('John'); // John 回来询问一些信息
console.log(deque.toString()); // John, Jack
借助 Deque 类,我们可以执行 Stack 和 Queue 类中的操作。我们同样可以使用 Deque 类来实现一个优先队列。
双向队列使用例子
回文检查器:回文是正反都能读通的单词、词组、数或一系列字符的序列,例如 madam或 racecar。
有不同的算法可以检查一个词组或字符串是否为回文。最简单的方式是将字符串反向排列并 检查它和原字符串是否相同。如果两者相同,那么它就是一个回文。我们也可以用栈来完成,但 是利用数据结构来解决这个问题的最简单方法是使用双端队列。
//传入字符串最为参数
function cycleString(sentence) {
let deque = new Deque();
let isEqual = true;
//全部改成小写,去空格,把字符串变成数组
const senArr = sentence.toLocaleLowerCase().split(" ").join('');
for (let i = 0; i < senArr.length; i++) {
deque.addRear(senArr[i]);
}
while (deque.size() > 1 && isEqual) {
let front = deque.removeFront();
let rear = deque.removeRear();
if (front !== rear) {
isEqual = false;
}
}
return isEqual;
}
首先将参数填入双向队列中。每次,从队列的首和尾各取一个字,如果首尾是相同的,则继续从两边各取一个字,直到整个队列的长度为1或者0个。如果其中有任何一对首尾不同的话,就将结果isEqual改成false,否认就一直是true。
//验证举例:
let result1 = cycleString('hello');
let result2 = cycleString('abcdcba');
console.log(result1, result2); //false, true
循环队列
百度百科:为充分利用向量空间,克服”假溢出“现象的方法是:将向量空间想象为一个首尾相接的圆环,并称这种向量为循环向量。存储在其中的队列称为循环队列(Circular Queue)。循环队列是把顺序队列首尾相连,把存储队列元素的表从逻辑上看成一个环,成为循环队列。
这其中的一种叫作循环队列。循环队列的一个例子就是击鼓传花游戏(hot potato)。在这个游戏中,孩子们围成一个圆圈,把花尽快地传递给旁边的人。某一时刻传花停止, 这个时候花在谁手里,谁就退出圆圈、结束游戏。重复这个过程,直到只剩一个孩子(胜者)
//两个参数:人员名单数组,每次传几次淘汰一个人
function hotPotato(people, number) {
const queue = new Queue();
const lostPeople = [];
//用参数填充队列
for (let i = 0; i < people.length; i++) {
queue.enqueue(people[i])
}
while (queue.size() > 1) {
for (let i = 0; i < number; i++) {
//把队头的元素移到队尾
queue.enqueue(queue.dequeue());
}
//从队列中取出队尾的一个元素,放到失败列表中
lostPeople.push(queue.dequeue());
}
return {
lostPeople,
winner: queue.dequeue()
}
}
const names = ['John', 'Jack', 'Camila', 'Ingrid', 'Carl'];
const result = hotPotato(names, 7);
console.log(result)
//{ lostPeople: [ 'Camila', 'Jack', 'Carl', 'Ingrid' ], winner: 'John' }
即获胜者为Jon
循环队列输出过程
参考资料
- 书籍:学习JavaScript数据结构与算法第三版