提示:DDU,供自己复习使用。欢迎大家前来讨论~
栈与队列Part01
了解栈与队列的内部实现机制
一、栈与队列理论基础
原理:
队列是先进先出,栈是先进后出。
栈和队列是STL(C++标准库)里面的两个数据结构。
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 Systems公司参照HP STL实现,被Linux的C++编译器GCC所采用,SGI STL是开源软件,源码可读性甚高。
栈提供push 和 pop 等等接口,所有元素必须符合先进后出规则,所以栈不提供走访功能,也不提供迭代器(iterator)。 不像是set 或者map 提供迭代器iterator来遍历所有元素。
队列中先进先出的数据结构,同样不允许有遍历行为,不提供迭代器, SGI STL中队列一样是以deque为缺省情况下的底部结构。
二、题目
题目一:232.用栈实现队列
解题思路:
使用栈来模式队列的行为,需要使用到两个栈,一个进,一个出,这样才可以达到先进先出的队列的特点。
细节:
一定要懂得复用,功能相近的函数要抽象出来,抽象成一个好用的函数或者工具类,不要大量的复制粘贴,很容易出问题!
class MyQueue {
public:
stack<int> stIn;
stack<int> stOut;
MyQueue() {}
void push(int x) { stIn.push(x); }
int pop() {
// 在输出栈为空时,添加元素
while (stOut.empty()) {
while (!stIn.empty()) {
stOut.push(stIn.top());
stIn.pop();
}
}
int res = stOut.top();
stOut.pop();
return res;
}
int peek() {
int res = this->pop();
stOut.push(res); // 因为pop函数弹出了元素res,所以再添加回去。
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();
*/
- 时间复杂度: push和empty为O(1), pop和peek为O(n)
- 空间复杂度: O(n)
题目二: 225. 用队列实现栈*
解题思路
2.1使用两个队列实现(惯性思维):
队列是先进先出的规则,把一个队列中的数据导入另一个队列中,数据的顺序并没有变,并没有变成先进后出的顺序。
用两个队列que1和que2实现队列的功能,que2其实完全就是一个备份的作用,把que1最后面的元素以外的元素都备份到que2,然后弹出最后面的元素,再把其他元素从que2导回que1。
class MyStack {
public:
queue<int> que1;
queue<int> que2; // 辅助队列,用来备份
/** Initialize your data structure here. */
MyStack() {}
/** Push element x onto stack. */
void push(int x) { que1.push(x); }
/** Removes the element on top of the stack and returns that element. */
int pop() {
int size = que1.size();
size--;
while (size--) { // 将que1 导入que2,但要留下最后一个元素
que2.push(que1.front());
que1.pop();
}
int result = que1.front(); // 留下的最后一个元素就是要返回的值
que1.pop();
que1 = que2; // 再将que2赋值给que1
while (!que2.empty()) { // 清空que2
que2.pop();
}
return result;
}
/** Get the top element.
** Can not use back() direactly.
*/
int top() {
int size = que1.size();
size--;
while (size--) {
// 将que1 导入que2,但要留下最后一个元素
que2.push(que1.front());
que1.pop();
}
int result = que1.front(); // 留下的最后一个元素就是要回返的值
que2.push(
que1.front()); // 获取值后将最后一个元素也加入que2中,保持原本的结构不变
que1.pop();
que1 = que2; // 再将que2赋值给que1
while (!que2.empty()) {
// 清空que2
que2.pop();
}
return result;
}
/** Returns whether the stack is empty. */
bool empty() { return que1.empty(); }
};
-
时间复杂度: pop为O(n),其他为O(1)
-
空间复杂度: O(n)
2.2 使用一个队列实现栈的操作(优化):
一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时再去弹出元素就是栈的顺序了。
class MyStack {
public:
queue<int> que;
/** Initialize your data structure here. */
MyStack() {
}
/** Push element x onto stack. */
void push(int x) {
que.push(x);
}
/** Removes the element on top of the stack and returns that element. */
int pop() {
int size = que.size();
size--;
while (size--) { // 将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部
que.push(que.front());
que.pop();
}
int result = que.front(); // 此时弹出的元素顺序就是栈的顺序了
que.pop();
return result;
}
/** Get the top element.
** Can not use back() direactly.
*/
int top(){
int size = que.size();
size--;
while (size--){
// 将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部
que.push(que.front());
que.pop();
}
int result = que.front(); // 此时获得的元素就是栈顶的元素了
que.push(que.front()); // 将获取完的元素也重新添加到队列尾部,保证数据结构没有变化
que.pop();
return result;
}
/** Returns whether the stack is empty. */
bool empty() {
return que.empty();
}
};
题目三:20. 有效的括号
括号匹配是使用栈解决的经典问题。
解题思路:
先想清楚一共有多少种情况{}()[]
-
第一种情况,字符串里左方向的括号多余了 ,所以不匹配。
-
第二种情况,括号没有多余,但是 括号的类型没有匹配上。
-
第三种情况,字符串里右方向的括号多余了,所以不匹配。
细节技巧:
-
可以使用剪枝操作,如果字符串是奇数一定是由不匹配的。
-
在匹配左括号的时候,右括号先入栈,就只需要比较当前元素和栈顶相不相等就可以了,比左括号先入栈代码实现要简单的多了!
**成功匹配的条件**:字符串遍历完之后,栈是空的,就说明全都匹配了。
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() || st.top() != s[i]) return false;//在不是左括号的情况下,还有元素的操作。
else st.pop(); // st.top() 与 s[i]相等,栈弹出元素
}
// 第一种情况:此时我们已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false,否则就return true
return st.empty();
}
};
- 时间复杂度: O(n)
- 空间复杂度: O(n)
题目四:1047. 删除字符串中的所有相邻重复项
解题思路:
- 匹配与消除:与匹配左右括号的问题类似,本题涉及到的也是匹配问题,但不同之处在于我们关注的是相邻元素的匹配,并在此基础上执行消除操作。
- 栈的应用:本题是使用栈来解决的经典问题。栈在这里充当了临时存储的角色,用于存放我们遍历过程中遇到的元素。
- 遍历与检查:在遍历字符串的过程中,我们利用栈来检查当前元素是否与栈顶元素相同。如果发现相邻元素相同,即执行消除操作。
- 结果的生成:遍历结束后,栈中剩余的元素将按照它们入栈的逆序排列。为了得到最终的正确顺序,我们需要将这些元素从栈中弹出,并进行一次反转操作。
- 最终结果:通过上述步骤,我们可以得到一个不含相邻重复元素的字符串,它既整洁又符合我们的要求。
代码如下:
class Solution {
public:
string removeDuplicates(string S) {
stack<char> st;
for (char s : S) {
if (st.empty() || s != st.top()) {
st.push(s);
} else {
st.pop(); // s 与 st.top()相等的情况
}
}
string result = "";
while (!st.empty()) { // 将栈中元素放到result字符串汇总
result += st.top();
st.pop();
}
reverse (result.begin(), result.end()); // 此时字符串需要反转一下
return result;
}
};
-
时间复杂度: O(n)
-
空间复杂度: O(n)
当然可以拿字符串直接作为栈,这样省去了栈还要转为字符串的操作。
class Solution {
public:
string removeDuplicates(string S) {
string result;
for(char s : S) {
if(result.empty() || result.back() != s) {
result.push_back(s);
}
else {
result.pop_back();
}
}
return result;
}
};
- 时间复杂度: O(n)
- 空间复杂度: O(1),返回值不计空间复杂度
-
递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数。
-
在企业项目开发中,尽量不要使用递归!在项目比较大的时候,由于参数多,全局变量等等,使用递归很容易判断不充分return的条件,非常容易无限递归(或者递归层级过深),造成栈溢出错误(这种问题还不好排查!
总结
- 栈(Stack)和队列(Queue)是两种基本的抽象数据类型,它们在操作方式上有着本质的不同,但有趣的是,它们可以相互实现。
- 栈在匹配问题中的经典使用(括号匹配)。
- 在处理字符串问题时,栈提供了一种高效且直观的方法来执行各种操作。