代码随想录算法训练营第10天
代码随想录算法训练营第10天 | 栈与队列理论基础、232.用栈实现队列、225. 用队列实现栈、20. 有效的括号、1047. 删除字符串中的所有相邻重复项
一、刷题部分
1.1 栈与队列理论基础
- 原文链接:代码随想录
- 题目链接:🈚️
栈是先进后出,队列是先进先出。
在 C++ 中,栈和队列是 STL 里面的两个数据结构,然而不同的 STL 版本是有区别的:
- HP STL:其他版本的 C++ STL,一般是以 HP STL 为蓝本实现出来的,HP STL 是 C++ STL 的第一个实现版本,而且开放源代码。
- P.J.Plauger STL 由 P.J.Plauger 参照 HP STL 为蓝本实现出来的,被 Visual C++ 编译器所采用,不是开源的。
- SGI STL 由 Silicon Graphics Computer System 公司参照 HP STL 实现,被 Linux 的 C++ 编译器 GCC 所采用,SGL STL 是开源软件,源码可读性甚高。
接下来介绍的栈与队列也是 SGI STL 里面的数据结构:
栈
栈提供 push 和 pop 接口,所有元素必须符合先进后出规则,所以栈不能提供走访功能,也不提供迭代器。
栈以底层容器完成所有工作,对外提供统一接口,底层容器是可插拔的(可以控制使用哪种容器来实现)。因此 STL 中的栈往往不算作容器,而是叫做 container adapter (容器适配器).
以下示意图表示了 STL 栈与容器的关系:
SGI STL 默认使用 deque 来实现栈。deque 是一个双向队列,只要封住一端就可以当做栈来使用。
若要手动指定 vector 为栈的底层实现,初始化语句:
std::stack<int, std::vector<int>> third; //使用 vector 为底层容器的栈
队列
与前面的栈基本一样,也是默认用 deque 为底层数据结构,也可以指定 list 为底层实现:
std::queue<int, std::list<int>> third; //使用 list 为底层容器的队列
1.2 232.用栈实现队列
- 原文链接:代码随想录
- 题目链接:232. 用栈实现队列 - 力扣(LeetCode)
1.2.1 题目描述
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push
、pop
、peek
、empty
):
实现 MyQueue
类:
void push(int x)
将元素 x 推到队列的末尾int pop()
从队列的开头移除并返回元素int peek()
返回队列开头的元素boolean empty()
如果队列为空,返回true
;否则,返回false
说明:
- 你 只能 使用标准的栈操作 —— 也就是只有
push to top
,peek/pop from top
,size
, 和is empty
操作是合法的。 - 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。
示例 1:
输入:
["MyQueue", "push", "push", "peek", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 1, 1, false]
解释:
MyQueue myQueue = new MyQueue();
myQueue.push(1); // queue is: [1]
myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue)
myQueue.peek(); // return 1
myQueue.pop(); // return 1, queue is [2]
myQueue.empty(); // return false
提示:
1 <= x <= 9
- 最多调用
100
次push
、pop
、peek
和empty
- 假设所有操作都是有效的 (例如,一个空的队列不会调用
pop
或者peek
操作)
进阶:
- 你能否实现每个操作均摊时间复杂度为
O(1)
的队列?换句话说,执行n
个操作的总时间复杂度为O(n)
,即使其中一个操作可能花费较长时间。
1.2.2 初见思路
这题貌似在本科学习数据结构的时候做过,现在回忆一下思路:
用 stack1 的栈口放数据,用 stack2 的栈口出数据。
push:先检查 stack2 是否为空,如果为空则将 stack1 里的数据倒入 stack2,否则不动。之后将数据 push 到 stack1 里。
pop:先检查 stack2 是否为空,如果为空则将 stack1 里的数据倒入 stack2,否则不动。之后将 stack2 里的数据 pop 出来。
peek:先检查 stack2 是否为空,如果为空则将 stack1 里的数据倒入 stack2,否则不动,之后将 stack2 顶端元素返回。
empty:先检查 stack2 是否为空,如果为空则将 stack1 里的数据倒入 stack2,否则不动。之后再检查 stack2 是否为空,如果为空则返回 true,否则返回 false。
思路有了接下来写一下成员对象:
private:
stack<int> s1;
stack<int> s2;
实现栈的转移(包含条件判断):
void transfer() {
if (!s2.empty()) {
return;
}
//实现栈的转移,是一个基本操作
while(!s1.empty()) {
s2.push(s1.top());
s1.pop();
}
}
实现push
void push(int x) {
transfer();
s1.push(x);
}
实现pop
int pop() {
transfer();
int res = s2.top();
s2.pop();
return res;
}
实现peek
int peek() {
transfer();
return s2.top();
}
实现empty
bool empty() {
transfer();
return s2.empty();
}
经测试,代码顺利通过力扣测试。接下来看一下录。
1.2.3 正式做题
与我的思路基本一致。
1.2.4 遇到的困难
🈚️
1.2.5 本题总结
本题拓展部分讲了在工业生产中,遇到重复的逻辑应该是将这个逻辑抽象成一个函数然后重复调用,而不是复制粘贴代码。
1.3 225. 用队列实现栈
- 原文链接:代码随想录
- 题目链接:225. 用队列实现栈 - 力扣(LeetCode)
1.3.1 题目描述
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push
、top
、pop
和 empty
)。
实现 MyStack
类:
void push(int x)
将元素 x 压入栈顶。int pop()
移除并返回栈顶元素。int top()
返回栈顶元素。boolean empty()
如果栈是空的,返回true
;否则,返回false
。
注意:
- 你只能使用队列的标准操作 —— 也就是
push to back
、peek/pop from front
、size
和is empty
这些操作。 - 你所使用的语言也许不支持队列。 你可以使用 list (列表)或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。
示例:
输入:
["MyStack", "push", "push", "top", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 2, 2, false]
解释:
MyStack myStack = new MyStack();
myStack.push(1);
myStack.push(2);
myStack.top(); // 返回 2
myStack.pop(); // 返回 2
myStack.empty(); // 返回 False
提示:
1 <= x <= 9
- 最多调用
100
次push
、pop
、top
和empty
- 每次调用
pop
和top
都保证栈不为空
进阶:你能否仅用一个队列来实现栈。
1.3.2 初见思路
先想想不进阶的题目,也就是用两个队列该怎么做:
由于单向队列不可能实现元素的逆序,因此类似于用队列实现栈的思路是不可行的。我简单设想了这样的方法,其实把队列当成栈那么最重要的是取数的时候应该取队尾的数,那么可以用一个队列主要存储内容,每次 栈push 操作就对该队列push,然后另一个队列作为辅助队列,每一次栈pop的时候,将第一个队列里的数全都按序pop到辅助队列只留最后一个元素,然后pop出来就行。
写一下代码:
先定义两个队列:
private:
queue<int> q1;
queue<int> q2;
实现push:
void push(int x) {
q1.push(x);
}
实现pop:
int pop(){
while (q1.size() > 1) {
q2.push(q1.front());
q1.pop();
}
int res = q1.front();
q1.pop();
while (!q2.empty()) {
q1.push(q2.front());
q2.pop();
}
return res;
}
实现top:
int top(){
int res;
while (!q1.empty()) {
res = q1.front();
q2.push(res);
q1.pop();
}
while (!q2.empty()) {
q1.push(q2.front());
q2.pop();
}
return res;
}
实现empty:
bool empty() {
if (q1.empty()) {
return true;
}
return false;
}
通过了力扣的测试。
其实在写这个代码的时候就已经想到了怎么来用一个队列做了,就是将队头元素pop出来之后立即push回去,这样就不需要一个辅助队列了。
class MyStack {
private:
queue<int> q;
public:
MyStack() {
}
void push(int x) {
q.push(x);
}
int pop(){
int size = q.size();
int res;
while (size > 1) {
//将队头pop出后立即push回来
res = q.front();
q.pop();
q.push(res);
size--;
}
//最后一个元素就丢掉了
res = q.front();
q.pop();
return res;
}
int top(){
int size = q.size();
int res;
while (size > 0) {
res = q.front();
q.pop();
q.push(res);
size--;
}
return res;
}
bool empty() {
if (q.empty()) {
return true;
}
return false;
}
};
自然也是通过了力扣的测试。下面看看录里怎么讲的。
1.3.3 正式做题
双队列的思路与我自己写的基本一致。
单队列的思路与我自己写的也是基本一致。
1.3.4 遇到的困难
🈚️
1.3.5 本题总结
还是需要动点脑筋的,直接生搬硬套栈模拟队列的思路确实做不出来这道题。
1.4 20. 有效的括号
- 原文链接:代码随想录
- 题目链接:20. 有效的括号 - 力扣(LeetCode)
1.4.1 题目描述
给定一个只包括 '('
,')'
,'{'
,'}'
,'['
,']'
的字符串 s
,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
示例 1:
输入:s = "()"
输出:true
示例 2:
输入:s = "()[]{}"
输出:true
示例 3:
输入:s = "(]"
输出:false
示例 4:
输入:s = "([])"
输出:true
提示:
1 <= s.length <= 104
s
仅由括号'()[]{}'
组成
1.4.2 初见思路
显然是需要使用栈的,思路大致如下:
使用一个栈来存储字符,每次遇到一个左括号就压栈,每次遇到一个右括号就检查栈顶元素是否匹配,匹配就出栈,否则直接返回不匹配;进行到最后如果栈不为空也是不匹配,栈为空说明匹配。具体写代码的时候可以给括号映射成数字,这样更好判断。
先做好映射,可以使用 map:
map<char,int> myMap;
myMap['('] = -1;
myMap['['] = -2;
myMap['{'] = -3;
myMap[')'] = 1;
myMap[']'] = 2;
myMap['}'] = 3;
定义好栈:
stack<int> myStack;
遍历string:
for (char c : s) {
}
左括号压栈:
if (myMap[c] < 0) {
myStack.push(myMap[c]);
}
右括号检查:
else {
if (myStack.empty() || (myMap[c] + myStack.top() != 0)) {
return false;
}
else {
myStack.pop();
}
}
最后检查栈空与否:
if (myStack.empty()) {
return true;
}
return false;
代码如下:
class Solution {
public:
bool isValid(string s) {
map<char,int> myMap;
myMap['('] = -1;
myMap['['] = -2;
myMap['{'] = -3;
myMap[')'] = 1;
myMap[']'] = 2;
myMap['}'] = 3;
stack<int> myStack;
for (char c : s) {
if (myMap[c] < 0) {
myStack.push(myMap[c]);
}
else {
if (myStack.empty() || (myMap[c] + myStack.top() != 0)) {
return false;
}
else {
myStack.pop();
}
}
}
if (myStack.empty()) {
return true;
}
return false;
}
};
1.4.3 正式做题
思路一致。
1.4.4 遇到的困难
在写的时候没有考虑到出现右括号的时候栈为空的情况,判题报错了才发现这个问题。
1.4.5 本题总结
主要还是需要再做题之前先把几种可能得情况考虑清楚再来做题,这样再问题没那么容易看出来的时候,会很有意义。
1.5 1047. 删除字符串中的所有相邻重复项
1.5.1 题目描述
给出由小写字母组成的字符串 s
,重复项删除操作会选择两个相邻且相同的字母,并删除它们。
在 s
上反复执行重复项删除操作,直到无法继续删除。
在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
示例:
输入:"abbaca"
输出:"ca"
解释:
例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 "aaca",其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"。
提示:
1 <= s.length <= 105
s
仅由小写英文字母组成。
1.5.2 初见思路
可以借助栈来一次做完。本题没有太多好说的,直接开写。
//定义栈
stack<char> st;
//定义返回值
string res;
//遍历字符串
for (char c : s) {
if (st.empty() || st.top() != c) {
st.push(c);
}
else {
st.pop();
}
}
//收尾工作
while (!st.empty()) {
res += st.top();
st.pop();
}
res = res.reverse();
return res
完整代码:
class Solution {
public:
string removeDuplicates(string s) {
//定义栈
stack<char> st;
//定义返回值
string res;
//遍历字符串
for (char c : s) {
if (st.empty() || st.top() != c) {
st.push(c);
}
else {
st.pop();
}
}
//收尾工作
while (!st.empty()) {
res += st.top();
st.pop();
}
reverse(res.begin(), res.end());
return res;
}
};
1.5.3 正式做题
思路一致。
1.5.4 遇到的困难
🈚️
1.5.5 本题总结
本题和上一题基本一样的思路,算是展示了栈的一些基本用法。
二、总结与回顾
今天简单了解了一下栈和队列的使用,整体难度不高。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性