資料結構之Queue | 讓我們一塊來學習資料結構

guojikun發表於2021-04-27

前面的兩篇文章分別介紹了List和Stack,下面讓我們一起來學習Queue

佇列的概況

佇列是一種列表,不同的是佇列只能在隊尾插入元素,在隊首刪除元素。佇列用於儲存按順序排列的資料,先進先出,這點和棧不一樣,在棧中,最後入棧的元素反而被優先處理。可以將佇列想象成在銀行前排隊的人群,排在最前面的人第一個辦理業務,新來的人只能在後面排隊,直到輪到他們為止。

佇列是一種先進先出(First-In-First-Out,FIFO)的資料結構。佇列被用在很多地方,比如提交作業系統執行的一系列程式、列印任務池等,一些模擬系統用佇列來模擬銀行或雜貨店裡排隊的顧客。

基礎佇列

佇列的兩種主要操作是:向佇列中插入新元素和刪除佇列中的元素。插入操作也叫做入隊,刪除操作也叫做出隊。入隊操作在隊尾插入新元素,出隊操作刪除隊頭的元素。下圖演示了這兩個操作。

queue.png
佇列的另外一項重要操作是讀取隊頭的元素。這個操作叫做 peek()。該操作返回隊頭元素,但不把它從佇列中刪除。除了讀取隊頭元素,我們還想知道佇列中儲存了多少元素,可以使用 length 屬性滿足該需求;要想清空佇列中的所有元素,可以使用 clear() 方法來實現。

使用陣列來實現佇列看起來順理成章。JavaScript 中的陣列具有其他程式語言中沒有的優點,陣列的 push() 方法可以在陣列末尾加入元素,shift() 方法則可刪除陣列的第一個元素。

構建Queue類

class Queue {
    constructor() {
        this.dataStore = [];
    }
    enqueue(element) {
        this.dataStore.push(element);
    }
    dequeue() {
        return this.dataStore.shift();
    }
    front() {
        return this.dataStore[0];
    }
    back() {
        return this.dataStore[this.dataStore.length - 1];
    }
    empty() {
        return this.dataStore.length === 0;
    }
    toString() {
        return this.dataStore.toString();
    }
    length() {
        return this.dataStore.length;
    }
}

優先佇列

在一般情況下,從佇列中刪除的元素,一定是率先入隊的元素。但是也有一些使用佇列的應用,在刪除元素時不必遵守先進先出的約定。這種應用,需要使用一個叫做優先佇列的資料結構來進行模擬。

從優先佇列中刪除元素時,需要考慮優先權的限制。比如醫院急診科的候診室,就是一個採取優先佇列的例子。當病人進入候診室時,分診護士會評估患者病情的嚴重程度,然後給一個優先順序程式碼。高優先順序的患者先於低優先順序的患者就醫,同樣優先順序的患者按照先來先服務的順序就醫。

先來定義儲存佇列元素的物件,然後再構建我們的優先佇列系統:

class Patient {
    constructor(name, code) {
        this.name = name;
        this.code = code;
    }
}

變數 code 是一個整數,表示患者的優先順序或病情嚴重程度(code越小代表病情越嚴重)。

下面需要重新定義Queue類的dequeue方法,使其刪除佇列中擁有最高優先順序的元素。新的 dequeue()方法遍歷佇列的底層儲存陣列,從中找出優先碼最低的元素,然後使用陣列的 splice()方法刪除優先順序最高的元素。新的dequeue()方法定義如下所示:

dequeue() {
    let priority = this.dataStore[0];
    for (const i = 0, len = this.dataStore.length; i < len; i++){
        if (this.dataStore[i].code < priority) {
            priority = i;
        }
    }
    return this.dataStore.splice(priority, 1);
}

最後,需要定義 toString() 方法來顯示 Patient 物件。

toString() {
    let retStr = "";
    for (var i = 0; i < this.dataStore.length; ++i) {
        retStr += `${this.dataStore[i].name} code: ${this.dataStore[i].code}\n`;
    }
    return retStr;
}

測試優先佇列

企業微信截圖_20210426173457.png

雙端佇列

雙端佇列(deque,或稱double-ended queue)是一種允許我們同時從前端和後端新增和移除元素的特殊佇列。

在電腦科學中,雙端佇列的一個常見應用是儲存一系列的撤銷操作。每當使用者在軟體中進行了一個操作,該操作就會被存在雙端佇列中。當使用者點選撤銷按鈕時,該操作會從雙端佇列中彈出,表示它被從後端移除了一個了。在進行了一定數量的操作後,最先進行的操作會被從雙端佇列的前端移除。由於雙端佇列同時遵循了先入先出和後入先出的原則,可以說是它是把佇列和棧相結合的一種資料結構。

建立Deque類

class Deque {
    constructor() {
        this.dataStore = [];
    }
    addFront(element) {
        if(this.empty()){
            this.addBack(element)
        }else{
            this.dataStore.unshift(element);
        }
    }
    addBack(element) {
        this.dataStore.push(element);
    }
    removeFront() {
        return this.dataStore.shift();
    }
    removeBack() {
        return this.dataStore.pop();
    }
    front() {
        return this.dataStore[0];
    }
    back() {
        return this.dataStore[this.dataStore.length - 1];
    }
    empty() {
        return this.dataStore.length === 0;
    }
    toString() {
        return this.dataStore.toString();
    }
    length() {
        return this.dataStore.length;
    }
}

測試Deque類的程式碼

QQ20210426-202023@2x.png

實際應用-->迴文字串的判斷

迴文是指這樣一種現象:一個單詞、短語或數字,從前往後寫和從後往前寫都是一樣的。 比如,單詞“dad”、“racecar”就是迴文;如果忽略空格和標點符號,下面這個句子也是回 文,“A man, a plan, a canal: Panama”;數字 1001 也是迴文。

在之前的文章中是使用棧(Stack)這一資料結構,其實迴文字串字號的判斷方法是使用雙端佇列(Deque)來實現。

function isPalindrome(word) {
    if (typeof word !== "string") {
        throw TypeError(`引數不是string型別`);
    }
    let tmp = new Deque();
    for (let element of word) {
        tmp.addBack(element);
    }
    while (tmp.length() > 1) {
        if (tmp.removeFront() !== tmp.removeBack()) {
            return false;
        }
    }
    return true;
}

console.log(isPalindrome("racecar")) // true
console.log(isPalindrome("hello")) // false

參考資料

  • 資料結構與演算法JavaScript描述
  • 學習JavaScript資料結構與演算法 第3版

相關文章