算法小白也能学会的数据结构——队列

135 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情

上一节我讲解了栈,朋友们纷纷表示比较这种一边看文一边收藏表情包的学习方式有点儿爽 🥳 🥳 🥳

这一节我们学习另一种与栈关系十分亲密的数据结构——队列。

2_pb5gd__.gif

队列的基本概念

首先我们看这个下面这台“屑阿妮亚”生产机 🤣

Jun-01-2022 12-07-25.gif

我们根据上面这台“屑阿妮亚”生产机来解读一下队列:

  1. 履带部分代表着队列的容器部分

  2. 最前面(图右)的阿妮亚表示着队列头部。同理,最后面(图左)的阿妮亚表示队列尾部

  3. 对于从履带(队列)中出去的阿妮亚就是已经出队列的元素了。

  4. 只有在图部分的阿妮亚(元素)才算在处于队列中

言归正传,队列存储数据的特性就是先进先出,后进后出(First in, First out。 即 FIFO)。用前端中的数组来说就是,它只会用到数组的pushshift这两个操作。

队列的实现

这里我将写出基于数组和链表这两种方式来实现队列。

基于数组实现队列

class ArrayQueue<T> {
    private data: T[] = []

    // 查看队头元素
    front(): T {
        return this.data[0]
    }
    // 查看队尾元素
    back(): T{
        return this.data[this.size - 1]
    }
    // 入队
    push(value: T) {
        this.data.push(value)
    }
    // 出队
    pop() {
        return this.data.shift()
    }

    size() {
        return this.data.length
    }
  
    empty() {
        return this.size() === 0
    }
}

以数组实现的队列就这么简单,它的插入和查找效率都非常的好,为O(1),但是我们知道,数组的shift方法调用是比较大的消耗的,也就是删除操作复杂度太高,O(n)。所以我们使用链表来实现会更加合适。

基于链表实现队列

class ListNodeQueue {
    private head: ListNode | null = null // 队列头(链表头)
    private tail: ListNode | null = null // 队列尾(链表尾)
    private _size: number = 0 // 队列中元素的个数

    size() {
        return this._size
    }

    empty() {
        return this.size() === 0
    }

	// 移除队首元素
    pop() {
        if (this.empty()) return
        // 获取当前队首的值
        const res = this.head as ListNode
        // 将队首设定为下一个元素
        this.head = (this.head as ListNode).next
        // 判断一下原队首是否等于队列尾,如果相等,说明在移除队首元素之前,当前队列中只有一个元素
        if (this.tail === res) {
            this.tail = null
        }
        this._size--
        // 最后返回其中的值
        return res.val
    }

    push(value: any) {
        const node = new ListNode(value)
        // 如果为空,则说明当前进来的元素会作为队首元素,同时也会是队尾元素
        if (this.empty()) {
            this.head = node
        } else {
            // 已经存在了就添加到队尾
            (this.tail as ListNode).next = node
        }

        // 更新队尾
        this.tail = node
        this._size++
    }

    front() {
            return this.head?.val
    }
    back() {
            return this.tail?.val
    }
}

class ListNode {
    val: any
    next: ListNode | null
    constructor(val: any, next: ListNode | null = null) {
        this.val = val
        this.next = next
    }
}

我们可以看到,我们的实现中,会有headtail_size分别记录着队列的队首队尾以及队列数量。 其中在poppush时都会更新队尾元素tail以保证下次push添加的正确性。同时也会更新_size的值以保证emptysize的返回值的正确。

队列有什么用?

经典问题来了,这玩意儿在我开发中有什么用?

老实讲,我在实际开发过程中也没用到过这玩意儿(也许是因为我的工作太low了吧😂),但实际上,我们每天都在跟这玩意儿打交道。比如我们发起的网络请求、使用的框架 react 等。

那么理解队列对于你理解其他的一些知识就会有帮助,更直接的作用可能就是你刷 leetcode 时可能就会要用到这种思想。甚至是你面试可能就会需要使用到队列。我们趁此机会来解道题吧~

这个题比较简单 leetcode 933.最近的请求次数 要求在每次调用RecentCounterping方法时,将传入的t与队首进行比较,如果超出t-head > 3000则需要将队首出列。最后返回队列中满足条件的数量即可。

class RecentCounter {
  queue: ListNodeQueue
  
  constructor() {
    // 我们自己写的队列不能定义为Queue,会与 leetcode 中内置的 Queue 冲突
    this.queue = new ListNodeQueue()
  }

  ping(t: number): number {
    this.queue.push(t)
    while(t - this.queue.front() > 3000) {
      this.queue.pop()
    }
    return this.queue.size()
  }
}

建议自己定义的链表和队列换个变量名,不然会跟 leetcode 中内置的变量名冲突 剑指 Offer II 041. 滑动窗口的平均值 与本题思路相似,建议你练练手

比如像 622. 设计循环队列 这题基本就是一个设计一个 queue,你也可以拿来练练手

到点了,该下班了,今天就到这里了,886

image.png