【常用数据结构】多语言详解“队列”相关面试题合集

760 阅读6分钟

队列

对于数据结构而言,队列的重要性不言而喻,往往是笔试/面试向中的重中之重。

今天一起学习 33 道与队列相关的题目。


622. 设计循环队列

设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。

循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。

你的实现应该支持如下操作:

  • MyCircularQueue(k): 构造器,设置队列长度为 kk
  • Front: 从队首获取元素。如果队列为空,返回 1-1
  • Rear: 获取队尾元素。如果队列为空,返回 1-1
  • enQueue(value): 向循环队列插入一个元素。如果成功插入则返回真。
  • deQueue(): 从循环队列中删除一个元素。如果成功删除则返回真。
  • isEmpty(): 检查循环队列是否为空。
  • isFull(): 检查循环队列是否已满。

提示:

  • 所有的值都在 00 至 10001000 的范围内;
  • 操作数将在 1110001000 的范围内;
  • 请不要使用内置的队列库。
数据结构

创建一个长度为 kk 的数组充当循环队列,使用两个变量 heta 来充当队列头和队列尾(起始均为 00),整个过程 he 始终指向队列头部,ta 始终指向队列尾部的下一位置(待插入元素位置)。

两变量始终自增,通过与 kk 取模来确定实际位置。

分析各类操作的基本逻辑:

  • isEmpty 操作:当 heta 相等,队列存入元素和取出元素的次数相同,此时队列为空;
  • isFull 操作:ta - he 即队列元素个数,当元素个数为 kk 个时,队列已满;
  • enQueue 操作:若队列已满,返回 1-1,否则在 nums[ta % k] 位置存入目标值,并将 ta 指针后移;
  • deQueue 操作:若队列为空,返回 1-1,否则将 he 指针后移,含义为弹出队列头部元素;
  • Front 操作:若队列为空,返回 1-1,否则返回 nums[he % k] 队头元素;
  • Rear 操作:若队列为空,返回 1-1,否则返回 nums[(ta - 1) % k] 队尾元素;

Java 代码:

class MyCircularQueue {
    int k, he, ta;
    int[] nums;
    public MyCircularQueue(int _k) {
        k = _k;
        nums = new int[k];
    }
    public boolean enQueue(int value) {
        if (isFull()) return false;
        nums[ta % k] = value;
        return ++ta >= 0;
    }
    public boolean deQueue() {
        if (isEmpty()) return false;
        return ++he >= 0;
    }
    public int Front() {
        return isEmpty() ? -1 : nums[he % k];
    }
    public int Rear() {
        return isEmpty() ? -1 : nums[(ta - 1) % k];
    }
    public boolean isEmpty() {
        return he == ta;
    }
    public boolean isFull() {
        return ta - he == k;
    }
}

C++ 代码:

class MyCircularQueue {
private:
    int k, he, ta;
    vector<int> nums;

public:
    MyCircularQueue(int _k) : k(_k), he(0), ta(0), nums(_k, 0) {}

    bool enQueue(int value) {
        if (isFull())
            return false;
        nums[ta % k] = value;
        ta++;
        return true;
    }

    bool deQueue() {
        if (isEmpty())
            return false;
        he++;
        return true;
    }

    int Front() {
        return isEmpty() ? -1 : nums[he % k];
    }

    int Rear() {
        return isEmpty() ? -1 : nums[(ta - 1) % k];
    }

    bool isEmpty() {
        return he == ta;
    }

    bool isFull() {
        return ta - he == k;
    }
};

Python 代码:

class MyCircularQueue:
    def __init__(self, k: int):
        self.k = k
        self.he = 0
        self.ta = 0
        self.nums = [0] * k

    def enQueue(self, value: int) -> bool:
        if self.isFull():
            return False
        self.nums[self.ta % self.k] = value
        self.ta += 1
        return True

    def deQueue(self) -> bool:
        if self.isEmpty():
            return False
        self.he += 1
        return True

    def Front(self) -> int:
        return -1 if self.isEmpty() else self.nums[self.he % self.k]

    def Rear(self) -> int:
        return -1 if self.isEmpty() else self.nums[(self.ta - 1) % self.k]

    def isEmpty(self) -> bool:
        return self.he == self.ta

    def isFull(self) -> bool:
        return self.ta - self.he == self.k

Go 代码:

type MyCircularQueue struct {
    k    int
    he   int
    ta   int
    nums []int
}

