Day10. 栈与队列: 232.用栈实现队列 225.用队列实现栈 20.有效的括号 1047.删除字符串中的所有相邻重复项

105 阅读9分钟

232.用栈实现队列

难度指数:😀🙂

题目链接:232.用栈实现队列

没有涉及具体的算法,考察对栈和队列的一些基本操作。

队列:入队出队

关键在模拟出队列的行为。

通过使用2个栈,来模拟队列的行为

在模拟队列弹出元素的时候,如何将元素弹出来:

1️⃣号栈 进栈的元素,其出栈的顺序并不是我们想要的,因此我们再借助一个栈 2️⃣号栈,改变元素的顺序

⚠️注意:1️⃣号栈 里面的元素一定要全部、一次性地转到 2️⃣号栈,这样模拟出队列的顺序才不会乱。

动画:

10.01.gif

代码思路:

 定义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 操作)”



10.01.gif

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️⃣种情况,我们在遍历这个字符串的时候,遇到了 ( ,就把一个对应的 ) 加入到栈里面。

因为到时候匹配的时候,直接弹出来,就可以和元素直接做比较,方便代码实现

(你要是傻傻地,遇到 ( 就把它放进栈里,但是弹出来的时候还要做判断:这个左括号对应的是右括号,而且还需要队类型进行判断。代码复杂些)

动画:

10.2.gif

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)


上一题是讲用栈来解决括号匹配问题,相邻的括号,即当左括号和右括号匹配了,做一个消除的动作。

用栈来解决:

本题是相邻的字母如果相同,做一个删除的动作。

用栈很合适

这个栈用来存我们遍历过的元素,每遍历一个字母,都要去栈里面询问遍历的前一个字母的是不是和这个字母相同。

10.3.gif 用字符串来模拟栈:

其实我们没有必要真的用一个栈,可以直接用一个字符串去模拟这个栈的行为,最后没有必要再把栈里的元素转成字符串。

灵魂发问:用字符串来模拟栈,那么栈里面的元素不也是反的吗?

答:未必。可以字符串的尾部作为栈的出口,头部作为栈底,最后的这个字符串就是我们要求的。

代码思路:

 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;
     }
 };

用字符串模拟栈,来完美地解决这个看似复杂的问题。

总结:

栈这种数据结构在计算机系统中有广泛的应用,特别擅长于处理相邻字母的,某些情况下做一些特殊判断。

例如:相邻地做括号匹配,相邻地做删除。