232.用栈实现队列
难度指数:😀🙂
题目链接:232.用栈实现队列
没有涉及具体的算法,考察对栈和队列的一些基本操作。
队列:入队 和 出队
关键在模拟出队列的行为。
通过使用2个栈,来模拟队列的行为
在模拟队列弹出元素的时候,如何将元素弹出来:
1️⃣号栈 进栈的元素,其出栈的顺序并不是我们想要的,因此我们再借助一个栈 2️⃣号栈,改变元素的顺序
⚠️注意:
1️⃣号栈里面的元素一定要全部、一次性地转到2️⃣号栈,这样模拟出队列的顺序才不会乱。
动画:
代码思路:
定义2个栈 stack_in、stack_out
void push(int x)
{
stack_in.push(x);
}
int pop(int x) //出栈的时候pop顺便把弹出元素的数值返回
{
//先判断要出栈的这个栈里面是否为空
if (stack_out.empty()) { //若为空
//把入栈里面所有的元素都加到出栈里面
while (!stack_in.empty()) {
stack_out.push(stack_in.top()); //出栈获取到元素
stack_in.pop();
}
}
int result = stack_out.top(); //result获取栈里面的第1个元素
stack_out.pop(x); //把第1个元素弹出
return result;
}
这样就实现了 pop的操作。
总结:用2个栈实现队列的操作,如果要弹出的话,判断这个出栈是否为空,如果为空,就把入栈的所有元素加到出栈,然后在出栈弹出元素;
如果出栈本来就不为空,那么就直接从出栈弹出元素就可以了。
说一嘴:
题目信息:“你 只能 使用标准的栈操作 —— 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。”
所以,一些非法操作就不用去做检验了,比如:队列里面本来就是空了,你还去调用 pop 操作,这是一个非法操作,本来应该做一个对应的判断的。
但是这道题目就为了方便,已经给你排除这方面的麻烦,无需再做这方面的判断了。
“假设所有操作都是有效的 (例如,一个空的队列不会调用 pop 或者 peek 操作)”
peek函数:获取队列里面出口处的第一个元素。
也可以说是取队列里面的第一个元素,只不过不需要弹出来,只是单纯想要得到这个元素的数值。
在模拟这个行为时,还是要从出栈里面找元素,那么就还需要走上面代码的流程。
因此peek()和pop()的大部分代码是重复的,可以复用:
peek()函数下的result: result = this->pop(); (在同个类下,this可以调用pop()) ,这样就实现了代码的复用。
因此,就获取到第一个元素的数值,但同时也把它弹出来了,因此需要把它push回去。 (🐤我们只是想查询它的数值而已!)
int peek()
{
result = this->pop();
stack_out.push(result);
return result;
}
好奇发问:元素又弹出,又放回,会不会增加时间复杂度?
答:并不会!这只是操作了一个元素而已。这个操作的时间复杂度只是一个常数项,并不影响整个算法的性能。
这个peek()的写法还是挺简洁的。
扩展:
开发大型项目时,函数复用很重要!!!
实现一个类时,发现某个函数在另一个类里有现成的,但是你调用另一个类又感觉很麻烦,你就会想把那个类里面的函数粘贴过来,
然后你发现你的很多个类都需要这个函数
AC代码: (核心代码模式)
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(); //result接受出栈的第一个元素
stOut.pop(); //出栈弹出元素
return result;
}
int peek() {
int res = this->pop(); //代码复用
stOut.push(res); //需要push回出栈(我们只是想查询它的数值而已)
return res;
}
bool empty() {
return stIn.empty() && stOut.empty();
}
};
/**
* 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();
*/
225.用队列实现栈
难度指数:😀🙂
题目链接:225.用队列实现栈
很多人可能惯性思维:栈和队列出入的元素的顺序不同,那么用队列来实现栈是否也需要两个队列。
用两个队列确实可以模拟,但本视频的重点是用一个队列来模拟栈。
队列里有多少个元素,假如队列里有size个元素,就把 size - 1 个元素弹出来,重复加入,然后把最火一个元素再弹出来。
(我们就是要把最后一个元素前面的所有元素都弹出来,再重复加入,这样就可以实现把3弹出来)
这样就模拟了用一个队列来实现栈的进元素和出元素的行为。
代码思路:
push() 是最好实现的一个操作。
定义一个队列queue
void push(int x)
{
queue.push(x);
}
前面题目,栈去调用push(),那么这里队列也去调用push(),把元素x放到队列里面。
实现 pop() :
首先,需要获取队列的 size 。(因为要想把队列里面最后一个元素,就需要把最后一个元素前面的所有元素都弹出来,移到后面)
即弹出 size - 1 个元素
int pop()
{
size = queue.size();
size--; //size做一个自减
while (size--) { //把队列里面前size - 1个元素都弹出去
queue.push(queue.front()); //把弹出来的元素重新放回队列
//front仅仅是取了第一个元素,但没有弹出来,要注意做一个弹出
queue.pop();
}
result = queue.front();
queue.pop();
return result;
}
因此,我们就实现一个队列来模拟栈的出元素的行为。
实现 top() 函数:
在栈里面,获取到它的出口的第一个元素。
int top()
{
return queue.back();
}
- front队头,出元素
- back队尾,进元素
AC代码: (核心代码模式)
class MyStack {
public:
queue<int> que;
MyStack() {
}
void push(int x) {
que.push(x);
}
int pop() {
int size = que.size();
size--;
while (size--) { //队列中前size - 1个元素都弹出去
que.push(que.front()); //将队头出来的元素重新放回队列
que.pop();
}
int result = que.front();
que.pop();
return result;
}
int top() {
return que.back();
}
bool empty() {
return que.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();
*/
20.有效的括号
难度指数:😀🙂
题目链接:20.有效的括号
用栈来解决的经典题目。
初看挺复杂的,但其实不匹配的场景就3个: (各式各样的情况,这3种场景都包含了)
看看例子:
1️⃣ ( [ { } ] () ❌ 多余
2️⃣ [ { ( } } ] ❌ 左右类型不匹配
3️⃣ [ { } ] ( ) ) ) ) ❌ 多余的右括号
还有一种这个情况:
[ { ] } 其实还是属于上面的第2️⃣种情况
如何用栈结构来解决这3种不匹配的问题?
首先是第1️⃣种情况,我们在遍历这个字符串的时候,遇到了 ( ,就把一个对应的 ) 加入到栈里面。
因为到时候匹配的时候,直接弹出来,就可以和元素直接做比较,方便代码实现。
(你要是傻傻地,遇到 ( 就把它放进栈里,但是弹出来的时候还要做判断:这个左括号对应的是右括号,而且还需要队类型进行判断。代码复杂些)
动画:
1️⃣ ( [ { } ] () ❌ 多余
如果字符串遍历完成之后,但是栈不为空,说明不匹配。
2️⃣[ { ( } } ] ❌ 左右类型不匹配
栈顶的元素和当前遍历的 } 不匹配
3️⃣[ { } ] ( ) ) ) ) ❌ 多余的右括号
遍历到 ) ,但是栈里面已经没有之前放进的 ( 所对应的元素了。 (右括号多了)
在代码中,将这3种不同的表现都写出来,这道题目就没啥问题了。
代码思路:
这道题是找匹配的括号,存在这样的特性:
如果是全部都匹配的括号,那么这个字符串长度一定是偶数;
如果字符串长度是奇数的话,一定会存在不匹配的括号。
这里可以做一个剪枝
stack<char> st;
//剪枝
if (s.size() % 2 != 0) { //若字符串长度是奇数
return false;
}
//遍历字符串
for (i = 0; i < s.size(); i++) {
//遇到左括号的场景:
if (s[i] == '(') { //若遍历到 (
st.push(')'); //就把对应的 ) 放到栈里
}
else if (s[i] == '{') {
st.push('}');
}
else if (s[i] == '[') {
st.push(']');
}
//第3️⃣种情况:遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号 return false
//第2️⃣种情况:遍历字符串匹配的过程中,发现栈里没有我们要匹配的字符。所以return false
else if (st.empty() || st.top() != s[i]) { //若栈口的元素不等于当前遍历的元素
return false;
}
else { //栈口的元素和当前遍历的元素相等的情况下
st.pop();
}
// 第1️⃣种情况:我们已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false,否则就return true
return st.empty();
}
最后else if这里要重新看视频
AC代码: (核心代码模式)
class Solution {
public:
stack<char> st;
bool isValid(string s) {
//剪枝
if(s.size() % 2 != 0) {
return false;
}
//遍历字符串
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() || st.top() != s[i]) {
return false;
}
else {
st.pop();
}
}
return st.empty();
}
};
技巧性的东西没有固定的学习方法,还是要多看多练,自己灵活运用了。
1047.删除字符串中的所有相邻重复项
难度指数:😀😐
题目链接:1047.删除字符串中的所有相邻重复项
本题如果不知道用栈这种数据结构来解决的话,可能看起来会比较复杂。
复杂在于:不仅要找相邻的重复项,相邻的重复项删除之后,如果又有相邻的重复项,还要继续删除。
a b b a c a
暴力解决还是挺复杂的,至少2层for循环,O(n^2)
上一题是讲用栈来解决括号匹配问题,相邻的括号,即当左括号和右括号匹配了,做一个消除的动作。
用栈来解决:
本题是相邻的字母如果相同,做一个删除的动作。
用栈很合适
这个栈用来存我们遍历过的元素,每遍历一个字母,都要去栈里面询问遍历的前一个字母的是不是和这个字母相同。
用字符串来模拟栈:
其实我们没有必要真的用一个栈,可以直接用一个字符串去模拟这个栈的行为,最后没有必要再把栈里的元素转成字符串。
灵魂发问:用字符串来模拟栈,那么栈里面的元素不也是反的吗?
答:未必。可以字符串的尾部作为栈的出口,头部作为栈底,最后的这个字符串就是我们要求的。
代码思路:
string result; //定义字符串
for (char s : S) { //s取S这个字符串里面的每个字母
//遍历元素的时候,先和栈里面的元素进行比较
//如果栈本来就是空的,就应该把遍历的元素直接放进去 || 栈顶的元素和遍历的元素不同
if (result.empty() || s != result.back()) {
result.push_back(s); //就把元素放进字符串的尾部
}
else { //当前遍历的元素和栈里的元素相等
result.pop_back(); //从尾部弹出
}
}
return result;
AC代码: (核心代码模式)
class Solution {
public:
string removeDuplicates(string S) {
string result; //定义字符串
for (char s : S) {
if (result.empty() || s != result.back()) {
result.push_back(s);
}
else { //当前遍历的元素和栈里的元素相等
result.pop_back();
}
}
return result;
}
};
用字符串模拟栈,来完美地解决这个看似复杂的问题。
总结:
栈这种数据结构在计算机系统中有广泛的应用,特别擅长于处理相邻字母的,某些情况下做一些特殊判断。
例如:相邻地做括号匹配,相邻地做删除。