前端學習 資料結構與演算法 快速入門 系列 —— 佇列和雙端佇列

彭加李發表於2021-08-09

佇列和雙端佇列

前面我們已經學習了資料結構。佇列和棧非常類似,棧的原則是先進後出,而佇列則是先進先出。同時,我們要學習雙端佇列,它是一種允許我們同時從前端和後端新增元素和移除元素的特殊佇列。

佇列資料結構

佇列遵循先進先出(FIFO,也稱為先到先服務)原則的一組有序的項。佇列在尾部新增元素,並從佇列頭部刪除元素。

現實生活中的佇列有:

  • 排隊買票,排在第一位的先接受服務,新來的人則排到隊尾
  • 列印,比如有 10 份文件,依次對每個文件點選列印,每個文件將傳送到列印佇列,第一個傳送的文件最先被列印,排在最末的文件則最後才列印

建立佇列

首先我們建立一個自己的類來表示佇列。

class Queue {
    constructor() {
        this.items = {}
        // 佇列頭部索引
        this.startIndex = 0
        // 佇列尾部索引
        this.lastIndex = 0
    }
}

我們使用一個物件來儲存佇列中的元素,當然你也可以使用陣列。由於需要從佇列頭部刪除元素,以及從隊尾插入元素,所以這裡定義了兩個變數。

接下來需要宣告一些佇列需要的方法:

  • enqueue(element1[, element2, element3, ...]),給佇列尾部插入一個或多個值
  • dequeue(),從佇列頭部刪除第一項,並返回被移除的元素
  • isEmpty(),如果佇列不包含任何元素則返回 true,否則返回 false
  • size(),返回佇列中元素的個數
  • peek(),取得佇列首部第一個元素。該方法在其他語言也可以叫做 front 方法
  • clear(),清除佇列
  • toString(),重寫 toString() 方法

向佇列插入元素

enqueue(...values) {
    values.forEach(item => {
        this.items[this.lastIndex++] = item;
    })
}

檢視佇列是否為空

isEmpty() {
    return Object.is(this.startIndex, this.lastIndex)
}

從佇列移除元素

dequeue() {
    if (this.isEmpty()) {
        return undefined
    }
    const value = this.items[this.startIndex]
    delete this.items[this.startIndex++]
    return value
}

檢視佇列頭元素

peek() {
    return this.items[this.startIndex]
}
front() {
    return this.peek()
}

佇列元素個數

size() {
    return this.lastIndex - this.startIndex
}

清空佇列

clear() {
    this.items = {}
    this.startIndex = 0
    this.lastIndex = 0
}

重寫 toString() 方法

toString() {
    if (this.isEmpty()) {
        return ''
    }
    return Object.values(this.items).join(',')
}

使用 Queue 類

class Queue {
    constructor() {
        this.items = {}
        // 佇列頭部索引
        this.startIndex = 0
        // 佇列尾部索引
        this.lastIndex = 0
    }
    // 向佇列插入元素
    enqueue(...values) {
        values.forEach(item => {
            this.items[this.lastIndex++] = item;
        })
    }
    isEmpty() {
        return Object.is(this.startIndex, this.lastIndex)
    }
    // 從佇列移除元素
    dequeue() {
        if (this.isEmpty()) {
            return undefined
        }
        const value = this.items[this.startIndex]
        delete this.items[this.startIndex++]
        return value
    }
    peek() {
        return this.items[this.startIndex]
    }
    front() {
        return this.peek()
    }
    size() {
        return this.lastIndex - this.startIndex
    }
    clear() {
        this.items = {}
        this.startIndex = 0
        this.lastIndex = 0
    }
    toString() {
        if (this.isEmpty()) {
            return ''
        }
        return Object.values(this.items).join(',')
    }
}
let q1 = new Queue()
q1.enqueue('a', 'b', { c: 'c1' })
console.log(q1.items) // { '0': 'a', '1': 'b', '2': { c: 'c1' } }
console.log(q1.toString()) // a,b,[object Object]
console.log(q1.peek()) // a
console.log(q1.front()) // a
console.log(q1.dequeue()) // a
console.log(q1.dequeue()) // b
console.log(q1.items) // { '2': { c: 'c1' } }

