之前在学习算法的时候学到了队列的一种概念, 在看到 juejin.cn/post/684490… 等等许多篇文章中全部是用数组去实现的, 对于这样的文章表示很不满.
队列的要求是: 数据插入队尾: 时间复杂度 O(1), 移除头部数据: 时间复杂度 O(1). 而其余文章中所用数组去实现, 移除头部的方法是 Array.prototype.shift, 这个方法的时间复杂度为 O(n), 并不满足队列的要求.
下面是对 Array.prototype.shift, Array.prototype.pop 两个方法性能的测试:
- 测试 pop, 浏览器瞬间打印时间(约9.1ms).
// Array.prorotype.pop TEST:
const arr = [];
console.time();
for (let i = 0; i < 99999; i++) {
arr.push(i);
}
for (let i = 0; i < 99999; i++) {
arr.pop();
}
setTimeout(() => {
console.timeEnd();
}, 0); // default: 9.115234375ms
- 测试 shift, 浏览器瞬间打印时间(约432.9ms).
const arr = [];
console.time();
for (let i = 0; i < 99999; i++) {
arr.push(i);
}
for (let i = 0; i < 99999; i++) {
arr.shift();
}
setTimeout(() => {
console.timeEnd();
}, 0); // default: 432.95703125ms
可见时间差距还是很明显的, 但是时间都比较短, 为了避免误测, 扩大效果, 把测试数据改为 999999. 结论为:
- pop: default: 24.708251953125ms.(浏览器瞬间出现打印效果)
- shift: default: 46803.153076171875ms. (浏览器加载了很久才出现)
造成上述的原因是, 定义数组, 实际上是计算机给我们开辟了连续的一个储存空间, 如果删除末尾的元素, 直接删除即可, 如果删除头部的元素, 后面所有的元素都要往前挪一位, 来保证下标 index 相同. (详细的可以自行 google 或者百度).
所以为了保证队列的操作时间, 我们不能用数组实现队列, 那么如何实现呢? 虽然 JavaScript 中没有自己封装的队列, 但是 java 中有, 所以通过 google 搜索 java queue source code: introcs.cs.princeton.edu/java/43stac…
所以 Queue 的本质是用链表实现的, 下面就仿照 java 这段代码对链表进行实现:
class Queue {
constructor() {
this.length = 0; // 队列的长度.
this.first = { next: null, item: null };; // 队列的头部.
this.last = { next: null, item: null }; // 队列的尾部.
}
}
生成一个基本的队列结构, 用 next 指针把队列中的各个数据串起来, 用 item 表示队列的数据. 一个队列有下面几个方法: size(获取长度), isEmpty(是否为空), peek(查询队列头部的数据), enqueue(把数据压入队尾), dequeue(移出队列头部的数据). 我们从简到复实现上述方法.
size() { // 获取队列长度.
return this.length;
}
isEmpty() { // 判断队列是否为空.
return this.length === 0;
}
peek() { // 查询队列头部的数据.
if (this.isEmpty()) throw new Error('queue is empty!');
return this.first.item;
}
这几个方法都比较简单, 不在多说. 入队也是比较简单的, 就是把尾结点的 item 值设置成进入队列的值, next 设置成 { item: null, next: null }, 在把 last 指针指向 last.next 即可. 如果队列为空, 那么队列头部指针指向队列尾部.
enqueue(item) {
this.last.item = item; // 尾部的 item 值设置.
this.last.next = { item: null, next: null }; // next 指针设置为 { item: null, next: null }
if (this.isEmpty()) this.first = this.last; // 如果队列为空, 头指针指向尾指针.
this.last = this.last.next; // 把尾指针指向尾指针的 next 属性.
this.length++;
}
对于指针指向不熟悉的同学, 可以对链表多做练习, 我自己刚开始也是绕来绕去. 出队也比较简单, 就把头指针指向他的 next 属性就好了.
dequeue() {
if (this.isEmpty()) throw new Error('queue is empty!');
const { item } = this.first;
this.first = this.first.next; // 头指针指向自己的 next 属性.
this.length--;
return item; // 返回出队的数据.
}
至此, 队列的基本方法已经完成了. 我们可以使用 const queue = new Queue(); 来使用, 如果想初始化头部怎么办? 很简单, 只需要对构造函数进行改动即可:
-- constructor() {
++ constructor(data) {
this.length = 0;
this.last = { next: null, item: null };
this.first = { next: null, item: null };
++ if (data) this.enqueue(data);
}
这时我们可以用 const queue = new Queue('data'); 来初始化队头.
完整代码:
class Queue {
constructor(data) {
this.length = 0;
this.last = { next: null, item: null };
this.first = { next: null, item: null };
if (data) this.enqueue(data);
}
size() {
return this.length;
}
isEmpty() {
return this.length === 0;
}
peek() {
if (this.isEmpty()) throw new Error('queue is empty!');
return this.first.item;
}
enqueue(item) {
this.last.item = item;
this.last.next = { item: null, next: null };
if (this.isEmpty()) this.first = this.last;
this.last = this.last.next;
this.length++;
}
dequeue() {
if (this.isEmpty()) throw new Error('queue is empty!');
const { item } = this.first;
this.first = this.first.next;
this.length--;
return item;
}
}
我在 从上到下打印二叉树 使用了自己封装的 Queue, 代码通过, 说明 enqueue 方法和 dequeue 方法是正确的, 时间复杂度方面我也分析过了, 是优与数组的, 至此, 队列的基本内容已经完成.
这是我写的第一篇文章, 难免会出现表述不清, 排序不够好的情况, 希望各位读者能够多多包容.
另因个人水平有限, 如果读者有更好的方法, 希望能够指出.