栈与队列基础理论
栈和队列是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是开源软件,源码可读性甚高。
栈(stack)基础理论
栈提供push 和 pop 等等接口,所有元素必须符合先进后出规则,所以栈不提供走访功能,也不提供迭代器(iterator)。 不像是set 或者map 提供迭代器iterator来遍历所有元素。
std::stack<int> myStack={1, 2, 3, 4, 5};
,创建栈,栈可以通过已有的容器进行初始化,以vector为容器进行初始化,此时栈中1在栈底,5在栈顶。
std::stack<int> stack(vec); // 使用 vector 初始化 stack
栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能)。
所以STL中栈往往不被归类为容器,而被归类为container adapter(容器适配器)。STL 中栈是用什么容器实现的?
栈的内部结构,栈的底层实现可以是vector,deque(双端队列),list 都是可以的,主要就是数组和链表的底层实现。
常用的SGI STL,如果没有指定底层实现的话,默认是以deque为缺省情况下栈的底层结构。deque是一个双向队列,只要封住一段,只开通另一端就可以实现栈的逻辑了。
栈的基础函数
函数名称 | 函数功能 | 返回值 |
---|---|---|
empty | 判空 | bool |
size | 获取栈有多少元素 | 整型 |
top | 取栈顶元素 | 返回栈顶元素的引用 |
push | 入栈 | void |
emplace | 入栈(和push有所不同) | void |
pop | 移除栈顶元素 | 无返回值 |
swap | 交换两个栈的元素 | void |
push和emplace的结果是一样的,都是在栈顶插入一个元素容器获取附加到它的另一个元素。不同之处在于元素的来源。
push 获取一个现有元素,并将其拷贝附加到容器中。简单,直接。 push 总是只接受一个参数,即要复制到容器的元素。
emplace 在容器中创建该类的另一个实例,该实例已经附加到容器中。 emplace 的参数作为参数转发给容器类的构造函数。
pop函数用来返回对栈顶元素的引用,可以访问或者修改栈顶元素。
队列(queue)基础理论
队列是先进先出的数据结构,同样不允许有遍历行为,不提供迭代器, SGI STL中队列一样是以deque为缺省情况下的底部结构。
STL 队列也不被归类为容器,而被归类为container adapter( 容器适配器)。
std::queue<int> myQueue={1, 2, 3, 4, 5};
,创建队列,1为队头,5为队尾
std::queue<int> queue(deq); // 使用 deque 初始化 queue
函数名称 | 函数功能 | 返回值 |
---|---|---|
empty | 判空 | bool |
size | 获取队列有多少元素 | 整型 |
front | 返回对队头元素的引用 | T& 或 const T& |
back | 返回对队尾元素的引用 | T& 或 const T& |
push | 向队列末尾添加一个元素 | void |
emplace | 入队列(和push有所不同) | void |
pop | 移除队列的第一个元素 | void |
swap | 交换两个队列的元素 | void |
232.用栈实现队列
使用栈实现队列的下列操作:
push(x) – 将一个元素放入队列的尾部。
pop() – 从队列首部移除元素。
peek() – 返回队列首部的元素。
empty() – 返回队列是否为空。
题解思路
利用两个栈来实现队列的行为
- 定义两个栈,一个进栈(stack in ),一个出栈(stack out)
- 实现pop函数,先将数据从stack in中全部导出到stack out,
- 移除队首元素
代码
class MyQueue {
public:
stack<int>stackin;
stack<int>stackout;
MyQueue() {
}
void push(int x) {
stackin.push(x);
}
int pop() {
if(stackout.empty())
{
while(!stackin.empty())
{
stackout.push(stackin.top()); //将stackin的栈头加入到stackout
stackin.pop(); //移除stackin的栈顶元素
}
}
int result = stackout.top();
stackout.pop();
return result;
}
int peek() {
if(stackout.empty())
{
while(!stackin.empty())
{
stackout.push(stackin.top());
stackin.pop();
}
}
return stackout.top();
}
bool empty() {
return stackin.empty() && stackout.empty();
}
};
225. 用队列实现栈
题干
使用队列实现栈的下列操作:
push(x) – 元素 x 入栈
pop() – 移除栈顶元素
top() – 获取栈顶元素
empty() – 返回栈是否为空
题解思路
两个队列来模拟栈
队列模拟栈,其实一个队列就够了,队列是先进先出的规则,把一个队列中的数据导入另一个队列中,数据的顺序并没有变,并没有变成先进后出的顺序。第一种方法用两个队列来模拟栈,只不过没有输入和输出的关系,而是另一个队列完全用来备份的,
用两个队列que1和que2实现队列的功能,que2其实完全就是一个备份的作用,把que1最后面的元素以外的元素都备份到que2,然后弹出最后面的元素,再把其他元素从que2导回que1。
一个队列来模拟栈
栈的pop函数模拟:
思路其实很简单,将队列中后加入的元素弹出再加入到队列中,就可以将最后一个加入队列中的元素弹出,加入队列中有size个元素,那就把size-1个元素弹出再重新加入到队列中,最后就可以将最后一个加入到队列中的元素弹出了。
class MyStack {
public:
queue<int>myque;
MyStack() {
}
void push(int x) {
myque.push(x);
}
int pop() {
int size = myque.size()-1;
while(size--)
{
myque.push(myque.front());
myque.pop();
}
int result = myque.front();
myque.pop();
return result;
}
int top() {
return myque.back();
}
bool empty() {
return myque.empty();
}
};
20. 有效的括号
题干
给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
注意空字符串可被认为是有效字符串。
题解思路
括号匹配是使用栈解决的经典问题。
题意其实就像我们在写代码的过程中,要求括号的顺序是一样的,有左括号,相应的位置必须要有右括号。如果还记得编译原理的话,编译器在词法分析的过程中处理括号、花括号等这个符号的逻辑,也是使用了栈这种数据结构。
由于栈结构的特殊性,非常适合做对称匹配类的题目。
字符串里的括号不匹配有几种情况
-
第一种情况,字符串里左方向的括号多余了 ,所以不匹配。
-
第二种情况,括号没有多余,但是括号的类型没有匹配上。
-
第三种情况,字符串里右方向的括号多余了,所以不匹配。
注意点
- 字符串的匹配顺序是从左到右,指针先指到的是左符号,当指针指向一个左符号的时候我们在栈里加入一个右符号,当指针指向到右符号的时候我们直接从栈里弹出元素进行匹配就可以了,
- 当遍历完字符串的时候发现栈不为空,那就说明出现多余的左符号了
- 下一种情况就是当匹配时指针指向的符号和栈中弹出的符号不匹配,这就是第二种情况括号的类型没有匹配上
- 最后一种情况就是当指针遍历到右符号进行匹配的时候,发现栈中已经没有符号了,这就是右符号多了
需要有一个剪枝操作,当这个字符串长度为奇数的时候,那肯定不可能匹配上,
代码实现
- 首先进行剪枝判断
- 定义栈
- 进入for循环,遍历字符串
- if遍历到左符号,那么就向栈中添加右符号
- else if栈为空,或者栈顶元素与当前遍历的元素不匹配,返回false
- 最后一个else,就是栈不为空,且栈顶元素与当前遍历元素匹配的情况,就将栈顶元素弹出
- 退出循环之后,再检测一下栈是否为空,如果不为空,那就对应第一种情况,返回false
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();
}
};
1047. 删除字符串中的所有相邻重复项
题干
给出由小写字母组成的字符串 S,重复项删除操作会选择两个相邻且相同的字母,并删除它们。
在 S 上反复执行重复项删除操作,直到无法继续删除。
在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
输入:“abbaca”
输出:“ca”
解释:例如,在 “abbaca” 中,我们可以删除 “bb” 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 “aaca”,其中又只有 “aa” 可以执行重复项删除操作,所以最后的字符串为 “ca”。
题解思路
本题也是用栈来解决的经典题目,
- 新建一个栈
- 遍历字符串,进入循环,当遍历到的字符与栈顶元素不相同时或者栈为空时,将此字符导入栈中,否则,删除栈顶元素
- 循环结束,新建一个字符串,通过循环将栈中的字符弹出并写入到字符串中
- 反转字符串,并返回
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();
}
string result = "";
while(!st.empty())
{
result += st.top();
st.pop();
}
reverse(result.begin(),result.end());
return result;
}
};
第二种方法可以拿字符串直接作为栈,这样省去了栈还要转为字符串的操作。
result 是一个标准的 C++ 字符串对象 (std::string),但它被用来模拟栈的行为:
- result.back():模拟栈的 top() 操作,用来访问栈顶元素。
- result.push_back(s):模拟栈的 push() 操作,将元素 s 压入栈顶。
- result.pop_back():模拟栈的 pop() 操作,移除栈顶元素。
- result.front():返回字符串的第一个字符的引用(“队列头”元素)。
- result.emplace_back():在字符串末尾直接构造字符,效果类似于 push_back(),但更高效,因为它避免了额外的拷贝或移动。
- result.clear():移除字符串中的所有字符,将字符串变为空。
这些函数都是C++标准库的函数,
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;
}
};