為什麼我要放棄javaScript資料結構與演算法(第四章)—— 佇列
有兩種結構類似於陣列,但在新增和刪除元素時更加可控,它們就是棧和佇列。
第四章 佇列
佇列資料結構
佇列是遵循FIFO(First In First Out,先進先出,也稱為先來先服務)原則的一組有序的項。佇列在尾部新增新元素,並從頂部移除元素。最新新增的元素必須排在佇列的末尾。
現實中,很常見的例子就是排隊。在計算機科學裡面是列印佇列。
建立佇列
我們需要建立自己的類來表示一個佇列,先從最基本的宣告開始:
function Queue(){
// 這裡是屬性和方法
}
首先需要一個用於儲存佇列中元素的資料結構。我們可以使用陣列,就像上一章 Stack 類中那樣使用(你會發現其實兩者很相似,只是新增和移除元素不一樣而已。)
let items = []
接下來需要宣告一些佇列可用的方法。
- enqueue(element(s)):向佇列尾部新增一個(或多個)新的項。
- dequeue():移除佇列中的第一個(排列在隊伍最前面的)項,並返回被移除的元素
- front():返回佇列中的第一個元素——最先被新增,也將是最先被移除的元素。佇列不做任何變動(不移除元素,只返回元素資訊——與 Stack 類的 peek 方法非常相似)
- isEmpty():如果佇列中不包含任何元素,返回 ture,否則返回 false
- size():返回佇列包含的元素個數,與陣列的 length 屬性類似。
向佇列新增元素
首先要實現的是 enqueue 方法。這個方法負責向佇列中新增新元素,還有一個非常重要的細節,新的專案只能新增到佇列末尾:
this.enqueue = function(element){
return items.push(element);
};
從佇列中移除元素
接下來就是 dequeue 方法,這個方法負責從佇列中移除項。由於佇列遵循先進先出原則,最先新增的項也是要最先被移除的。陣列中的 shift 方法會從陣列中移除儲存在索引0(第一個位置)的元素。
this.dequeue = function(element){
return items.shift();
}
只有 enqueue 方法和 dequeue 方法可以新增和移除元素,這樣就確保了 Queue 類遵循先進先出的原則。
檢視佇列頭元素
為我們類實現一些額外的輔助方法。我們想知道佇列最前面是什麼,可以使用 front 方法檢視
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());
}
例項
function Queue(){
let items = [];
this.enqueue = function(element){
return 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.clear = function(){
items = [];
}
this.print = function(){
console.log(items.toString());
}
}
let queue = new Queue(); // 新建 類 Queue 的例項 queue
console.log(queue.isEmpty()); // 佇列沒有元素,返回 true
queue.enqueue(5); // 先佇列中加 5
queue.enqueue(8); // 先佇列中加 8
queue.dequeue(); // 減去佇列的開頭
console.log(queue.front()); // 8
queue.enqueue(11); // 先佇列中加 11
console.log(queue.size()); // 佇列的長度 2
console.log(queue.isEmpty()); // 佇列有元素,返回 false
queue.enqueue(15); // 先佇列中加 15
queue.print(); // 輸出佇列中的元素 8,11,15
使用ES6 語法實現的 Queue 類
我們使用一個 WeakMap 來儲存私有屬性items,並用外層函式(閉包)來封裝 Queue 類。
let Queue = (function(){
const items = new WeakMap(); // 聲明瞭一個 WeakMap 型別的變數 items
class Queue{
constructor(){
items.set(this, []) // 在 constructor 中,以this(Stack類自己引用)為鍵,把代表棧的陣列存入 items
}
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 (){
let q = items.get(this);
return q.length == 0;
}
size (){
let q = items.get(this);
let r = q.length
return r;
}
clear (){
items.set(this, [])
}
print (){
let q = items.get(this);
console.log(q.toString());
}
}
return Queue;
})();
優先佇列
佇列大量應用在電腦科學以及我們的生活中,其中一個就是優先佇列。元素的新增和移除是基於優先順序的。現實中的例子就是登機的順序。頭等艙和商務艙的乘客優先順序要優於經濟艙乘客。
另外一個現實的例子就是醫院的候診室。醫生會優先處理病情比較嚴重的患者。
實現一個佇列,有兩種選項:設定優先順序,然後在正確的位置新增元素;或者用入列操作新增元素,然後按照優先順序操作它們。在這個例項中,我們會在正確的位置新增元素,因此可以對它們使用預設的出列操作。
function ProrityQueue(){
let items = [];
function QueueElement(element, priority){
// 引數包含了新增到佇列的元素以及其的優先順序
this.element = element;
this.priority = priority;
}
this.enqueue = function(element, priority){
let queueElement = new QueueElement(element, priority);
let added = false;
// 如果佇列為空可以直接將元素插入,否則就要比較元素與該元素的優先順序。
// 當找到一個比要新增元素的 priority 值更高(優先順序更低)的項時,
// 我們就把元素插入它之前,但是如果優先順序相同的話就遵循先進先出的原則
for(let i = 0; i < items.length; i++){
if(queueElement.priority < items[i].priority ){
items.splice(i,0,queueElement);
added = true;
break;
}
}
if(!added){
// 如果新增元素的 priority 值大於任何已有的元素,把它新增到佇列的末尾就行了
items.push(queueElement);
}
}
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.clear = function(){
items = [];
}
this.print = function(){
for(let i = 0; i < items.length; i++){
console.log(`${items[i].element} - ${items[i].priority}`);
}
}
}
let prorityQueue = new ProrityQueue();
prorityQueue.enqueue('John',2);
prorityQueue.enqueue('Mike',1);
prorityQueue.enqueue('Jenny',1);
prorityQueue.print();
/*
Mike - 1
Jenny - 1
John - 2
*/
迴圈佇列——擊鼓傳花
還有另一個修改版的佇列實現,就是迴圈佇列。迴圈佇列的一個例子就是擊鼓傳花遊戲(Hot Potato)。在這個遊戲中,孩子們圍成一個圓圈,把花盡快地傳遞給旁邊的人,某一時刻傳花停止,這個時候,花就在誰的手裡,誰就退出圓圈結束遊戲。重複這個過程,直到最後一個孩子,就是勝者。
在這個例子中,我們要實現一個模擬的擊鼓傳花遊戲。
function Queue(){
let items = [];
this.enqueue = function(element){
return 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.clear = function(){
items = [];
}
this.print = function(){
console.log(items.toString());
}
}
function hotPotata(nameList, num){
let queue = new Queue();
// 將姓名名單 nameList 逐個加入到佇列中
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 = hotPotata(names, 7);
console.log('獲勝者是'+ winner);
// Camila在擊鼓傳花遊戲中被淘汰
// John在擊鼓傳花遊戲中被淘汰
// Carl在擊鼓傳花遊戲中被淘汰
// Jack在擊鼓傳花遊戲中被淘汰
// 獲勝者是Ingrid
下圖模擬了這個輸出過程:
可以改變傳入 hotPotata 函式的數字,模擬不同的場景。
JavaScript 任務佇列
當我們在瀏覽器中開啟新標籤時,就會建立一個任務佇列。這是因為每個標籤都是單執行緒處理所有的任務,它被稱為 事件迴圈。瀏覽器要負責多個任務,如渲染 HTML ,執行 JavaScript 程式碼,處理使用者互動(使用者輸入,滑鼠點選等),執行和處理非同步請求。
小結
這一章學習了佇列這種資料結構。實現了自己的佇列演算法,學習瞭如何通過 enqueue 方法和 dequeue 方法新增和移除元素。還學習了兩種非常著名的特殊佇列的實現,優先佇列和迴圈佇列(使用擊鼓傳花的實現)
下一章,將學習連結串列,一種比陣列更加複雜的資料結構。