【3】js队列的实现-JavaScript学习数据结构

198 阅读6分钟

队列基本概念

维基百科:队列,又称为伫列(queue),计算机科学中的一种抽象资料型别,是先进先出(FIFO, First-In-First-Out)的线性表。在具体应用中通常用链表或者数组来实现。队列只允许在后端(称为rear)进行插入操作,在前端(称为front)进行删除操作。队列的操作方式和堆栈类似,唯一的区别在于队列只允许新数据在后端进行添加。

image.png

(单向)队列原理说明图

(单向)队列的基本操作

操作名定义方法方法说明
入队列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 类后,我们可以执行下面的方法:

image.png

双向队列的操作过程

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

image.png

循环队列输出过程

参考资料