func Constructor(k int) MyCircularQueue {
    return MyCircularQueue{k: k, nums: make([]int, k)}
}

func (q *MyCircularQueue) EnQueue(value int) bool {
    if q.IsFull() {
        return false
    }
    q.nums[q.ta%q.k] = value
    q.ta++
    return true
}

func (q *MyCircularQueue) DeQueue() bool {
    if q.IsEmpty() {
        return false
    }
    q.he++
    return true
}

func (q *MyCircularQueue) Front() int {
    if q.IsEmpty() {
        return -1
    }
    return q.nums[q.he%q.k]
}

func (q *MyCircularQueue) Rear() int {
    if q.IsEmpty() {
        return -1
    }
    return q.nums[(q.ta-1)%q.k]
}

func (q *MyCircularQueue) IsEmpty() bool {
    return q.he == q.ta
}

func (q *MyCircularQueue) IsFull() bool {
    return q.ta-q.he == q.k
}

TypeScript 代码:

class MyCircularQueue {
    private k: number;
    private he: number;
    private ta: number;
    private nums: number[];

    constructor(k: number) {
        this.k = k;
        this.he = 0;
        this.ta = 0;
        this.nums = new Array(k).fill(0);
    }

    enQueue(value: number): boolean {
        if (this.isFull()) {
            return false;
        }
        this.nums[this.ta % this.k] = value;
        this.ta++;
        return true;
    }

    deQueue(): boolean {
        if (this.isEmpty()) {
            return false;
        }
        this.he++;
        return true;
    }

    Front(): number {
        return this.isEmpty() ? -1 : this.nums[this.he % this.k];
    }

    Rear(): number {
        return this.isEmpty() ? -1 : this.nums[(this.ta - 1) % this.k];
    }

    isEmpty(): boolean {
        return this.he === this.ta;
    }

    isFull(): boolean {
        return this.ta - this.he === this.k;
    }
}

  • 时间复杂度:构造函数复杂度为 O(k)O(k),其余操作复杂度为 O(1)O(1)
  • 空间复杂度:O(k)O(k)

1190. 反转每对括号间的子串

给出一个字符串 s(仅含有小写英文字母和括号)。

请你按照从括号内到外的顺序,逐层反转每对匹配括号中的字符串,并返回最终的结果。

注意,您的结果中 不应 包含任何括号。

示例 1:

输入:s = "(abcd)"

输出:"dcba"

提示:

  • 0<=s.length<=20000 <= s.length <= 2000
  • s 中只有小写英文字母和括号
  • 我们确保所有括号都是成对出现的
双端队列

根据题意,我们可以设计如下处理流程:

  • 从前往后遍历字符串,将不是 ) 的字符串从「尾部」放入队列中
  • 当遇到 ) 时,从队列「尾部」取出字符串,直到遇到 ( 为止,并对取出字符串进行翻转
  • 将翻转完成后字符串重新从「尾部」放入队列
  • 循环上述过程,直到原字符串全部出来完成
  • 从队列「头部」开始取字符,得到最终的答案

可以发现,上述过程需要用到双端队列(或者栈,使用栈的话,需要在最后一步对取出字符串再进行一次翻转)。

Java 中,双端队列可以使用自带的 ArrayDeque, 也可以直接使用数组进行模拟。

代码(数组模拟双端队列):

class Solution {
    int N = 2010, he = 0, ta = 0;
    char[] d = new char[N], path = new char[N];
    public String reverseParentheses(String s) {
        int n = s.length();
        char[] cs = s.toCharArray();
        for (int i = 0; i < n; i++) {
            char c = cs[i];
            if (c == ')') {
                int idx = 0;
                while (he < ta) {
                    if (d[ta - 1] == '(' && --ta >= 0) {
                        for (int j = 0; j < idx; j++) d[ta++] = path[j];
                        break;
                    } else {
                        path[idx++] = d[--ta];
                    }
                }
            } else {
                d[ta++] = c;
            }
        }
        StringBuilder sb = new StringBuilder();
        while (he < ta) sb.append(d[he++]);
        return sb.toString();
    }
}
  • 时间复杂度:每个 ( 字符只会进出队列一次;) 字符串都不会进出队列,也只会被扫描一次;分析的重点在于普通字符,可以发现每个普通字符进出队列的次数取决于其右边的 ) 的个数,最坏情况下每个字符右边全是右括号,因此复杂度可以当做 O(n2)O(n^2),但实际计算量必然取不满 n2n^2,将普通字符的重复弹出均摊到整个字符串处理过程,可以看作是每个字符串都被遍历常数次,复杂度为 O(n)O(n)
  • 空间复杂度:O(n)O(n)

