# 一、理论基础
栈是先进后出,队列是先进先出,二者在 STL 中都是容器适配器,其底层实现可依靠 deque、list 等容器。
# 二、题目实践
# 1、232. 用栈实现队列
1 | class MyQueue { |
官方题解比我简单一些。其实我不用每次 peek 和 pop 之后强迫症把数据又倒回 stack1。入队的时候无脑入栈 stack1,出队的时候,如果 stack1 非空,先把 stack1 倒到 stack2,再从 stack2 顶端 pop 即可。如果 stack1 为空,那就直接从 stack2 顶端 pop:
1 |
|
# 2、225. 用队列实现栈
这题在做的时候相比上一题花了我好多时间,主要是感觉有一丢丢绕。
- 思路 1:
我的做法是两个队列来回倒腾,每一次 pop 和 top 都要把数据从 in 倒腾到 out,把 in 的最后一个标记住、处理,然后再把 out 倒腾回 in。其实比较麻烦,我还得有一个 size 来记录当前一共有多少元素,以定位到 in 的最后一个元素:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52class MyStack {
public:
MyStack() {
//
}
void push(int x) {
_que1.push(x);
_size++;
}
int pop() {
int ret = -1;
while(_size != 1) {
ret = _que1.front();
_que1.pop();
--_size;
_que2.push(ret);
} ret = _que1.front();
_que1.pop();
--_size;
while(!_que2.empty()) {
_que1.push(_que2.front());
_que2.pop();
++_size;
} return ret;
}
int top() {
int ret = -1;
while(!_que1.empty()) {
ret = _que1.front();
_que1.pop();
_que2.push(ret);
}
while(!_que2.empty()) {
_que1.push(_que2.front());
_que2.pop();
} return ret;
}
bool empty() {
return _size == 0;
}
private:
std::queue<int, deque<int>> _que1;
std::queue<int, deque<int>> _que2;
int _size = 0;
};
但其实不用这样。
思路 2:
每次 push 进 in,然后再把 out 里的倒腾回来,这样就倒序了。
然后每次都 swap 一下 in 和 out,确保如果有数据的话都在 out 里:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48class MyStack {
public:
MyStack() {
}
void push(int x) {
m_qIn.push(x);
while(!m_qOut.empty()) {
int top = m_qOut.front();
m_qOut.pop();
m_qIn.push(top);
}
swap(m_qIn, m_qOut);
// while(!m_qIn.empty()) {
// int top = m_qIn.front();
// m_qIn.pop();
// m_qOut.push(top);
// }
}
int pop() {
if(!m_qOut.empty()) {
int ret = m_qOut.front();
m_qOut.pop();
return ret;
} else {
return -1;
}
}
int top() {
if(!m_qOut.empty()) {
return m_qOut.front();
} else {
return -1;
}
}
bool empty() {
return (m_qOut.empty());
}
private:
queue<int> m_qIn;
queue<int> m_qOut;
};思路 3:
还有一种方法更厉害,只需要用到一个 queue 就搞定了。思路就是,既然你 queue 是先进先出,那么我 pop 出去之后立马又 push 进来,然后当我 pop 了 len - 1 个元素之后,接下来再 pop 出来的就是目标值了:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43class MyStack {
public:
MyStack() {
}
void push(int x) {
m_queue.push(x);
}
int pop() {
int length = m_queue.size();
for(int i = 0; i < length - 1; ++i) {
int front = m_queue.front();
m_queue.pop();
m_queue.push(front);
}
int ret = m_queue.front();
m_queue.pop();
return ret;
}
int top() {
int length = m_queue.size();
int ret = -1;
for(int i = 0; i < length; ++i) {
int front = m_queue.front();
if(i == length-1) ret = front;
m_queue.pop();
m_queue.push(front);
} return ret;
}
bool empty() {
return (m_queue.empty());
}
private:
queue<int> m_queue;
};
# 3、20. 有效的括号
#栈
这里我用一个 vector 模拟一个栈(因为我觉得没必要用到 stack),每次遇到左括号就压栈,遇到右括号就出栈,并且判断出栈的左括号与当前右括号是否匹配。一次遍历之后,如果括号都是有效的,则栈理应为空:
1 | class Solution { |
这里注意,vector 判空是有 empty 的,太久没写都生疏了。
还有一种不用栈的思路,就是,我们知道对于有效的括号来说,一定会有至少一对括号是紧邻着出现的,所以我们可以在字符串中寻找然后替换为空字符串,以消除。直到无法消除为止,此时判断字符串是否为空:
1 | class Solution { |
# 4、1047. 删除字符串中的所有相邻重复项
#栈
1 | class Solution { |
同样的思路,但是官方写法更精简:
1 | class Solution { |
因为 string 可以用 back 来获取末端元素!这就体现了对 API 的熟练程度了!
# 5、150. 逆波兰表达式求值
#栈
思路也是很显而易见 —— 遇到符号之后对栈顶两个元素进行运算:
这种情况下,根本不用考虑遇到连续运算符,反正一遇到运算符就计算并压入新的计算结果。
1 | class Solution { |
# 6、239. 滑动窗口最大值
#单调队列
这题有点难,不愧是 hard 题。
一开始我想的是用一个 deque 来模拟一个普通队列,然后每次迭代时一进一出,然后利用 max_element 对 deque 元素进行求最大值的操作。但是这样的话,其实和两层 for 循环进行暴力解没什么区别。
后来我也有想到优先队列,维护一个大顶堆。但是,这样虽然能够求得最大值,却无法在滑动窗口时选择该弹出哪个元素。
我也有想过维护一个 max 变量,和其相应的索引。然后试图通过判断右边移入元素和左边移出元素和我记录的 max 变量以及 max 索引的关系来解题,但是我没办法判断窗口中 “第二大”、“第三大” 等元素是什么,所以有可能在最左端移除了最大值,最右端进来的不是更大的值,则此时窗口中的最大值应该是原本窗口中的第二大值,但我无法索引。
最终看了题解,才知道要维护一个单调队列。有一点点抽象,但仔细思考之后还是能够理解的:
- 思路 1:
我们自定义一个单调队列,它应该具备以下条件:
该队列单调递减,队头元素(左边)始终最大,队尾元素(右边)始终最小。
则,当每一次有新的元素入队时,将该元素与队尾(右边)元素进行比较,把小于该值的元素从队列中 pop 出来,最终再把该元素 push 进去。这样,每次 push 之后,都能确保队列中的元素始终单调递减。
同时,每一次要将窗口最左边的元素出队时,我们需要判断该元素是否为队头(左边)元素 —— 因为如果它不是队头,说明它早就在我们的 push 操作时就被 pop 出来了,就不需要重复 pop 一个不存在的元素了。
这样做的目的是出于这样的一个思想:当窗口中存在一个最大值时,我们滑动窗口更新最大值时,要么是原本最大值的右侧出现了新的最大值,要么就是原本最大值的右侧始终没有新的最大值出现,原最大值滑出窗口,原最大值右边的第二大值继任成为最大值。第二大值只可能从最大值的右边出现。所以,基于此,我们在 push 时就可以把小于当前值的队尾元素 pop 出来。
1 | class Solution { |
- 思路 2:
看了官方题解,发现用优先队列也可以(也确实因为 “最大值” 让人最直观地联想到优先队列)。
用一个大顶堆来记录最大值,这个大家都知道。但是,如上文所述,要解决窗口右滑时如何从优先队列中弹出窗口最左侧元素的问题。
这里我们在优先队列中存放的就不只是值,顺便把该值在 nums 中的索引给存入,形成一个 pair 键值对。
我们的优先队列根据 nums[i]
进行排序,可得最大值。然后我们右滑时,如果堆顶元素的索引小于窗口左端即将滑出的索引,则弹出。否则不用操作。(因为我们只考虑最大值。如果一个数值已经滑出窗口了,但它不在优先队列的堆顶,无所谓 —— 反正我们只会用到堆顶,后续总会有机会把它 pop 出来的)
1 | class Solution { |
# 7、347. 前 K 个高频元素
我想到的思路是,先用 map 一次遍历得到每个元素的频率,然后遍历这个 map,将其每个元素(键值对)压入优先队列中,以值为键,以键为值,利用优先队列的自动排序特性进行排序。最后再把优先队列的前 k 个元素 pop 出来压入数组中作为结果进行返回即可:
1 | class Solution { |
看了一眼官方题解,思路差不多,但是它维护的是一个最小堆的优先队列,然后严格控制队列的大小为 k,最后再无脑把整个队列里的所有数据 pop 出来。思路大差不差,但是性能理论上会比我的解法优很多。
网友说:
“如果用最大堆,所有元素压入堆,复杂度是 nlogn,而且浪费内存。如果是用最小堆,则复杂度是 nlogk”