ES6封装队列并支持容量和动态扩容

161 阅读4分钟

队列:先进先出的数据结构

队列是一种遵循“先进先出”(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缓存效率和随机访问速度时。尽管数组在扩容时会有一定的性能损耗,但在实际应用中,合理的初始容量和扩容策略可以有效地减轻这一问题。