前端学习 数据结构与算法 快速入门 系列 —— 队列和双端队列

其他章节请看:

前端学习 数据结构与算法 快速入门 系列

队列和双端队列

前面我们已经学习了数据结构。队列和栈非常类似,栈的原则是先进后出,而队列则是先进先出。同时,我们要学习双端队列,它是一种允许我们同时从前端和后端添加元素和移除元素的特殊队列。

队列数据结构

队列遵循先进先出(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(',')
    }
}

其他章节请看:

前端学习 数据结构与算法 快速入门 系列

posted @ 2021-08-09 21:06  彭加李  阅读(298)  评论(0编辑  收藏  举报