第四章 队列
作者: Loiane Groner
我们已经学习了栈的运作原理。除了队列是遵循先进先出原则外,队列在其他方面与栈是非常相似的。
队列是有一些列遵守先进先出原则的成员组成的。新添加的元素都被放置在队列的尾部,而在删除元素时则从队列的顶部删除。最新的元素会被放置在队列的尾部。
队列结构在现实生活中最贴切的例子就是人们排队买票了:
当我们在排队时,队伍最前面的是最先来排队的那个人。
在计算机科学中,队列结构的实例是印刷流水线。假设我们要用打印机打印五个文件,我们要先打开每个文件,并点击打印按钮。每个文件都会被送到印刷流水线,第一个被点击印刷按钮的文件将会是第一个被打印,之后按照被点击打印按钮的顺序依次打印。
生成一个队列
我们将手写一个队列结构。让我们从最基础的声明开始:
function Queue() {
//此处为定义属性和方法的代码
}
首先,我们需要一个变量来存储栈结构的成员。我们可以使用数组来达到这一目的,就像上一章用数组来存储栈的成员一样:
var items =[];
接下来,我们要声明队列的常用方法:
- enqueue(element(s) ):它会在队列的尾部增加一个成员。
- dequeue( ):它会删除队列的一个成员,并返回被删除的成员。
- front( ):它会返回队列的第一个成员素,也就是队列中最早被添加的成员。这个方法不会改变原数组。
- isEmpty( ):如队列没有成员,它会返回true;反之则返回false。
- size( ):它会返回队列成员的数量。这个方法和数组的length属性很像。
我们第一个要添加的是enqueue方法。这个方法是用来给队列添加元素的:我们只能在队列的尾部添加元素。enqueue方法是这样写的:
this.enqueue = function(element){
items.push(element);
};
我们使用数组来存储队列的成员,所以我们可以使用JavaScript给数组定义的push方法。
接下来,我们要添加的是dequeue方法。这个方法是用来给队列删除成员的。因为队列遵循FIFO原则,即最早被添加的元素也是最先被删除的元素。因此,我们可以使用JavaScript定义的数组的shift方法。shift方法的工作原理是,删除下标为0的成员:
this.dequeue = function() {
return items.shift()
};
有了enqueue 作为唯一的增加成员方法和dequeue作为唯一的删除成员的方法,队列结构就可以很好的遵守FIFO原则了。
现在让我们为队列结构添加帮手函数。如果我们想要查看最早被添加到队列的成员,我们可以使用front方法。这个方法将会返回队列的第一个成员:
this.front = function() {
return items[0];
};
下一个方法为isEmpty方法。当队列为空时,isEmpty会返回true,反之则返回false:
this.isEmpty = function ( ) {
return items.length === 0;
};
有了isEmpty方法,我们可以轻松的判断队列的内部数组的长度是否为0。
与数组类型的length属性相似,我们可以为队列结构添加length属性。当然,我们通常使用的是‘size’而非 ‘length’来表示队列的长度。因为我们用内部数组来存储队列的成员,我们用函数来返回内部数组的长度即可:
this.size = function ( ) {
return items.length;
};
我们现在已经完成了一个队列结构。和栈结构一样,我们可以定义print方法如下来查看队列:
this.print = function () {
console.log( items.toString() );
};
就这样完成了!!
完成队列结构
让我们看看完整的栈数据结构的代码:
function Queue() {
var 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.size = function () {
return items.length;
};
this.print = function () {
console.log( items.toString() );
};
}
Note
队列结构和栈结构非常相似。唯一不同的就是,队列因为遵守FIFO原则而采用了enqueue方法和dequeue方法,而栈因为遵守LIFO原则而采用了push和pop方法。
使用队列结构
在我们深入栈结构的实例之前,我们需要知道如何使用栈结构。
首先我们要知道如何初始化一个栈结构,然后我们可以查看它是否为空:
var queue = new Queue();
console.log( stack.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因为队列有3个成员。
下图展示了前两行代码的执行过程:
之后,我们用了两次dequeue方法来删除队列前面的成员:
之后我们再次查看队列的内容,此时队列只有成员Camila。队列的前两个成员已经被推出了,此时原先在队列最后面的成员Camila成为队列的最前面的成员。也就是说,队列遵守了FIFO原则。
优先队列
队列被广泛的应用在计算机科学和我们的生活中。接下来为大家介绍一下改进版的队列。
优先队列是队列结构的一种改进版本。队列会基于成员的优先级进行增加和删除。优先队列在现实生活中的例子是在登机时,头等舱和商务舱的乘客在选择座位上,相较于经济舱的乘客有优先权。在一些国家,老人和孕妇在乘车时也有坐座位的优先权。
另一个例子是人们在医院(急诊部)里接受问诊的次序。病情严重的病人比病情没那么严重的人更早被医生问诊。通常,护士会对病人们的患病情况进行分别,并为其病患情况进行分级。
有两种方法可以实现一个优先队列:你可以设置优先级并把新成员放在正确的位置,或者依据成员们对优先级进行排序和已经优先级删除成员。下面的例子中,我们将添加一个成员到符合其优先级的位置:
function PriorityQueue( ) {
var itmes = [];
function QueueElement (element, priority) { //{1}
this.element = element;
this.priority = priority;
}
this.enqueue = function (element, priority) {
var queueElement = new QueueElement( element, priority);
if (this.isEmpty( )){
items.push(queueElement); //{2}
} else {
var added = false;
for (var i=0; i< items.length;i++){
if (queueElement.priority < items[i].priority){
items.splice(i, 0 , queueElement); //{3}
add = true;
break;{4}
}
}
if (!added){ //{5}
items.push(queueELement);
}
}
};
优先队列和普通队列的不同之处在于,优先队列需要产生一个PriorityQueue类***(行{1})***来存储队列成员的值和其优先级。
如果队列为空,我们直接把新成员加入队列即可***(行{2})。如果队列不为空,我们需要比较新成员和现有成员的优先级。当我们发现有成员的优先级比新成员的优先级要高,那么我们就把新成员插入到这个成员的前面(依照这个逻辑,我们也尊重了先前加入的同优先级成员)。为了实现插入,我们可以使用JavaScript提供的splice方法。一旦我们发现一个成员的优先级比新成员的优先级高,我们把新成员插入到这个成员的前面(行{3}),并停止这个循环(行{4})***。按照这个方法,队列将会按照优先级进行排列。
当然,如果新成员的优先级比队列里任何一个成员的优先级都高,我们只要把新成员放在队列的尾部即可***(行{5})***:
var priorityQueue = new PriorityQueue();
priorityQueue.enqueue(“John”, 2);
priorityQueue.enqueue(“Jack”, 1);
priorityQueue.enqueue(“Camila”, 1);
priorityQueue.print();
下图展示了优先队列的运行过程:
第一个进入队列的成员的优先级为2。因为队列为空,它是队列唯一的成员。之后。我们加入了一个优先级为1的Jack成员。因为Jack的优先级比John的要高,所以Jack被插入到John的前面,Jack成为了队列的第一个成员。之后,我们加入一个优先级为1的Camila成员。Camila成员和Jack成员的优先级相同,Camila将会排列在Jack成员的后面;Camila成员的优先级比John成员的要高,Camila将会被插入到John的前面。
这个优先队列被称为最小优先队列,因为我们把优先级数值小的成员放在了队列的前面。与之相反的是最大优先队列,在这个队列中,优先级数值高的成员将会被放在队列的前面。小的被放在后面。
环形队列
当然,还有另一个改良后的队列,即环形队列。一个环形队列的例子是热土豆游戏。在这个游戏中,孩子们围成一个圆圈,他们将尽可能快的将热土豆传给他们的邻居。当游戏主持人喊停时(每传n次喊一次停),人们停止传送土豆,此时手里有土豆的孩子将会从游戏出局。之后重复这个过程直到游戏只剩下一个人(这个人是胜利者)。
现在,让我来实现一个热土豆游戏:
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 + ‘was eliminated from the Hot Potato game.’);
}
return queue.dequeue( ); //{5}
}
var names = [‘John’, ‘Jack’, ‘Camila’, ‘Ingrid’, ‘Carl’];
var winner = hotPotato(names, 7);
console.log(‘The winner is:’ + winner);
为了模拟这一游戏,我们将用到队列结构***(行{1})。我们将会得到名字列表并把他们放到队列中(行{2})。num变量是用来遍历队列的。我们将队列的第一个成员删除,并将它添加到队列的尾部(行{3}),以实现传土豆的过程。一旦传土豆的次数到达num,手里有土豆的人将会被从游戏中出局(行{4})***。当只剩一个人时,这个人将是游戏的胜利者。
这段代码的输出结果为:
Camila was eliminated from the Hot Potato game。
Jack was eliminated from the Hot Potato game。
Carl was eliminated from the Hot Potato game。
Ingrid was eliminated from the Hot Potato game。
The winner is:John。
小结
在这一章,我们学习了队列结构。我们实现了一个队列结构。我们知道如何定义enqueue 和 dequeuer来增加和删除元素。我们也介绍了两个非常著名的队列特例:优先队列和环形队列。
下一章,我们将学习链表,一个比数组要复杂的数据结构。
注:本文翻译自Loiane Groner的《Learning JavaScript Data Structures and Algorithm》