队列
对于数据结构而言,队列的重要性不言而喻,往往是笔试/面试向中的重中之重。
今天一起学习 道与队列相关的题目。
622. 设计循环队列
设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于 FIFO
(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。
循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。
你的实现应该支持如下操作:
MyCircularQueue(k)
: 构造器,设置队列长度为 。Front
: 从队首获取元素。如果队列为空,返回 。Rear
: 获取队尾元素。如果队列为空,返回 。enQueue(value)
: 向循环队列插入一个元素。如果成功插入则返回真。deQueue()
: 从循环队列中删除一个元素。如果成功删除则返回真。isEmpty()
: 检查循环队列是否为空。isFull()
: 检查循环队列是否已满。
提示:
- 所有的值都在 至 的范围内;
- 操作数将在 至 的范围内;
- 请不要使用内置的队列库。
数据结构
创建一个长度为 的数组充当循环队列,使用两个变量 he
和 ta
来充当队列头和队列尾(起始均为 ),整个过程 he
始终指向队列头部,ta
始终指向队列尾部的下一位置(待插入元素位置)。
两变量始终自增,通过与 取模来确定实际位置。
分析各类操作的基本逻辑:
isEmpty
操作:当he
和ta
相等,队列存入元素和取出元素的次数相同,此时队列为空;isFull
操作:ta - he
即队列元素个数,当元素个数为 个时,队列已满;enQueue
操作:若队列已满,返回 ,否则在nums[ta % k]
位置存入目标值,并将ta
指针后移;deQueue
操作:若队列为空,返回 ,否则将he
指针后移,含义为弹出队列头部元素;Front
操作:若队列为空,返回 ,否则返回nums[he % k]
队头元素;Rear
操作:若队列为空,返回 ,否则返回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;
}
}
- 时间复杂度:构造函数复杂度为 ,其余操作复杂度为
- 空间复杂度:
1190. 反转每对括号间的子串
给出一个字符串 s
(仅含有小写英文字母和括号)。
请你按照从括号内到外的顺序,逐层反转每对匹配括号中的字符串,并返回最终的结果。
注意,您的结果中 不应 包含任何括号。
示例 1:
输入:s = "(abcd)"
输出:"dcba"
提示:
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();
}
}
- 时间复杂度:每个
(
字符只会进出队列一次;)
字符串都不会进出队列,也只会被扫描一次;分析的重点在于普通字符,可以发现每个普通字符进出队列的次数取决于其右边的)
的个数,最坏情况下每个字符右边全是右括号,因此复杂度可以当做 ,但实际计算量必然取不满 ,将普通字符的重复弹出均摊到整个字符串处理过程,可以看作是每个字符串都被遍历常数次,复杂度为 - 空间复杂度:
剑指 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
提示:
- 最多调用
next
方法 次
双端队列
根据题意,我们可以使用变量 n
将初始化传入的 size
进行转存,同时使用「双端队列」来存储 next
所追加的值(添加到队列尾部),当双端队列所包含元素超过规定数量 n
时,我们从队列头部进行 pop
操作,整个维护过程使用变量 sum
记录当前包含的元素和。
利用 next
操作最多被调用 次,我们可以使用直接开个 数组来充当双端队列,使用两指针 j
和 i
分别指向队列的头部和尾部。
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);
}
}
- 时间复杂度:,其中 为
next
操作的调用次数 - 空间复杂度:
总结
与数组、链表等数据结构类型,队列同样是线性类型的数据结构。
其具先进先出的特性,往往与后进先出的栈共同提及,队列作为基础数据结构往往出现在中小厂的面试/笔试题中,需要重点掌握。