雙端佇列資料結構

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

雙端佇列在現實生活中的例子有排隊買票。舉個例子,一個剛買完票的人如果還需要回去諮詢一些事,就可以直接回到隊伍頭部,而在隊尾的人如果有事,也可以直接離開隊伍

建立雙端佇列

雙端佇列作為一種特殊的佇列,擁有如下方法:

  • addFront(elemnt[, element2, element3, ...]),給佇列前端新增一個或多個元素
  • addBack(elemnt[, element2, element3, ...]),給佇列尾部新增一個或多個元素
    • 實現與 Queue 類的 enqueue 方法一樣
  • removeFront(),從佇列頭部刪除元素,並返回刪除的元素
    • 實現與 Queue 類的 dequeue 方法一樣
  • removeBack(),從佇列尾部刪除元素,並返回刪除的元素
  • clear(),清空雙端佇列
    • 實現與 Queue 類中的 clear 方法一樣
  • isEmpty(),雙端佇列中如果沒有元素,則返回 true,否則返回 false
    • 實現與 Queue 類中的 isEmpty 方法一樣
  • peekFront(),取得佇列頭部第一個元素
    • 實現與 Queue 類中的 peek 方法一樣
  • peekBack(),取得佇列尾部最後一個元素
  • size(),取得佇列中元素的個數
    • 實現與 Queue 類中的 size 方法一樣
  • toString(),重寫 toString() 方法

Tip:根據每個人不同的實現,會有部分程式碼與佇列相同

我們首先宣告一個 Deque 類及其建構函式:

class Deque {
    // 與 Queue 建構函式相同
    constructor() {
        this.items = {}
        this.startIndex = 0
        this.lastIndex = 0
    }
}

給佇列前端新增

// 給佇列頭部新增一個或多個元素
addFront(...values) {
    values.reverse().forEach(item => {
        this.items[--this.startIndex] = item;
    })
}

從佇列尾部刪除元素

// 從佇列尾部刪除元素
removeBack() {
    if (this.isEmpty()) {
        return undefined
    }
    this.lastIndex--
    const value = this.items[this.lastIndex]
    delete this.items[this.lastIndex]
    return value
}

取得佇列尾部最後一個元素

peekBack() {
    return this.items[this.lastIndex - 1]
}

重寫 toString() 方法

toString() {
    if (this.isEmpty()) {
        return ''
    }
    let { startIndex, lastIndex } = this
    const result = []
    while (!Object.is(startIndex, lastIndex)) {
        result.push(this.items[startIndex++])
    }
    return result.join(',')
}

使用 Deque 類

/**
 * 雙端佇列
 *
 * @class Deque
 */
class Deque {
    // 與 Queue 建構函式相同
    constructor() {
        this.items = {}
        this.startIndex = 0
        this.lastIndex = 0
    }
    // 給佇列頭部新增一個或多個元素
    addFront(...values) {
        values.reverse().forEach(item => {
            this.items[--this.startIndex] = item;
        })
    }
    // 實現與 Queue 類的 enqueue 方法一樣
    addBack(...values) {
        values.forEach(item => {
            this.items[this.lastIndex++] = item;
        })
    }
    // 實現與 Queue 類的 dequeue 方法一樣
    removeFront() {
        if (this.isEmpty()) {
            return undefined
        }
        const value = this.items[this.startIndex]
        delete this.items[this.startIndex++]
        return value
    }
    // 從佇列尾部刪除元素
    removeBack() {
        if (this.isEmpty()) {
            return undefined
        }
        this.lastIndex--
        const value = this.items[this.lastIndex]
        delete this.items[this.lastIndex]
        return value
    }
    // 實現與 Queue 類中的 peek 方法一樣
    peekFront() {
        return this.items[this.startIndex]
    }
    peekBack() {
        return this.items[this.lastIndex - 1]
    }
    // 與 Queue 類中的 isEmpty 方法一樣
    isEmpty() {
        return Object.is(this.startIndex, this.lastIndex)
    }
    // 實現與 Queue 類中的 size 方法一樣
    size() {
        return this.lastIndex - this.startIndex
    }
    // 實現與 Queue 類中的 clear 方法一樣
    clear() {
        this.items = {}
        this.startIndex = 0
        this.lastIndex = 0
    }
    // 重寫 toString() 方法
    toString() {
        if (this.isEmpty()) {
            return ''
        }
        let { startIndex, lastIndex } = this
        const result = []
        while (!Object.is(startIndex, lastIndex)) {
            result.push(this.items[startIndex++])
        }
        return result.join(',')
    }
}

