队列:先进先出的数据结构
队列是一种遵循“先进先出”(FIFO, First-In-First-Out)原则的线性数据结构,这意味着最早添加到队列中的元素将是最先被移除的。队列在许多应用场景中都有重要作用,比如任务调度、打印队列管理等。
操作限制
在标准队列实现中,通常只允许两种主要操作:
push(入队):向队列尾部添加新元素。pop(出队):从队列头部移除元素。
此外,还有一些辅助操作:
peek:查看队头元素但不移除它。isEmpty:检查队列是否为空。size:返回队列中元素的数量。
封装的重要性
封装是面向对象编程的一个重要特性,它允许我们将数据和方法捆绑在一起,并隐藏内部实现细节。对于队列而言,封装可以带来以下好处:
- 复用:良好的封装使得代码可以在不同的项目或上下文中轻松复用。
- 保护私有状态:通过定义私有变量(例如,在ES6类中使用
#前缀),我们可以确保外部代码无法直接修改队列的状态,从而维护队列的一致性和可靠性。
使用ES6封装队列并支持容量和动态扩容
class AutoExpandArrayQueue {
#nums;// 数组 私有属性 es6 语法
#font = 0;// 对头 为什么要设计对头? 内存优化
#queSize = 0;// 队列长度
constructor(capacity) {
// 分配了capacity单位个连续的内存空间
//这段内存就在缓存当中
this.#nums = new Array(capacity);
}
//获得队列容量
get capacity(){
return this.#nums.length;
}
//获得队列长度
get size(){
return this.#queSize;
}
//判断队列是否为空
isEmpty(){
return this.#queSize === 0;
}
//入队
push(num){
if(this.size === this.capacity) {
this.#expandCapacity();
}
const rear = (this.#font + this.size) % this.capacity;
this.#nums[rear] = num;
this.#queSize++;
}
//出队
pop() {
const num = this.peek();
this.#font = (this.#font + 1) % this.capacity;
this.#queSize--;
return num;
}
//取队头
peek(){
if(this.isEmpty()) return 'undefined'
return this.#nums[this.#font];
}
//扩容
#expandCapacity(){
//不能干掉别人的
//重新分配内存 搬运工作
const newCapacity = this.capacity * 2;
const newNums = new Array(newCapacity);
for(let i = 0; i < this.size; i++){
newNums[i] = this.#nums[(this.#font + i) % this.capacity]
}
this.#nums = newNums;
this.#front = 0;
}
}
这段代码实现了具有自动扩容能力的循环队列,并且利用了ES6的私有属性来保护内部状态。当队列满时,会创建一个两倍大小的新数组,并将现有元素复制过去,同时重置对头指针#front为0。
数组分配连续内存空间的意义
数组在内存中分配一段连续的空间有几个优点:
- CPU缓存友好:由于现代CPU采用多级缓存机制,当访问数组中的某个元素时,CPU会预先加载该元素周围的相邻元素到缓存中。因此,如果接下来需要访问附近的元素,可以直接从快速缓存中读取,而不是再次访问较慢的主内存。
- 提高性能:连续内存块的访问模式更符合CPU预取器的工作原理,有助于减少页面错误并加快程序执行速度。
- 简化索引计算:由于每个元素的位置是固定的偏移量,这使得通过下标直接访问元素变得非常高效。
数组实现队列 vs. 链表实现队列
-
数组实现的优点:
- 更快的随机访问:由于元素存储在连续的内存块中,查找特定位置的元素比链表快。
- 更好的缓存局部性:正如前面提到的,连续内存块更适合CPU缓存,提高了整体性能。
- 较低的内存开销:不需要额外存储指向下一个节点的引用。
然而,数组实现有一个明显的缺点是在达到容量上限时需要进行O(n)的时间复杂度的拷贝操作,但这可以通过适当的选择初始容量和扩容策略来缓解。平均来看,这种成本是可以接受的,特别是在大多数情况下不会频繁触及容量上限的情况下。
-
链表实现的优点:
- 灵活的插入和删除:在链表中插入或删除元素只需改变指针,而不需要移动其他元素。
- 无需提前知道最大容量:链表可以根据需要动态增长,不需要预先确定最大长度。
但是,链表也有一些劣势,比如较差的缓存性能和较高的内存消耗(每个节点都需要额外的指针)。因此,选择哪种实现方式取决于具体的应用场景和需求。
总结
本文探讨了队列的基本概念、操作限制以及如何使用ES6语法封装一个具备自动扩容功能的队列。我们还讨论了为什么数组在某些方面优于链表作为队列的底层实现,尤其是在考虑CPU缓存效率和随机访问速度时。尽管数组在扩容时会有一定的性能损耗,但在实际应用中,合理的初始容量和扩容策略可以有效地减轻这一问题。