第五章 栈与队列part01
理论基础
了解一下 栈与队列的内部实现机制,文中是以C++为例讲解的。
文章讲解:代码随想录
先学下最基础的知识点:队列是先进先出,栈是先进后出。
栈的内部结构是这样的,可以看出来栈有点像一个类,用vector,deque和list实现存储数据,但是只给了外部服务接口就那么几个函数。
默认用deque实现栈,当然,list和map,也可,代码如下:
std::stack<int, std::vector<int> > third; // 使用vector为底层容器的栈
std::queue<int, std::list<int>> third; // 定义以list为底层容器的队列
容器适配器
容器适配器通常是基于已有的标准容器(如std::vector
、std::deque
等)构建的,适配器修改了它们的接口,使其表现得像另一种特定用途的容器。
C++标准库中有三种主要的容器适配器:
-
std::stack
:基于std::deque
、std::vector
或std::list
实现的后进先出(LIFO)数据结构。它只允许在容器的一端进行插入和删除操作,类似于堆栈。 -
std::queue
:基于std::deque
或其他双端容器实现的先进先出(FIFO)数据结构。它允许从一端插入元素,从另一端移除元素。 -
std::priority_queue
:基于std::vector
或其他随机访问容器实现的优先队列。这个适配器会根据元素的优先级进行排序,以确保优先级最高的元素总是位于队列的前端。
特点
-
限制接口:容器适配器通常限制了底层容器的一部分接口,以提供更专注于特定用途的功能。例如,
std::stack
并不允许随机访问,只允许push
、pop
、top
操作。 -
灵活的底层容器:容器适配器可以使用不同的底层容器来实现,只要这些容器满足某些需求。例如,
std::stack
可以使用std::deque
(默认)、std::vector
或者std::list
作为其底层容器。
232.用栈实现队列
使用栈实现队列的下列操作:
push(x) -- 将一个元素放入队列的尾部。
pop() -- 从队列首部移除元素。
peek() -- 返回队列首部的元素。
empty() -- 返回队列是否为空。
大家可以先看视频,了解一下模拟的过程,然后写代码会轻松很多。
题目链接/文章讲解/视频讲解:代码随想录
感觉没必要看视频,直接看下面的动图,思路就明显了。
只要一个进栈,一个出栈,就很好理解队列的操作。接下来就要学习如何实现,
push,还是很好实现的,直接导入stIn里面就可以了。
pop实现也很简单,把stIn里面的全导入到stout,弹出stOut里面最上面的,但是我最开始写的代码,又把stOut全导入到stIn。这种思路没有问题,但是很麻烦,卡尔给的方法很直接,stIn只是用来临时存储作用和导入的。stOut才是用来弹出的。记住一定要stOut是空的才好,只有这样,才能保证顺序。存储的新的一波stIn导入到stOut是正常顺序的。
class MyQueue {
public:
stack<int> stIn;
stack<int> stOut;
MyQueue() {
}
void push(int x) {
stIn.push(x);
}
int pop() {
if(stOut.empty()==1)
while(stIn.empty()==0)
{
stOut.push(stIn.top());
stIn.pop();
}
int result= stOut.top();
stOut.pop();
return result;
}
int peek() {
int temp= this->pop();
stOut.push(temp);
return temp;
}
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();
*/
写代码的时候发现:pop()
函数错误:stIn.pop()
实际上并不会返回值。你需要先获取栈顶元素的值,再调用 pop()
移除该元素。
在 C++ 的标准库中,std::stack<T>::pop()
函数的确不会返回被弹出的元素值。原因在于 C++ 设计者对栈(stack)的抽象模型及其使用方式进行了设计权衡,决定让 pop()
函数仅执行移除操作,而不返回值。
具体原因如下:
-
职责分离:
pop()
的设计原则是“职责单一”。它的唯一职责是将栈顶的元素移除,而不是返回这个元素。如果需要获取栈顶元素,应该调用top()
,而不是pop()
。C++ 标准库将这两个操作分开,以减少函数的职责,让代码更清晰,职责更明确。 -
避免误用:如果
pop()
同时移除并返回栈顶元素,程序员可能会忽略检查栈是否为空,直接使用返回值。这会导致未定义行为(Undefined Behavior),因为当栈为空时,pop()
将尝试移除不存在的元素。将这两个操作分开,可以更好地提醒程序员先调用top()
获取值,再调用pop()
进行移除。
225. 用队列实现栈
可能大家惯性思维,以为还要两个队列来模拟栈,其实只用一个队列就可以模拟栈了。
建议大家掌握一个队列的方法,更简单一些,可以先看视频讲解
使用两个的方法思路就是q2用来当作一个temp变量,用来临时存储。代码没有什么好讲的,整体上和用栈模拟队列有点不一样,几乎全程靠q1来C位,q2只是用来临时存储的工具。
题目链接/文章讲解/视频讲解:代码随想录
class MyStack {
public:
queue<int> q1;
queue<int> q2;
MyStack() {
}
void push(int x) {
q1.push(x);
}
int pop() {
int size=q1.size();
while(--size)
{
q2.push(q1.front());
q1.pop();
}
int result=q1.front();
q1.pop();
q1=q2;
while(q2.empty()==0)
q2.pop();
return result;
}
int top() {
return q1.back();
}
bool empty() {
return q1.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();
*/
1 | 2 | 3 | 4 | 5 | 6 | 7 |
start | 8 | |||||
17 | 9 | |||||
16 | 15 | 14 | 13 | 12 | 11 | 10 |
1 | <- |
1 | 2 | <- |
2 | 1 | 3 |
1 | 3 | 2 |
3 | 2 | 1 |
感觉使用单栈来模拟思路更清晰简单。每次插入元素后,将队列中的元素重新排列,使得插入的元素成为队列的队首元素,这样在 pop()
或 top()
操作时,直接访问队首元素即可。
根据上面表格,还是很好理解,先push(1);再push(2):push(2),pop(1),push(1);再push(3):push(3);pop(2),push(2),pop(1),push(1)。
思路很简单,但是我在想可不可以push正常push,pop再排序呢。
1 | <- |
1 | 2 | <- |
1 | 2 | 3 | <- |
2 | 3 | 1 |
3 | 1 | 2 |
1 | 2 |
好像也可以,但是最大的问题,还是查找和pop,时间复杂度高太多了,各有利弊,如果需要大量查,感觉还是,push排序,pop正常,如果需要大量添加,正常push,pop再排序。
class MyStack {
public:
queue<int> q1;
MyStack() {
}
void push(int x) {
q1.push(x);
}
int pop() {
int size=q1.size();
while(--size)
{
q1.push(q1.front());
q1.pop();}
int result=q1.front();
q1.pop();
return result;
}
int top() {
int result=this->pop();
q1.push(result);
return result;
}
bool empty() {
return q1.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. 有效的括号
讲完了栈实现队列,队列实现栈,接下来就是栈的经典应用了。
大家先自己思考一下 有哪些不匹配的场景,在看视频 我讲的都有哪些场景,落实到代码其实就容易很多了。
题目链接/文章讲解/视频讲解:代码随想录
感觉和栈的思路基本上一致,把字符串s压入栈中,看下是不是右括号,如果是右括号一定有一个左括号可以配对。可以仔细观察就能发现,无论“(){}[]{}[]”这种并列的,还是“{({[()]})}”这种内嵌的,一定是可以相邻,可以对应删除掉。如果有一个右括号左括号没有匹配的,说明一定是错的,可以直接返回了。
特别注意,一个for循环后,一定要判断下栈是否为空,为空的话才是匹配完,不为空说明有漏下的左括号。
class Solution {
public:
bool isValid(string s) {
stack<char> st;
for(int i=0;i<s.size();i++)
{
if(s[i]==')'||s[i]==']'||s[i]=='}')
{
if(st.empty()==1)
return false;
if((st.top()=='('&&s[i]==')')||(st.top()=='['&&s[i]==']')||(st.top()=='{'&&s[i]=='}'))
st.pop();
else
return false;
}
else
st.push(s[i]);
}
if(st.empty()==1)
return true;
else
return false;
}
};
1047. 删除字符串中的所有相邻重复项
栈的经典应用。
代码很简单,和之前的括号匹配很类似,所以挺简单的。
class Solution {
public:
string removeDuplicates(string s) {
stack<char> st;
for(int i=0;i<s.size();i++)
{
if(st.empty()||st.top()!=s[i])
st.push(s[i]);
else
st.pop();
}
string result;
while (!st.empty()) {
result += st.top();
st.pop();
}
reverse(result.begin(), result.end());
return result;
}
};
要知道栈为什么适合做这种类似于爱消除的操作,因为栈帮助我们记录了 遍历数组当前元素时候,前一个元素是什么。
生成字符串会与栈中字符顺序相反。如果需要保持原来的顺序,可以在生成字符串后进行反转,或者在构建字符串时从栈底开始。
题目链接/文章讲解/视频讲解:代码随想录
写代码的时候就想到,还要把栈导入,再导出,好麻烦,为什么不可以直接新建个string,在string上改呢。果然自己能想到的,别人肯定能想到。确实敲代码敲多了,确实能感觉到哪些地方重复浪费。原来字符串也可以push_back(),empty(),以及遍历,学到了。对,string的本质是个字符动态数组。
class Solution {
public:
string removeDuplicates(string s) {
string result;
for(auto it : s) {
if(result.empty() || result.back() != it) {
result.push_back(it);
}
else {
result.pop_back();
}
}
return result;
}
};