let deque = new Deque()
// 佇列頭部新增
deque.addFront('a')
deque.addFront('c', 'd')
console.log(deque.toString()) // c,d,a

// 佇列尾部新增
deque.addBack(3)
deque.addBack(4, 5)
console.log(deque.toString()) // c,d,a,3,4,5

// 從頭部和尾部刪除元素
console.log(deque.removeBack()) // 5
console.log(deque.removeFront()) // c
console.log(deque.toString()) // d,a,3,4

// 檢視佇列頭部和尾部的元素
console.log(deque.peekFront()) // d
console.log(deque.peekBack()) // 4

// 佇列中元素個數
console.log(deque.size()) // 4

// 清空佇列
deque.clear()

// 佇列是否為空
console.log(deque.isEmpty()) // true
// 佇列元素個數
console.log(deque.size()) // 0

使用佇列和雙端佇列解決問題

迴圈佇列 —— 擊鼓傳花

擊鼓傳花遊戲:

小孩子圍成一個圓圈,把花盡可能地傳遞給旁邊的人,某一時刻,花在誰手裡,誰就退出圓圈。重複這個過程,直到只剩一個小孩(勝利)。

比如:

// 有五個小孩
const people = ['p1', 'p2', 'p3', 'p4', 'p5']
// 開始遊戲
擊鼓傳花(people)
傳遞次數:  3
p4 淘汰
傳遞次數:  5
p1 淘汰
傳遞次數:  1
p3 淘汰
傳遞次數:  1
p2 淘汰
p5 勝利

筆者實現如下:

// 生成 1 ~ 5 的隨機數
function getRandomInt(max = 5) {
    return Math.floor(Math.random() * max) + 1;
}

function 擊鼓傳花(people, count = getRandomInt) {
    const queue = new Queue()
    // 進入佇列
    people.forEach(item => queue.enqueue(item))

    // 傳遞 size - 1 次
    let number = queue.size() - 1
    while (number--) {
        let _count = count()
        console.log('傳遞次數: ', _count);
        while (_count--) {
            queue.enqueue(queue.dequeue())
        }
        console.log(queue.dequeue() + ' 淘汰');
    }

    // 剩下的就是勝利者
    console.log(queue.peek() + ' 勝利');
}

迴文檢查器

維基百科對迴文的解釋:

迴文是正反都能讀通的單詞、片語、數或一系列字元的序列,例如 madam 或 racecar。

有不同的演算法檢查一個片語或字串是否為迴文。

最簡單的方式是將字串反向排列並檢查它和原來字串是否相同。如果兩者相同,那麼它就是一個迴文。

利用資料結構來解決此問題最簡單的方式是使用雙端佇列。

筆者實現如下:

/**
 * 迴文檢查器
 *
 * @param {String} str
 * @return {Boolean} 
 */
