JavaScript 数据结构学习笔记 (二) 栈与队列

159 阅读4分钟

JavaScript 数据结构学习笔记 (二) 栈与队列

什么是栈

  • 我们在学习严老的《数据结构》时,知道栈是一种遵循 后进先出(LIFO) 原则的有序集合;新加入的或者等待被删除的元素都保存在栈的同一端,称之为栈顶,另一端就叫做栈底。
  • 生活中有些例子就很符合栈这种数据结构,例如叠盘子。
  • 栈更是编译器实现的重要的数据结构

创建栈

  • 我们知道,js中的数组非常灵活,它也拥有非常多的方法,用来实现栈和队列这样的结构非常简易。
  • 代码如下:
function Stack() {
    let items = [];
    // 向栈顶添加元素
    this.Push = function (element) {
        items.push(element);
    }
    //从栈顶移除元素并返回该元素
    this.Pop = function () {
        return items.pop();
    }
    //返回栈顶元素,但是并不删除它。
    this.Peek = function () {
        return items[items.length-1];
    }
    //判断栈是否为空
    this.IsEmpty = function () {
        return items.length == 0;
    }
    //返回栈的长度
    this.Size = function () {
        return items.length;
    }
    //栈置空
    this.Clear = function () {
        items = [];
    }
    //打印栈
    this.Print = function () {
        console.log(items.toString());
    }
}

ES6改进function Stack

  • 我们知道,原生js只能仿照类的行为。所以在上文的代码中,我们声明了一个私有的items变量,它只能被Stack函数访问,这个方法会为每一个实例都创建一个item变量的副本。所以,当我们创建多个实例的时候,就会造成内存损耗冗长,那么就不太合适了。

ES6的限定作用域Symbol()实现类

先来看一下代码:

let _items = Symbol();
class Stack {
    constructor(){
        this[_items] = [];
    }
    push(ele){
        this[_items].push(ele);
    }
    pop(){
        return this[_items].pop();
    }
    peek(){
        return this[_items][this[_items].length-1];
    }
    IsEmpty() {
        return this[_items].length == 0;
    }
    Size() {
        return this[_items].length;
    }
    Clear() {
        this[_items] = [];
    }
    Print() {
        console.log(this[_items].toString());
    }
    toString(){
        return this[_items].toString();
    };
}
  • 可以看到,通过ES6语法,我们把Stack函数转化成了Stack类。但js于C++,Java不同,他不能直接在类里声明变量。需要在类的构造函数constructor()里声明,在类的其他函数里通过this.变量名来引用该变量。
  • 尽管如此,items仍是公共的,无法声明私有变量或者方法,于是我们采用Symbol。
  • Symbol作为ES6新加入的基本类型,他是不可变的
  • 在上面的代码段中,我们用Symbol类型声明了变量_items,在constructor里初始化它的值,如果要访问它,只需要把所有的this.变量名换成[this.变量名]就行了

队列

什么是队列

  • 与栈很像,队列是一种遵循 先进先出(FIFO) 原则的一组有序集合,队列在队尾添加元素,在队头移除元素
  • 生活中最典型的就是排队了,所以才叫queue嘛哈哈

创建队列

上面在栈的创建时已经介绍了ES6的Symbol方法,现在我们来使用WeakMap来保存私有属性items,并用闭包函数来封装Queue类

  • 代码如下:
let Queue2 = (function(){
    const items = new WeakMap();
    class Queue2 {
        constructor(){
            items.set(this,[]);
        }
        enqueue(ele){
            let q = items.get(this);
            q.push(ele);
        }
        dequeue(){
            let q = items.get(this);
            let r = q.shift();
            return r;
        }
        front(){
            let q = items.get(this);
            let r = q[0];
            return r;
        }
        isEmpty = function(){
            let q = items.get(this);
            let r = q.length;
            return r == 0;
        }
        size = function(){
            let q = items.get(this);
            return q.length;
        }
        print = function(){
            let q = items.get(this);
            console.log(q.toString());
        }

    }
    return Queue2;
})();

let queue2 = new Queue2();
console.log(queue2.isEmpty());

queue2.enqueue("John");
queue2.enqueue("Jack");
queue2.enqueue("camlls");
queue2.print();//John,Jack,camlls
console.log(queue2.size());//3
console.log(queue2.isEmpty());//false
  • 之所以用这样的方式来实现,是希望尽可能的向面向对象的原则上靠,即只给用户我们想让他们用到的。

优先队列

优先队列的原则:

  • [1] 如果队列为空,直接将元素入列。
  • [2] 比较该元素与其他元素的优先级,当找到一个比该元素的priority值大(优先级更低)的项时,把它插到新元素之前
  • [3] 如果优先级相同,则先进先出。

优先队列的创建:

  • 创建优先队列有两种方法:

      1.设置优先级,在正确的位置添加元素
      2.用入列操作添加元素,然后按照优先级移除它们
    
  • 我选择使用第一种办法:

function PriorityQueue(){
    let items = [];
    function QueueElement (ele,priority){
        this.ele = ele;
        this.priority = priority;
    }
    this.enqueue = function(ele,priority){
        let queueElement = new QueueElement(ele,priority);
        let added = false;
        for(let i=0;i<items.length;i++){
            if(queueElement.priority<items[i].priority){
                items.splice(i,0,queueElement);
                added=false;
                break;
            }
        }
        if(!added){
            items.push(queueElement);
        }
    }
    this.print = function(){
        for(let i=0;i<items.length;i++){
            console.log(`${items[i].ele} - ${items[i].priority}`);
        }
    }
    this.dequeue = function(){
        return items.shift();
    }
    this.front = function(){
        return items[0];
    }
    this.isEmpty = function(){
        return items.length == 0;
    }
    this.size = function(){
        return items.length;
    }   
}
let pQ = new PriorityQueue();
pQ.enqueue("John",2);
pQ.enqueue("Jack",1);
pQ.enqueue("Camila",1);
pQ.print();

结果: pri

循环队列-击鼓传花

  • 循环队列的原理其实很简单:就是队头出来了又回到队尾去排队
function hotPotato (nameList, num){
    let queue = new Queue();

    for (let i=0;i<nameList.length;i++){
        queue.enqueue(nameList[i]);
    }

    let eliminated = '';
    while (queue.size()>1) {
        for (let i=0;i<num;i++){
        //队头出来对尾插
            queue.enqueue(queue.dequeue());
        }
        eliminated = queue.dequeue();
        console.log(eliminated + '在击鼓传花中被淘汰。');
    }
    return queue.dequeue();
}
let names = ['John','Jack','Camila','Ingrid','Carl'];
let winner = hotPotato(names,7);
console.log('the winner is'+winner);
  • 结果如下: bass

小结

可以看到,基于js数组如此灵活的方法以及ES6语法的类的实现,像栈和队列这样稍微基础的数据结构类型可以相对简易的实现,也容易我们理解