非科班学习算法day10 | LeetCode232:用栈实现队列 ,Leetcode225:用队列实现栈 ,Leetcode20:有效括号,LeetCode1047:删除字符串中的所有相邻项
目录
介绍
包含LC的四道题目,还有相应概念的补充。暂时告别了复杂的字符串处理,来到了栈和队列的应用。这两者的特性都是一定的,常用的函数也就那几个,主要是控制怎么模拟出正确的过程。
相关图解和更多版本:
代码随想录 (programmercarl.com)https://programmercarl.com/#%E6%9C%AC%E7%AB%99%E8%83%8C%E6%99%AF
一、基础概念补充:
1.c++中的栈
C++中的栈(stack)是一种后进先出(Last In, First Out, LIFO)的数据结构,它只允许在一端进行插入和删除操作。这一端称为栈顶,另一端称为栈底。C++标准库提供了<stack>
头文件,其中定义了std::stack
容器适配器来实现栈的功能。std::stack
本身不是一个容器,而是基于其他容器(如std::vector
、std::deque
或std::list
,默认是std::deque
)实现的。
常用函数
-
构造函数
stack();
// 默认构造函数,创建一个空栈。explicit stack(const Container& cont);
// 使用给定的容器cont来初始化栈。
-
元素访问
top();
// 返回栈顶元素的引用,但不移除它。如果栈为空,则行为未定义。
-
容量
- 由于
std::stack
是基于其他容器实现,直接访问容量相关函数(如大小size)通常需要通过底层容器的接口间接实现,标准库中没有直接提供栈容量相关的成员函数。
- 由于
-
修改器
push(const value_type& x);
// 在栈顶添加一个元素x。push(value_type&& x);
// C++11起,支持右值引用,高效地将x推入栈顶。pop();
// 移除栈顶元素,无返回值。
-
状态检查
empty();
// 如果栈为空,返回true;否则,返回false。 // 注意:没有直接提供size()函数,但可以通过底层容器的size()方法间接获取栈的大小。
2.c++中的队列
C++中的队列(queue)是一种先进先出(First In, First Out, FIFO)的数据结构,它允许在队尾插入元素,在队头删除元素。C++标准库在<queue>
头文件中提供了std::queue
容器适配器来实现队列功能,同样地,std::queue
也是基于其他容器(默认是std::deque
)实现的,并不直接管理内存。
常用函数
-
构造函数
queue();
// 默认构造函数,创建一个空队列。explicit queue(const Container& cont);
// 使用给定的容器cont来初始化队列。
-
元素访问
front();
// 返回队头元素的引用,但不移除它。如果队列为空,则行为未定义。back();
// 返回队尾元素的引用,不移除它。如果队列为空,则行为未定义。
-
容量
- 同栈一样,直接的容量查询不是
std::queue
的标准成员函数,但可通过底层容器接口间接实现。
- 同栈一样,直接的容量查询不是
-
修改器
push(const value_type& x);
// 在队尾添加一个元素x。push(value_type&& x);
// C++11起,支持右值引用,高效地将x加入队尾。pop();
// 移除队头元素,无返回值。
-
状态检查
empty();
// 如果队列为空,返回true;否则,返回false。 // 注意:没有直接提供size()函数,但可以通过底层容器的size()方法间接获取队列的大小。
二、LeetCode题目
1.LeetCode232: 用栈实现队列
题目链接:232. 用栈实现队列 - 力扣(LeetCode)
题目解析
因为栈是单个开口的,所以要用栈实现队列,就要借助两个栈。这两个栈,一个用来模拟进栈,另一个用来模拟出栈。
c++代码如下:
class MyQueue {
public:
//建立进出栈模拟
stack<int> stIn;
stack<int> stOut;
MyQueue() {
}
void push(int x)
{
stIn.push(x);
}
int pop()
{
if(stOut.empty())
{
while(!stIn.empty())
{
stOut.push(stIn.top());
stIn.pop();
}
}
int result = stOut.top();
stOut.pop();
return result;
}
int peek()
{
int result = this->pop();
stOut.push(result);
return result;
}
bool empty()
{
if(stIn.empty() && stOut.empty())
{
return true;
}
return false;
}
};
/**
* Your MyQueue object will be instantiated and called as such:
* MyQueue* obj = new MyQueue();
* obj->push(x);
* int param_2 = obj->pop();
* int param_3 = obj->peek();
* bool param_4 = obj->empty();
*/
注意点1:出队列的模拟是借助出栈来实现,不过显然,队列和栈的进出顺序不一样,所以,将进栈的元素依次弹出并添加到出栈当中。
注意点2:获取队列头的元素和弹出队列头元素的操作很像,只不过就是弹出和不弹出的区别,所以这里调用了之前写的函数,然后再进一步操作,很大程度上缩减了代码,并且更利于维护。
2.Leetcode225:用队列实现栈
题目链接:225. 用队列实现栈 - 力扣(LeetCode)
题目解析
借用和上一道题相似的思路,可以建立两个队列来模拟栈
C++代码如下:
class MyStack {
public:
//创建两个队列
queue<int>que1;
queue<int>que2;
MyStack() {
}
void push(int x)
{
que1.push(x);
}
int pop()
{
int size = que1.size();
while(size > 1)
{
que2.push(que1.front());
que1.pop();
size--;
}
int result = que1.front();
que1.pop();
while(!que2.empty())
{
que1.push(que2.front());
que2.pop();
}
return result;
}
int top()
{
return que1.back();
}
bool empty()
{
return que1.empty();
}
};
/**
* Your MyStack object will be instantiated and called as such:
* MyStack* obj = new MyStack();
* obj->push(x);
* int param_2 = obj->pop();
* int param_3 = obj->top();
* bool param_4 = obj->empty();
*/
3.Leetcode20:有效括号
题目解析
也是借助栈,采用好用的方法,会大大减少条件判断。问题就是,栈中的元素怎么处理?这里是采用遇到左括号,进栈对应的右括号;遇到右括号,弹栈。监控这个过程,如果发生了异常,就表示不成立。
C++代码如下:
class Solution {
public:
bool isValid(string s)
{
//剪枝
if(s.size() % 2 != 0)return false;
//建立栈
stack<char>st;
//进栈规则
for(int i = 0; i < s.size(); i++)
{
if(s[i] == '(') st.push(')');
else if(s[i] == '[') st.push(']');
else if(s[i] == '{') st.push('}');
else if(st.empty() || s[i] != st.top()) return false;
//()]]
else st.pop();
}
return st.empty();
}
};
注意点1:这里建立的栈存放的类型是字符,在c++中字符和字符串并不一样。
注意点2:我一开始写的条件是s[i] != st.top() || st.empty(),这样会造成如下问题:
这里有一个潜在的逻辑问题。理论上,在检查 s[i] != st.top() 之前应该先确认栈非空。但由于逻辑或(||)的短路特性,实际执行时:
如果先计算 s[i] != st.top(),在某些编译器/环境可能尝试访问空栈的顶部,这是未定义行为,可能引发运行时错误。
但实际上,由于 || 操作符的特性,计算机会直接跳过 s[i] != st.top() 的检查,直接评估 st.empty(),该部分为真(true),导致整个表达式结果为 true,正确地反映出栈为空的情况,而不实际执行对空栈顶的访问。
尽管在大多数现代编译器中,由于短路特性,第三种情况通常不会出现问题,但最佳实践仍然是先检查栈是否为空,以避免潜在的未定义行为,这也是为什么推荐使用 st.empty() || s[i] != st.top() 的原因。
总结一下就是,在要访问可能为空的容器的元素之前,最好先进行检查,&&同理。
注意点3:把这种问题理解成消除问题,这也是遇到左边括号,进栈相应的右边括号的原因,借助栈,减少了大量的条件判断,底层的原因就是因为,需要处理的元素之间有相应关系。
4.Leetcode1047:删除字符串中的所有相邻重复项
题目链接:1047. 删除字符串中的所有相邻重复项 - 力扣(LeetCode)
题目解析
根据对数组、字符串的了解,如果选择直接判断元素是否相同,然后对其原地操作,前向遍历会影响索引,所以非常难使用;后向遍历也要判断很多次,因为题目中提到了需要反复操作直到没有相邻重复的。
其实这个过程很像消消乐,遇到这种问题,最好的处理就是使用栈。
C++代码如下:
class Solution {
public:
string removeDuplicates(string s)
{
//建立栈
stack<char> st;
//进栈
for(auto ch : s)
{
if(!st.empty() && ch == st.top())
{
st.pop();
}
else
{
st.push(ch);
}
}
string new_s = "";
while(!st.empty())
{
new_s += st.top();
st.pop();
}
reverse(new_s.begin(), new_s.end());
return new_s;
}
};
注意点1:和上一道题的处理相似,或者说栈的处理都类似:看准当前栈顶的元素,看准当前需要处理的元素(未进栈),根据这两者做出判断
注意点2:维护的是栈的状态。
总结
栈多画一画图,还是比较好理解 0v0 ,打卡第10天,坚持!!!