参考资料
代码随想录
a.C++中容器有哪些特点
b.栈有哪些特点
c.总结栈为什么不是容器
STL库是C++中借助模板把常用的数据结构和算法分离的库
基本组件除了容器还有迭代器算法和函数对象,算法运行时通过迭代器对容器进行访问;
简单的理解容器,它就是一些模板类的集合,但和普通模板类不同的是,容器中封装的是组织数据的方法(也就是数据结构)。
栈本身不归类为容器,但是栈是以底层容器来完成所有功能的。栈的底层实现可以是vector、deque、list,栈归类为容器适配器。
栈提供push和pop等接口,所有元素必须符合先进后出的规则,不提供走访功能,也不提供迭代器。不像set或map提供迭代器iterator来遍历所有元素。
push(x)——将一个元素放入栈的顶部
pop()——从栈顶弹出元素
peek()——返回栈最前端的元素
empty()——返回栈是否为空
push(x)——将一个元素放入队列的尾部
pop()——从队列尾部移除元素
peek()——返回队列首部的元素
empty()——返回队列是否为空
用栈实现队列
(1)基本原理
栈是后进先出,队列是先进先出;
相关操作:
操作名称 | 操作解释 |
---|---|
push(x) | 将一个元素放入队列的尾部 |
pop() | 从队列首部移除元素 |
peek() | 返回队列首部的元素 |
empty() | 返回队列是否为空 |
(2)代码实现 | |
步骤: | |
a.定义两个栈来表示队列 | |
b.将元素压入栈等同于压入队列 | |
c.元素出栈,需要两个栈配合,stOut为空时,再从stIn导入数据 | |
d.返回首部的元素,读取之后还需要放回去 | |
e.两个栈都为空时,才能判断队列为空 |
class MyQueue {
public:
stack<int> stIn; //定两个栈来实现队列
stack<int> stOut;
MyQueue() {
}
void push(int x) {
stIn.push(x);//直接放入栈,等同于直接放入队列的尾部
}
int pop() {
//先把stIn中的数据导入stOut,再从stOut中弹出数据,相当于做了一下顺序的调换
if(stOut.empty()){
while(!stIn.empty()){
stOut.push(stIn.top());//top是读取顶端元素,pop是弹出元素
stIn.pop();
}
}
int result = stOut.top();
stOut.pop();
return result;
}
int peek() {
int res = this->pop();//使用pop读取开头元素,定义了之前已经有的函数
stOut.push(res);//再放回去
return res;
}
bool empty() {
return stIn.empty()&&stOut.empty();
}
};
用队列实现栈
(1)基本原理
用两个队列来模拟栈,队列先进先出,栈先进后出,把队列后出的数据放入另一个队列先进行缓存。
方法一:两个队列,重点在出栈时会将最后一个元素前面的所有值存放到另一个队列
方法二:只用一个队列也能达到效果,将队列弹出的元素不断添加到队列的末尾
(2)代码实现
步骤
a.定义两个队列
b.将元素压入队列等同于压入栈
c.元素出栈,需要两个队列配合,依次将que1除了最后一个元素以外放入que2;
首先记录size,然后用size–作为循环条件;
返回元素que1front元素
d.栈的top就是队列的末尾
e.两个栈都为空时,才能判断队列为空
class MyStack {
public:
queue<int> que1;
queue<int> que2;
MyStack() {
}
void push(int x) {
que1.push(x);
}
int pop() {
//为了记录最后一个元素需要知道队列的长度
int size = que1.size();
while(--size){//需要留下最后一个元素,所以用--size
//将最后一个元素之前的元素依次放入队列2
que2.push(que1.front());
que1.pop();
}
int result = que1.front();
que1.pop();
//为了方便处理,所以一直用que1作为需要处理的对象
que1 = que2;
while(!que2.empty()){
que2.pop();
}
return result;
}
int top() {
return que1.back();//que的基本操作和stack的有所不同,主要区分
}
bool empty() {
return que1.empty();
}
};
有效的括号
(1)基本原理
栈数据结构的应用:括号的匹配,匹配类型是数据非常适合用栈来解决
有左括号,相应的位置就必须有右括号;
不匹配情况 | 解决措施 |
---|---|
缺少右括号 | 遍历字符串匹配,发现栈不为空。 |
左右括号类型不匹配 | 遍历字符串,栈中没有相匹配的字符 |
缺少左括号 | 遍历没有结束,栈中已经空了 |
(2)代码实现 | |
步骤: | |
a.定义一个栈存储一侧的括号类型 | |
b.遇到左括号,存储对应的右括号 | |
(为了方便配对弹出) | |
c.如果遍历完字符串,栈不为空,则return false; | |
栈为空则return ture |
class Solution {
public:
bool isValid(string s) {
stack<int> 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();//左括号右括号相等就弹出元素
}
return st.empty();
}
};
删除字符串中所有与相邻重复项
(1)基本原理
应用:消消乐
函数的递归调用:每一次递归调用都会把函数的局部变量、参数值返回地址等压入调用栈中。
把字符串顺序放到一个栈中,相同的话栈就弹出,最后栈中剩下的元素都是相邻不相同的元素。
(2)代码实现
a.定义一个栈用来存储字符串中的元素
b.新元素与栈顶元素相同则弹出该元素
c.遍历到数组末尾,一次读取栈中的元素,并逆序输出
class Solution {
public:
string removeDuplicates(string s) {
stack<char> st;
int size = s.size();
string result = "";//注意定义的时候不要有空格,之前的错误就是因为这里有空格
for (char a : s) { //这里用数组定义时的for(int i = 1;i
if (st.empty() || a != st.top()) {
st.push(a);
} else {
st.pop(); // s 与 st.top()相等的情况
}
}
while(!st.empty()){//栈的基本操作中并没有读取他长度的函数
result+=st.top();//pop是弹出操作,返回的是少一个元素的st.top才是读取最顶层元素的方法
st.pop();
}
reverse(result.begin(),result.end());//reverse()函数的使用,字符串也是有迭代器的
return result;
}
};
逆波兰表达式求值
(1)基本原理
逆波兰表达式:是一种后缀表达式,所谓后缀就是指算符写在后面。
优点:去掉括号后表达式无歧义
知识点之间的联系:栈与递归相互之间可以转换,逆波兰表达式相当于二叉树的后序遍历
(2)代码实现
适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中。
步骤:
a.定义一个栈存储数组中的操作符号和元素
b.先对是否是运算符进行判断
(因为这里的数据不是单个字符,不能用单个数值进行判读)
c.是元素符则弹出两个元素并进行计算
d.是数据则压入栈中
e.将结果弹出
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> stk;
for(int i = 0;i<tokens.size();i++){
if(tokens[i]=="+"||tokens[i]=="-"||tokens[i]=="*"||tokens[i]=="/"){
int tmp1 = stk.top();
stk.pop();
int tmp2 = stk.top();
stk.pop();
if(tokens[i]=="+") stk.push(tmp1+tmp2);
if(tokens[i]=="-") stk.push(tmp2-tmp1);
if(tokens[i]=="*") stk.push(tmp1*tmp2);
if(tokens[i]=="/") stk.push(tmp2/tmp1);
}else{
//因为这一步已经进行了字母到数字的转换,后面的计算可用
stk.push(stoi(tokens[i]));
}
}
return stk.top();
}
};
滑动窗口最大值
(1)基本原理
单调队列的经典问题
方法一:遍历一遍,每次从串口中找到最大的数值;
复杂度过高。
思考的关键点在:队列不需要维护窗口中所有的元素,只需要维护窗口中有可能成为最大值的元素。
(2)代码实现
步骤:
a.设计一个单调队列,比较当前数值是否等于队列出口元素的数值
设计队列主要有3个功能,
pop()移除窗口左端的元素,
push()加入窗口右端的元素,
front()弹出队列中的最大值。
b.把第一个窗口里的数值放入队列
c.遍历数组后面的元素,每移动一步执行队列中的各种操作
class Solution {
private:
//类的定义和使用,类的private函数和public关键字;类的末尾处要有分号。
class MyQueue{
public:
deque<int> que;
//设计队列的pop功能函数,只有当需要弹出的数值是队列头中时才需要弹出,因为其他地方的数值并没有放入队列
void pop(int value){
//双端队列有pop_front()和pop_back()
if(!que.empty()&&value == que.front()){
que.pop_front();
}
}
//如果push的元素大于入口元素的数值,就将队列后端的数值弹出
void push(int value){
while(!que.empty()&&value>que.back()){
que.pop_back();
}
que.push_back(value);
}
int front(){
return que.front();
}
};
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
MyQueue que;
vector<int> result;
for(int i = 0;i<k;i++){
que.push(nums[i]);
}
//记录前k个元素中的最大值
result.push_back(que.front());
//从i=k开始,比从端口的前端开始方便记录,不过也差不多,在pop中有体现
//后面的每次循环执行一次Myque函数中的操作
for(int i=k;i<nums.size();i++){
que.pop(nums[i-k]);
que.push(nums[i]);
result.push_back(que.front());
}
return result;
}
};
前K个高频元素
要统计元素出现的频率——map;
解决的问题:
对元素出现的频率进行统计;
对频率次数进行排序;
找出前K个高频元素。
需要补充的知识点:堆,堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值
统计前k个元素,小顶堆每次将最小的元素弹出,最后小顶堆的积累才是前k个最大元素。
class Solution {
public:
//自定义一个比较函数,true不执行交换操作
class mycomparsion{
public:
bool operator()(const pair<int,int>& lhs,const pair<int,int>& rhs){
return lhs.second> rhs.second;
}
};
vector<int> topKFrequent(vector<int>& nums, int k) {
//用map统计元素出现的频率,map
unordered_map<int,int> map;
for(int i = 0;i<nums.size();i++){
map[nums[i]]++;
}
//对频率排序
priority_queue<pair<int,int>,vector<pair<int,int>>,mycomparsion> pri_que;
//用固定大小为k的小顶堆,扫描所有频率对应的数值
for(unordered_map<int,int>::iterator it = map.begin();it!=map.end();it++){
pri_que.push(*it);
//如果堆的大小大于k,则队列弹出,保证堆的大小一直为k
if(pri_que.size()>k){
pri_que.pop();
}
}
//找出前k个高频元素,小顶堆先弹出最小的,用倒序来输出到数组
vector<int> result(k);
for(int i = k-1;i>=0;i--){
result[i] = pri_que.top().first;
pri_que.pop();
}
return result;
}
};