前端学数据结构之队列

前面的话

  队列和非常类似,但是使用了不同的原则,而非后进先出。本文将详细介绍队列的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代码,处理用户交互(用户输入、鼠标点击等),执行和处理异步请求

 

posted @ 2018-01-02 10:35 小火柴的蓝色理想 阅读(...) 评论(...) 编辑 收藏