• 前端学数据结构之队列


    前面的话

      队列和非常类似,但是使用了不同的原则,而非后进先出。本文将详细介绍队列的JS实现

    数据结构

      队列是遵循FIFO(First In First Out,先进先出,也称为先来先服务)原则的一组有序的项。 队列在尾部添加新元素,并从顶部移除元素。最新添加的元素必须排在队列的末尾

      在现实中,最常见的队列的例子就是排队:

    queue1

      还有,在电影院、自助餐厅、杂货店收银台,我们也都会排队。排在第一位的人会先接受服务

      在计算机科学中,一个常见的例子就是打印队列。比如说我们需要打印五份文档。我们会打开每个文档,然后点击打印按钮。每个文档都会被发送至打印队列。第一个发送到打印队列的文档会首先被打印,以此类推,直到打印完所有文档

    创建队列

      我们需要创建自己的类来表示一个队列。先从最基本的声明类开始:

    function Queue() {
     //这里是属性和方法
    } 

      首先需要一个用于存储队列中元素的数据结构。可以使用数组,就像在上一篇博文Stack类中那样使用(Queue类和Stack类非常类似,只是添加和移除元素的原则不同):

    let items = [];

      接下来需要声明一些队列可用的方法

    enqueue(element(s)):向队列尾部添加一个(或多个)新的项。
    dequeue():移除队列的第一(即排在队列最前面的)项,并返回被移除的元素。
    front():返回队列中第一个元素——最先被添加,也将是最先被移除的元素。队列不 做任何变动(不移除元素,只返回元素信息——与Stack类的peek方法非常类似)。
    isEmpty():如果队列中不包含任何元素,返回true,否则返回false。
    size():返回队列包含的元素个数,与数组的length属性类似。

    【enqueue】

      首先要实现的是enqueue方法。这个方法负责向队列添加新元素。这里有一个非常重要的细节,新的项只能添加到队列末尾:

      既然我们使用数组来存储队列的元素,就可以用JS的array类的push方法

    this.enqueue = function(element){ 
      items.push(element);
    };

    【dequeue】

      接下来要实现dequeue方法。这个方法负责从队列移除项。由于队列遵循先进先出原则,最先添加的项也是最先被移除的。可以用JavaScript的array类的shift方法。shift方法会从数组中移除存储在索引0(第一个位置)的元素

    this.dequeue = function(){ 
      return items.shift();
    };

      只有enqueue方法和dequeue方法可以添加和移除元素,这样就确保了Queue类遵循先进先出原则

    【front】

      现在来为我们的类实现一些额外的辅助方法。如果想知道队列最前面的项是什么,可以用front方法。这个方法会返回队列最前面的项(数组的索引为0):

    this.front = function(){ 
      return items[0];
    };

    【isEmpty】

      如果队列为空,isEmpty方法返回true,否则返回false

      对于isEmpty方法,可以简单地验证内部数组的length是否为0。

    this.isEmpty = function(){ 
      return items.length == 0;
    };

    【size】

      可以为Queue类实现类似于array类的length属性的方法。size方法也跟Stack类里的一样:

    this.size = function(){ 
      return items.length;
    };

    【print】

      增加一个print方法:

    this.print = function(){ 
      console.log(items.toString());
    };

      Queue类的完整代码如下

    function Queue() {
    
        let items = [];
    
        this.enqueue = function(element){
            items.push(element);
        };
    
        this.dequeue = function(){
            return items.shift();
        };
    
        this.front = function(){
            return items[0];
        };
    
        this.isEmpty = function(){
            return items.length == 0;
        };
    
        this.clear = function(){
            items = [];
        };
    
        this.size = function(){
            return items.length;
        };
    
        this.print = function(){
            console.log(items.toString());
        };
    }

    使用Queue类

      首先要做的是实例化我们刚刚创建的Queue类,然后就可以验证它为空(输出为true,因为我们还没有向队列添加任何元素):

    let queue = new Queue(); 
    console.log(queue.isEmpty()); //输出true

      接下来,添加一些元素(可以向队列添加任何类型的元素):

    queue.enqueue("John"); 
    queue.enqueue("Jack");

      添加另一个元素:

    queue.enqueue("Camila");

      再执行一些其他的命令:

    queue.print(); 
    console.log(queue.size()); //输出3 
    console.log(queue.isEmpty()); //输出false
    queue.dequeue(); 
    queue.dequeue(); 
    queue.print();

      如果打印队列的内容,就会得到John、Jack和Camila这三个元素。因为我们向队列添加了三个元素,所以队列的大小为3(当然也就不为空了)

      下图展示了目前为止执行的所有入列操作,以及队列当前的状态:

    queue2

      然后,出列两个元素(执行两次dequeue方法)。下图展示了dequeue方法的执行过程:

    queue3

      最后,再次打印队列内容时,就只剩Camila一个元素了。前两个入列的元素出列了,最后入列的元素也将是最后出列的。也就是说,我们遵循了先进先出原则

    ES6

      我们也可以用ECMAScript 6语法编写Queue类。在这种方法中,我们要用一个WeakMap来保存私有属性items,并用外层函数(闭包)来封装Queue类。

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

    优先队列

      队列大量应用在计算机科学以及我们的生活中,实现的默认队列也有一些修改版本。其中一个修改版就是优先队列。元素的添加和移除是基于优先级的。一个现实的例子就是机场登机的顺序。头等舱和商务舱乘客的优先级要高于经济舱乘客。在有些国家,老年人和孕妇(或带小孩的妇女)登机时也享有高于其他乘客的优先级

      另一个现实中的例子是医院的(急诊科)候诊室。医生会优先处理病情比较严重的患者。通常,护士会鉴别分类,根据患者病情的严重程度放号。

      实现一个优先队列,有两种选项:设置优先级,然后在正确的位置添加元素;或者用入列操作添加元素,然后按照优先级移除它们。在这个示例中,我们将会在正确的位置添加元素,因此可以对它们使用默认的出列操作:

    function PriorityQueue() {
     let items = [];
     function QueueElement (element, priority){ // {1}
      this.element = element;
      this.priority = priority;
     }
     this.enqueue = function(element, priority){
      let queueElement = new QueueElement(element, priority);
      let added = false;
      for (let i=0; i<items.length; i++){
        if (queueElement.priority < items[i].priority){ // {2}
          items.splice(i,0,queueElement); // {3}
          added = true;
          break; // {4}
        }
      }
      if (!added){
        items.push(queueElement); //{5}
      }
     };
     this.print = function(){
      for (let i=0; i<items.length; i++){
        console.log(`${items[i].element} - ${items[i].priority}`);
      }
     };
     //其他方法和默认的Queue实现相同
    }

      默认的Queue类和PriorityQueue类实现上的区别是,要向PriorityQueue添加元素,需要创建一个特殊的元素(行{1})。这个元素包含了要添加元素,需要创建一个特殊的元素(行{1})。这个元素包含了要添加到队列的元素(它可以是任意类型)及其在队列中的优先级

      如果队列为空,可以直接将元素入列(行{2})。否则,就需要比较该元素与其他元素的优先级。当找到一个比要添加的元素的priority值更大(优先级更低)的项时,就把新元素插入到它之前(根据这个逻辑,对于其他优先级相同,但是先添加到队列的元素,我们同样遵循先进先出的原则)。要做到这一点,我们可以用JavaScript的array类的splice方法。 一旦找到priority值更大的元素,就插入新元素(行{3})并终止队列循环(行{4})。这样, 队列也就根据优先级排序了

      如果要添加元素的priority值大于任何已有的元素,把它添加到队列的末尾就行了(行{5}):

    var priorityQueue = new PriorityQueue(); 
    priorityQueue.enqueue("John", 2);
    priorityQueue.enqueue("Jack", 1);
    priorityQueue.enqueue("Camila", 1); 
    priorityQueue.print();

      以上代码是一个使用PriorityQueue类的示例。在下图中可以看到每条命令的结果(以上代码的结果)

     queue4

      第一个被添加的元素是优先级为2的John。因为此前队列为空,所以它是队列中唯一的元素。接下来,添加了优先级为1的Jack。由于Jack的优先级高于John,它就成了队列中的第一个元素。然后,添加了优先级也为1的Camila。Camila的优先级和Jack相同,所以它会被插入到Jack之后(因为Jack先被插入队列);Camila的优先级高于John,所以它会被插入到John之前

      我们在这里实现的优先队列称为最小优先队列,因为优先级的值较小的元素被放置在队列最前面(1代表更高的优先级)。最大优先队列则与之相,把优先级的值较大的元素放置在队列最前面

    循环队列

      还有另一个修改版的队列实现,就是循环队列。循环队列的一个例子就是击鼓传花游戏(Hot Potato)。在这个游戏中,孩子们围成一个圆圈,把花尽快地传递给旁边的人。某一时刻传花停止, 这个时候花在谁手里,谁就退出圆圈结束游戏。重复这个过程,直到只剩一个孩子(胜者)

      在下面这个示例中,我们要实现一个模拟的击鼓传花游戏:

    function hotPotato (nameList, num){ 
      var queue = new Queue(); // {1}
      for (var i=0; i<nameList.length; i++){
        queue.enqueue(nameList[i]); // {2}
      }
      var eliminated = ''; 
      while (queue.size() > 1){
        for (var i=0; i<num; i++){ 
          queue.enqueue(queue.dequeue()); // {3}
        }
        eliminated = queue.dequeue();// {4} 
        console.log(eliminated + '在击鼓传花游戏中被淘汰。');
      }
      return queue.dequeue();// {5}
    }
    
    var names = ['John','Jack','Camila','Ingrid','Carl']; 
    var winner = hotPotato(names, 7);
    console.log('胜利者:' + winner);

      实现一个模拟的击鼓传花游戏,要用到Queue类(行{1})。我们会得到一份名单,把里面的名字全都加入队列(行{2})。给定一个数字,然后迭代队列。从队列开头移除一项,再将其添加到队列末尾(行{3}),模拟击鼓传花(如果把花传给了旁边的人,被淘汰的威胁立刻就解除了)。一旦传递次数达到给定的数字,拿着花的那个人就被淘汰了(从队列中移除——行{4})。最后只剩下一个人的时候,这个人就是胜者(行{5})

      以上算法的输出如下:

    Camila在击鼓传花游戏中被淘汰。 
    Jack在击鼓传花游戏中被淘汰。
    Carl在击鼓传花游戏中被淘汰。 
    Ingrid在击鼓传花游戏中被淘汰。 
    胜利者:John

      下图模拟了这个输出过程:

    queue5

      可以改变传入hotPotato函数的数字,模拟不同的场景

    最后

      JavaScript内部控制所使用的也是队列如此基础的数据结构

      当我们在浏览器中打开新标签时,就会创建一个任务队列。这是因为每个标签都是单线程处理所有的任务,它被称为事件循环。浏览器要负责多个任务,如渲染HTML,执行JavaScript代码,处理用户交互(用户输入、鼠标点击等),执行和处理异步请求

  • 相关阅读:
    ibatis中isEquals、isNotEmpty的用法
    truncate与delete 、drop的区别
    javaweb学习总结二十二(servlet开发中常见的问题汇总)
    ORACLE时间函数(SYSDATE)深入理解
    大数据
    javaweb学习总结二十一(servlet开发入门、servlet生命周期以及调用过程)
    javaweb学习总结二十(http响应)
    javaweb学习总结十九(http协议概述以及http请求信息分析)
    Telnet客户端连接服务器,看不见字符,只显示横线
    Eclipse打JAR包的使用
  • 原文地址:https://www.cnblogs.com/xiaohuochai/p/8174854.html
Copyright © 2020-2023  润新知