function palindromeChecker(str) {
    if (typeof str !== 'string') {
        throw new Error('請輸入字串')
    }
    let flag = true
    // 忽略大小寫以及移除所有空格
    str = str.toLowerCase().replace(/\s/g, '')
    // 轉為雙端佇列
    let deque = new Deque()
    deque.addBack(...str)

    // 對比次數
    let count = Math.floor(deque.size() / 2)
    while (count-- && flag) {
        if (deque.removeFront() !== deque.removeBack()) {
            flag = false
        }
    }
    // 返回結果

    return flag
}
// 所有輸出都是 true
console.log('a', palindromeChecker('a'))
console.log('aa', palindromeChecker('aa'))
console.log('kayak', palindromeChecker('kayak'))
console.log('level', palindromeChecker('level'))
console.log('Was it a car or a cat I saw', palindromeChecker('Was it a car or a cat I saw'))
console.log('Step on no pets', palindromeChecker('Step on no pets'))

Tip:筆者將回文的範圍放寬鬆了一些,比如忽略大小寫,同時移除所有空格。如果願意,你還可以忽略所有特殊字元。

佇列完整程式碼

class Queue {
    constructor() {
        this.items = {}
        // 佇列頭部索引
        this.startIndex = 0
        // 佇列尾部索引
        this.lastIndex = 0
    }
    // 向佇列插入元素
    enqueue(...values) {
        values.forEach(item => {
            this.items[this.lastIndex++] = item;
        })
    }
    isEmpty() {
        return Object.is(this.startIndex, this.lastIndex)
    }
    // 從佇列移除元素
    dequeue() {
        if (this.isEmpty()) {
            return undefined
        }
        const value = this.items[this.startIndex]
        delete this.items[this.startIndex++]
        return value
    }
    peek() {
        return this.items[this.startIndex]
    }
    front() {
        return this.peek()
    }
    size() {
        return this.lastIndex - this.startIndex
    }
    clear() {
        this.items = {}
        this.startIndex = 0
        this.lastIndex = 0
    }
    toString() {
        if (this.isEmpty()) {
            return ''
        }
        return Object.values(this.items).join(',')
    }
}
/**
 * 雙端佇列
 *
 * @class Deque
 */
class Deque {
    // 與 Queue 建構函式相同
    constructor() {
        this.items = {}
        this.startIndex = 0
        this.lastIndex = 0
    }
    // 給佇列頭部新增一個或多個元素
    addFront(...values) {
        values.reverse().forEach(item => {
            this.items[--this.startIndex] = item;
        })
    }
    // 實現與 Queue 類的 enqueue 方法一樣
    addBack(...values) {
        values.forEach(item => {
            this.items[this.lastIndex++] = item;
        })
    }
    // 實現與 Queue 類的 dequeue 方法一樣
    removeFront() {
        if (this.isEmpty()) {
            return undefined
        }
        const value = this.items[this.startIndex]
        delete this.items[this.startIndex++]
        return value
    }
    // 從佇列尾部刪除元素
    removeBack() {
        if (this.isEmpty()) {
            return undefined
        }
        this.lastIndex--
        const value = this.items[this.lastIndex]
        delete this.items[this.lastIndex]
        return value
    }
    // 實現與 Queue 類中的 peek 方法一樣
    peekFront() {
        return this.items[this.startIndex]
    }
    peekBack() {
        return this.items[this.lastIndex - 1]
    }
    // 與 Queue 類中的 isEmpty 方法一樣
    isEmpty() {
        return Object.is(this.startIndex, this.lastIndex)
    }
    // 實現與 Queue 類中的 size 方法一樣
    size() {
        return this.lastIndex - this.startIndex
    }
    // 實現與 Queue 類中的 clear 方法一樣
    clear() {
        this.items = {}
        this.startIndex = 0
        this.lastIndex = 0
    }
    // 重寫 toString() 方法
    toString() {
        if (this.isEmpty()) {
            return ''
        }
        let { startIndex, lastIndex } = this
        const result = []
        while (!Object.is(startIndex, lastIndex)) {
            result.push(this.items[startIndex++])
        }
        return result.join(',')
    }
}

相關文章