1. 程式人生 > >為什麼我要放棄javaScript資料結構與演算法(第四章)—— 佇列

為什麼我要放棄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 方法新增和移除元素。還學習了兩種非常著名的特殊佇列的實現,優先佇列和迴圈佇列(使用擊鼓傳花的實現)

下一章,將學習連結串列,一種比陣列更加複雜的資料結構。