实现JavaScript基本数据结构系列---队列

163 阅读4分钟

这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

队列

队列和栈非常类似,但它采用的是先进先出(FIFO)的原则的一组有序的项。队列在尾部添加元素,在头部删除元素,最新添加的元素必须在队列的末尾。最常见的例子就是大家买饭时要排队,或者是打印文档的例子,先来先服务。

功能

你会发现Queue类和Stack类非常相似,只是添加和删除元素的方式不同,那我们用什么数据结构来存储队列中的元素呢?我们可以使用数组,和实现Stack一样,为了获取数据时更高效,我们使用对象。来看看我们要实现哪些方法吧,大家也可以根据这些方法想想该怎么实现,再来阅读一下代码。

  1. enqueue(ele): 像队列尾部添加一个或多个新元素
  2. dequeue(): 移除队列第一项,并返回被移除元素
  3. peek(): 返回队列第一个元素
  4. isEmpty(): 队列是否为空
  5. size(): 返回队列中包含元素个数
/**
 *  队列
 *  1. enqueue(ele): 像队列尾部添加一个或多个新元素
    2. dequeue(): 移除队列第一项,并返回被移除元素
    3. peek(): 返回队列第一个元素
    4. isEmpty(): 队列是否为空
    5. size(): 返回队列中包含元素个数
 */

class Queue {
    constructor() {
        this._queue = {};
        this._count = 0; // 队列长度,-1 是队尾元素位置
        this._lowestCount = 0; // 队首元素位置
    }

    enqueue(ele) {
        this._queue[this._count] = ele;
        this._count++
    }

    dequeue() {
        if (this.isEmpty()) return undefined;
        const result = this._queue[this._lowestCount];
        delete this._queue[this._lowestCount];
        this._lowestCount++;
        return result;
    }

    peek() {
        if (this.isEmpty()) return undefined;
        
        return this._queue[this._lowestCount];
    }

    isEmpty() {
        return  this._count - this._lowestCount === 0;
    }

    size() {
        return this._count - this._lowestCount;
    }
}

以上就是实现一个队列的基本方式,除了这个队列还有其他的嘛?接下来我们来看看双端队列和循环队列吧

双端队列

双端队列是一种允许我们同时从前后两端添加和移除元素的特殊队列,常见的例子有回文检查器、文件撤销操作等,他同时遵守了先进先出和后进先出的原则,所以可以说他是把队列和栈相结合的一种数据结构。 在双端队列和普通队列中,大多方法基本一致,在这只实现了addFront方法,其他方法可以自己写一写。

class Deque {
    constructor() {
        this._queue = {};
        this._count = 0; // 队列长度,-1 是队尾元素
        this._lowestCount = 0; // 队首元素
    }

    // 队首添加元素
    addFront(ele) {
        // 当没有元素时,在前面添加和队尾一样
        if (this.isEmpty()) {
            this.addBack(ele);
        } else {
            if (this._lowestCount > 0) {
                // 队首元素发生了变化,比如删除过时,说明队列前还可以插于
                this._lowestCount--;
                this._queue[this._lowestCount] = ele;
            } else {
                // 队列前面咩有位置了,依次向后移动
                for (let i = this._count; i > 0; i--) {
                    this._queue[i] = this._queue[i - 1];
                }
                this._count++;
                this._lowestCount = 0;
                this._queue[this._lowestCount] = ele;
            }
        }
    }

    // 队尾添加元素
    addBack(ele) {
        this._queue[this._count] = ele;
        this._count++
    }
    
    isEmpty() {
        return this._count - this._lowestCount === 0;
    }

    size() {
        return this._count - this._lowestCount;
    }
}

应用场景举例:如何验证一个字符串是否是回文呢?第一种方式是将字符串反转,对比原字符串是否相等;第二种是使用双端队列来实现,思路如下:

  1. 判断输入的字符串是否合法
  2. 清洗字符串特殊字符,如空格、?等需要去除的字符
  3. 将字符串遍历,依次添加进队列中
  4. 当队列长度大于1时,循环执行:同时取出队首、队尾元素,进行比较,两者不想等,返回false结束循环
  5. 循环没有被提前终止,说明是回文,返回true

循坏队列 ----- 击鼓传花

在最后还可以介绍一下循坏队列 ----- 击鼓传花,循环队列又是队列的另一个应用。

/** 
 * 击鼓传花
 * 场景:孩子们围成一个圈,将花尽快的传递给旁边的人,某一时刻花在谁手里了,谁就被淘汰掉。重复该过程至最后一个人
 */

const hotPotato = (eles, num) => {
    const queue = [];
    const failList = [];

    eles.forEach(ele => queue.push(ele)); // 添加进队列

    // 开始玩游戏
    while(queue.length > 1) {
        // 在数数,花从第一个传到第二个,依次往下,可以想像成把第一个人放到最后
        for (let i = 0; i < num; i++) {
            queue.push(queue.shift())
        }
        // 进行完一轮,此时花在谁手里(第一个)就淘汰
        failList.push(queue.shift());

        // 继续循环
    }

    return {
        failList,
        winner: queue.pop()
    }
}

const names = ['John', 'Jack', 'Camile', 'Ingrid', 'Carl'];
const result = hotPotato(names, 7);
console.log(result.winner); // John