剑指 Offer II 041. 滑动窗口的平均值

给定一个整数数据流和一个窗口大小,根据该滑动窗口的大小,计算滑动窗口里所有数字的平均值。

实现 MovingAverage 类:

  • MovingAverage(int size) 用窗口大小 size 初始化对象。
  • double next(int val) 成员函数 next 每次调用的时候都会往滑动窗口增加一个整数,请计算并返回数据流中最后 size 个值的移动平均值,即滑动窗口里所有数字的平均值。

示例:

输入:
inputs = ["MovingAverage", "next", "next", "next", "next"]
inputs = [[3], [1], [10], [3], [5]]

输出:
[null, 1.0, 5.5, 4.66667, 6.0]

解释:
MovingAverage movingAverage = new MovingAverage(3);
movingAverage.next(1); // 返回 1.0 = 1 / 1
movingAverage.next(10); // 返回 5.5 = (1 + 10) / 2
movingAverage.next(3); // 返回 4.66667 = (1 + 10 + 3) / 3
movingAverage.next(5); // 返回 6.0 = (10 + 3 + 5) / 3

提示:

  • 1<=size<=10001 <= size <= 1000
  • 105<=val<=105-10^5 <= val <= 10^5
  • 最多调用 next 方法 10410^4
双端队列

根据题意,我们可以使用变量 n 将初始化传入的 size 进行转存,同时使用「双端队列」来存储 next 所追加的值(添加到队列尾部),当双端队列所包含元素超过规定数量 n 时,我们从队列头部进行 pop 操作,整个维护过程使用变量 sum 记录当前包含的元素和。

利用 next 操作最多被调用 10410^4 次,我们可以使用直接开个 10410^4 数组来充当双端队列,使用两指针 ji 分别指向队列的头部和尾部。

Java 代码:

class MovingAverage {
    int[] arr = new int[10010];
    int n, sum, j, i;
    public MovingAverage(int size) {
        n = size;
    }
    public double next(int val) {
        sum += arr[i++] = val;
        if (i - j > n) sum -= arr[j++];
        return sum * 1.0 / (i - j);
    }
}

C++ 代码:

class MovingAverage {
private:
    int arr[10010];
    int n, sum, j, i;

public:
    MovingAverage(int size) : n(size), sum(0), j(0), i(0) {}

    double next(int val) {
        sum += arr[i++] = val;
        if (i - j > n)
            sum -= arr[j++];
        return sum * 1.0 / (i - j);
    }
};

Python 代码:

class MovingAverage:
    def __init__(self, size):
        self.arr = [0] * 10010
        self.n = size
        self.sum = 0
        self.j = 0
        self.i = 0

    def next(self, val):
        self.sum += val
        self.arr[self.i] = val
        self.i += 1
        if self.i - self.j > self.n:
            self.sum -= self.arr[self.j]
            self.j += 1
        return self.sum / (self.i - self.j)

GO 代码:

type MovingAverage struct {
    arr  [10010]int
    n    int
    sum  int
    j, i int
}

func Constructor(size int) MovingAverage {
    return MovingAverage{n: size}
}

func (m *MovingAverage) Next(val int) float64 {
    m.sum += val
    m.arr[m.i] = val
    m.i++
    if m.i-m.j > m.n {
        m.sum -= m.arr[m.j]
        m.j++
    }
    return float64(m.sum) / float64(m.i-m.j)
}

TypeScript 代码:

class MovingAverage {
    private arr: number[] = new Array(10010);
    private n: number;
    private sum: number;
    private j: number;
    private i: number;

    constructor(size: number) {
        this.n = size;
        this.sum = 0;
        this.j = 0;
        this.i = 0;
    }

    public next(val: number): number {
        this.sum += val;
        this.arr[this.i++] = val;
        if (this.i - this.j > this.n)
            this.sum -= this.arr[this.j++];
        return this.sum / (this.i - this.j);
    }
}

  • 时间复杂度:O(m)O(m),其中 mmnext 操作的调用次数
  • 空间复杂度:O(n)O(n)

总结

与数组、链表等数据结构类型,队列同样是线性类型的数据结构。

其具先进先出的特性,往往与后进先出的栈共同提及,队列作为基础数据结构往往出现在中小厂的面试/笔试题中,需要重点掌握。