算法学习——栈与队列

栈与队列

  • 栈与队列理论基础
  • 用栈实现队列
    • 思路
    • 代码
  • 用队列实现栈
    • 思路
    • 代码
  • 删除字符串中的所有相邻重复项
    • 思路
    • 代码
  • 有效的括号
    • 思路
    • 代码
  • 逆波兰表达式求值
    • 思路
    • 代码
  • 滑动窗口最大值
    • 思路
    • 代码
    • 未完待续
  • 前 K 个高频元素
    • 思路
    • 代码
    • 拓展
  • 总结
    • 栈在系统中的应用
    • 括号匹配问题
    • 字符串去重问题
    • 逆波兰表达式问题
    • 滑动窗口最大值问题
    • 求前 K 个高频元素

栈与队列理论基础

队列是先进先出,栈是先进后出。
在这里插入图片描述
这里再列出四个关于栈的问题,大家可以思考一下。

  1. C++中stack 是容器么?
  2. 我们使用的stack是属于哪个版本的STL?
  3. 我们使用的STL中stack是如何实现的?
  4. stack 提供迭代器来遍历stack空间么?

首先要知道 栈和队列是STL(C++标准库)里面的两个数据结构。
C++标准库是有多个版本的,要知道我们使用的STL是哪个版本,才能知道对应的栈和队列的实现原理。

那么来介绍一下,三个最为普遍的STL版本:

  1. HP STL 其他版本的C++ STL,一般是以HP STL为蓝本实现出来的,HP STL是C++ STL的第一个实现版本,而且开放源代码。
  2. P.J.Plauger STL 由P.J.Plauger参照HP STL实现出来的,被Visual C++编译器所采用,不是开源的。
  3. SGI STL 由Silicon Graphics Computer Systems公司参照HP STL实现,被Linux的C++编译器GCC所采用,SGI STL是开源软件,源码可读性甚高。

接下来介绍的栈和队列也是SGI STL里面的数据结构, 知道了使用版本,才知道对应的底层实现。
先说一说栈,栈先进后出,如图所示:
在这里插入图片描述
栈提供push 和 pop 等等接口,所有元素必须符合先进后出规则,所以栈不提供走访功能,也不提供迭代器(iterator)。 不像是set 或者map 提供迭代器iterator来遍历所有元素。

栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能)。
所以STL中栈往往不被归类为容器,而被归类为container adapter(容器适配器)。

那么问题来了,STL 中栈是用什么容器实现的?

从下图中可以看出,栈的内部结构,栈的底层实现可以是vector,deque,list 都是可以的, 主要就是数组和链表的底层实现。
在这里插入图片描述
我们常用的SGI STL,如果没有指定底层实现的话,默认是以deque为缺省情况下栈的底层结构。
deque是一个双向队列,只要封住一端,只开通另一端就可以实现栈的逻辑了。
SGI STL中 队列底层实现缺省情况下一样使用deque实现的。

我们也可以指定vector为栈的底层实现,初始化语句如下:

std::stack<int, std::vector<int> > third;  // 使用vector为底层容器的栈

刚刚讲过栈的特性,对应的队列的情况是一样的。

队列中先进先出的数据结构,同样不允许有遍历行为,不提供迭代器, SGI STL中队列一样是以deque为缺省情况下的底部结构。

也可以指定list 为起底层实现,初始化queue的语句如下:

std::queue<int, std::list<int>> third; // 定义以list为底层容器的队列

所以STL 队列也不被归类为容器,而被归类为container adapter( 容器适配器)。

用栈实现队列

力扣题目链接

思路

栈是先进后出的数据结构,队列则是先进先出。
那么这道题,我们需要两个栈才能模拟一个队列。
为什么这么说?
栈插入元素后,元素是逆序排列的,但是我把元素取出来后再插入一个栈,不就是负负得正了吗。
我先准备一个输入栈In,每次做push操作的时候都向这个栈里插入元素。
当需要pop元素的时候,将输入栈中的元素全部取出然后插入到输出栈Out中。此时,输出栈Out的栈顶元素就是我们需要的第一个元素。

代码

void push(int x) {stIn.push(x);}int pop() {if(!stOut.empty()){int x = stOut.top();stOut.pop();return x;}else{while(!stIn.empty()){int x = stIn.top();stIn.pop();stOut.push(x);}int x = stOut.top();stOut.pop();return x;}}int peek() {int a = pop();stOut.push(a);return a;}bool empty() {if(stIn.empty()&&stOut.empty()){return true;}else{return false;}}

用队列实现栈

力扣题目链接

思路

队列是先进先出的数据结构,栈是先进后出。如果像上一道题一样做法,那么无论如何也不可能实现先进后出。
但是,模拟的时候我发现,如果我每次弹出队列中除了最后一个元素的所有元素,那么此时最后一个元素就是模拟栈弹出的第一个元素。
我只需要将队列弹出元素存入另一个队列中,当弹出最后一个元素的时候再将他们放入队列中即可。
que2其实完全就是一个备份的作用,把que1最后面的元素以外的元素都备份到que2,然后弹出最后面的元素,再把其他元素从que2导回que1。

代码

 void push(int x) {d1.push_back(x);}int pop() {int n = d1.size();n--;while(n--){int x = d1.front();d1.pop_front();d2.push_back(x);}int result = d1.front();d1.pop_front();while(!d2.empty()){int x = d2.front();d2.pop_front();d1.push_back(x);}return result;}int top() {int x = pop();d1.push_back(x);return x;}bool empty() {if(d1.empty()){return true;}else{return false;}}

删除字符串中的所有相邻重复项

力扣题目链接

给出由小写字母组成的字符串 S,重复项删除操作会选择两个相邻且相同的字母,并删除它们。
在 S 上反复执行重复项删除操作,直到无法继续删除。
在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。

示例:
输入:“abbaca”
输出:“ca”
解释:例如,在 “abbaca” 中,我们可以删除 “bb” 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 “aaca”,其中又只有 “aa” 可以执行重复项删除操作,所以最后的字符串为 “ca”。

思路

我们在删除相邻重复项的时候,其实就是要知道当前遍历的这个元素,我们在前一位是不是遍历过一样数值的元素,那么如何记录前面遍历过的元素呢?

所以就是用栈来存放,那么栈的目的,就是存放遍历过的元素,当遍历当前的这个元素的时候,去栈里看一下我们是不是遍历过相同数值的相邻元素。

代码

class Solution {
public:string removeDuplicates(string s) {stack<char> st;for(int i = 0 ; i < s.size(); i++){char ch;// 只有当栈非空时才检查栈顶元素if(!st.empty() && s[i] == st.top()){st.pop();}else{st.push(s[i]);}}string result = "";while(!st.empty()){char c = st.top();result += c;st.pop();}reverse(result.begin(),result.end());return result;}
};

有效的括号

力扣题目链接

给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串,判断字符串是否有效。

有效字符串需满足:

左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
注意空字符串可被认为是有效字符串。

思路

该题是很经典的栈问题,用来括号匹配。
我的代码可能看起来比较乱。
我的思路是,首先判断栈是否为空,如果为空,我们需要将遍历到的字符放入栈中。此时,有四种情况,前三种是三个不同的左括号,最后一个情况是任意一个右括号。如果出现了栈是空的,并且当前遍历的括号是右括号,已经说明不匹配了。
接下来就是栈不为空的情况。那么有两种,一是遍历的括号和栈中的括号一样,那么就抵消了。
二是,如果是左括号,那么还是把对应的右括号压入栈中。如果是右括号,那就是不匹配的情况。
最后检查一下栈是否为空即可判断出括号是否完全匹配。

代码

class Solution {
public:bool isValid(string s) {stack<char> st;for(int i = 0 ; i < s.size(); ++i){if(st.empty()){if(s[i]=='('){st.push(')');}else if(s[i]=='[') st.push(']');else if(s[i]=='{') st.push('}');else{return false;}}else{if(s[i]== st.top()){st.pop();}else{if(s[i]=='(') st.push(')');else if(s[i]=='[') st.push(']');else if(s[i]=='{') st.push('}');else{return false;}}}}if(st.empty()){return true;}else{return false;}}
};

逆波兰表达式求值

力扣题目链接

根据 逆波兰表示法,求表达式的值。
有效的运算符包括 + , - , * , / 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。

说明:
整数除法只保留整数部分。 给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。

思路

这道题的做法没什么好说的,很经典的栈问题。如果遍历到的是数字,就压入栈中,如果是运算符,那么从栈中取出两个元素进行相应的运算,之后把结果压入栈中。

唯一需要注意的地方是:在逆波兰表达式中,先弹出的是第二个操作数,后弹出的是第一个操作数。

代码

class Solution {
public:int evalRPN(vector<string>& tokens) {stack<int> st;for(int i = 0 ; i < tokens.size();++i){if(tokens[i]== "+"){int x = st.top();st.pop();int y = st.top();st.pop();int sum = x+y;st.push(sum);}else if(tokens[i]== "-"){int x = st.top();st.pop();int y = st.top();st.pop();int sum = y-x;st.push(sum);}else if(tokens[i]== "*"){int x = st.top();st.pop();int y = st.top();st.pop();int sum = x * y;st.push(sum);}else if(tokens[i]== "/"){int x = st.top();st.pop();int y = st.top();st.pop();int sum = y / x;st.push(sum);}else{int a = stoi(tokens[i]);st.push(a);}}int result = st.top();st.pop();return result;}
};

滑动窗口最大值

力扣题目链接

给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回滑动窗口中的最大值。

思路

代码

未完待续

前 K 个高频元素

力扣题目链接

给定一个非空的整数数组,返回其中出现频率前 k 高的元素。

示例 1:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]

思路

这道题目主要涉及到如下三块内容:
1.要统计元素出现频率
2.对频率排序
3.找出前K个高频元素

那么针对 对频率进行排序 我们可以使用一种容器适配器 叫做优先级队列
优先级队列其实就是一个披着队列外衣的 堆,因为优先级队列的接口是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来像一个队列。
而且优先级队列内部元素是自动依照元素的权值排列。那么它是如何有序排列的呢?

缺省情况下priority_queue利用max-heap(大顶堆)完成对元素的排序,这个大顶堆是以vector为表现形式的complete binary tree(完全二叉树)。

什么是堆呢?

堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。

所以大家经常说的大顶堆(堆头是最大元素),小顶堆(堆头是最小元素),如果懒得自己实现的话,就直接用priority_queue(优先级队列)就可以了,底层实现都是一样的,从小到大排就是小顶堆,从大到小排就是大顶堆。

本题我们就要使用优先级队列来对部分频率进行排序。

此时要思考一下,是使用小顶堆呢,还是大顶堆?

有的同学一想,题目要求前 K 个高频元素,那么果断用大顶堆啊。

那么问题来了,定义一个大小为k的大顶堆,在每次移动更新大顶堆的时候,每次弹出都把最大的元素弹出去了,那么怎么保留下来前K个高频元素呢。

而且使用大顶堆就要把所有元素都进行排序,那能不能只排序k个元素呢?

所以我们要用小顶堆,因为要统计最大前k个元素,只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素。

代码

class Solution {
public://小顶堆排序规则class mycompare{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) {//统计元素出现频率unordered_map<int ,int > map; for(int i : nums){map[i]++;}//对频率进行排序priority_queue<pair<int,int>,vector<pair<int,int>>,mycompare> pri_que;for(auto it = map.begin();it!=map.end();it++){pri_que.push(*it);if(pri_que.size() > k){pri_que.pop();}}//找出前k个频率大的元素vector<int>result(k);for(int i = 0 ; i < k ;++i){result[i] = pri_que.top().first;pri_que.pop();}return result;}
};

拓展

大家对这个比较运算在建堆时是如何应用的,为什么左大于右就会建立小顶堆,反而建立大顶堆比较困惑。

确实 例如我们在写快排的cmp函数的时候,return left>right 就是从大到小,return left<right 就是从小到大。

优先级队列的定义正好反过来了,可能和优先级队列的源码实现有关(我没有仔细研究),我估计是底层实现上优先队列队首指向后面,队尾指向最前面的缘故!

关于优先级队列的定义

priority_queue<pair<int,int>, vector<pair<int,int>>, mycompare> pri_que;

pair<int,int>:这里的 pair 用于存储两个信息:map中的元素(int 类型)及其出现频率(也是 int 类型)。

vector<pair<int,int>>:这是 priority_queue 的底层容器。它指定了 priority_queue 内部应该使用 vector 来存储元素,这些元素是 pair<int, int> 类型的。

mycompare:这是一个比较类,用于定义堆中元素的排序规则。在小顶堆中,顶部元素是最小的元素。

总结

栈在系统中的应用

如果还记得编译原理的话,编译器在词法分析的过程中处理括号、花括号等这个符号的逻辑,就是使用了栈这种数据结构。

再举个例子,linux系统中,cd这个进入目录的命令我们应该再熟悉不过了。

cd a/b/c/…/…/
这个命令最后进入a目录,系统是如何知道进入了a目录呢 ,这就是栈的应用。这在leetcode上也是一道题目,编号:71. 简化路径,大家有空可以做一下。

递归的实现是栈:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。

所以栈在计算机领域中应用是非常广泛的。

括号匹配问题

括号匹配是使用栈解决的经典问题。

建议要写代码之前要分析好有哪几种不匹配的情况,如果不动手之前分析好,写出的代码也会有很多问题。

先来分析一下 这里有三种不匹配的情况,

第一种情况,字符串里左方向的括号多余了,所以不匹配。
第二种情况,括号没有多余,但是括号的类型没有匹配上。
第三种情况,字符串里右方向的括号多余了,所以不匹配。

这里还有一些技巧,在匹配左括号的时候,右括号先入栈,就只需要比较当前元素和栈顶相不相等就可以了,比左括号先入栈代码实现要简单的多了!

字符串去重问题

思路就是可以把字符串顺序放到一个栈中,然后如果相同的话 栈就弹出,这样最后栈里剩下的元素都是相邻不相同的元素了。

逆波兰表达式问题

本题中每一个子表达式要得出一个结果,然后拿这个结果再进行运算,那么这岂不就是一个相邻字符串消除的过程。

滑动窗口最大值问题

在栈与队列:滑动窗口里求最大值引出一个重要数据结构 (opens new window)中讲解了一种数据结构:单调队列。

这道题目还是比较绕的,如果第一次遇到这种题目,需要反复琢磨琢磨

主要思想是队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队列里的元素数值是由大到小的。

那么这个维护元素单调递减的队列就叫做单调队列,即单调递减或单调递增的队列。C++中没有直接支持单调队列,需要我们自己来一个单调队列

而且不要以为实现的单调队列就是 对窗口里面的数进行排序,如果排序的话,那和优先级队列又有什么区别了呢。

设计单调队列的时候,pop,和push操作要保持如下规则:

pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作
push(value):如果push的元素value大于入口元素的数值,那么就将队列出口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止
保持如上规则,每次窗口移动的时候,只要问que.front()就可以返回当前窗口的最大值。

一些同学还会对单调队列都有一些困惑,首先要明确的是,题解中单调队列里的pop和push接口,仅适用于本题。

单调队列不是一成不变的,而是不同场景不同写法,总之要保证队列里单调递减或递增的原则,所以叫做单调队列。

不要以为本地中的单调队列实现就是固定的写法。

我们用deque作为单调队列的底层数据结构,C++中deque是stack和queue默认的底层实现容器(这个我们之前已经讲过),deque是可以两边扩展的,而且deque里元素并不是严格的连续分布的。

求前 K 个高频元素

通过求前 K 个高频元素,引出另一种队列就是优先级队列。

什么是优先级队列呢?

其实就是一个披着队列外衣的堆,因为优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。

而且优先级队列内部元素是自动依照元素的权值排列。那么它是如何有序排列的呢?

缺省情况下priority_queue利用max-heap(大顶堆)完成对元素的排序,这个大顶堆是以vector为表现形式的complete binary tree(完全二叉树)。

什么是堆呢?

堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。

所以大家经常说的大顶堆(堆头是最大元素),小顶堆(堆头是最小元素),如果懒得自己实现的话,就直接用priority_queue(优先级队列)就可以了,底层实现都是一样的,从小到大排就是小顶堆,从大到小排就是大顶堆。

本题就要使用优先级队列来对部分频率进行排序。 注意这里是对部分数据进行排序而不需要对所有数据排序!

所以排序的过程的时间复杂度是 O ( log ⁡ k ) O(\log k) O(logk),整个算法的时间复杂度是 O ( n log ⁡ k ) O(n\log k) O(nlogk)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/284652.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

redis之五种基本数据类型

redis存储任何类型的数据都是以key-value形式保存&#xff0c;并且所有的key都是字符串&#xff0c;所以讨论基础数据结构都是基于value的数据类型 常见的5种数据类型是&#xff1a;String、List、Set、Zset、Hash 一) 字符串(String) String是redis最基本的类型&#xff0c;v…

【SpringBoot篇】基于布隆过滤器,缓存空值,解决缓存穿透问题 (商铺查询时可用)

文章目录 &#x1f354;什么是缓存穿透&#x1f384;解决办法⭐缓存空值处理&#x1f388;优点&#x1f388;缺点&#x1f38d;代码实现 ⭐布隆过滤器&#x1f38d;代码实现 &#x1f354;什么是缓存穿透 缓存穿透是指在使用缓存机制时&#xff0c;大量的请求无法从缓存中获取…

docker-compose介绍和用法

docker-compose介绍和用法详解 1、docker-compose介绍2、docker-compose build3、docker-compose down4、docker-compose up -d 1、docker-compose介绍 Docker Compose是一个用于快速配置多个Docker容器的工具。它是一个定义和运行多容器的Docker应用工具&#xff0c;通过YAML…

设计模式-GOF对各个模式的定义

以下内容是对设计模式之父GOF的著作《设计模式——可复用面向对象软件的基础》定义的摘抄 1 抽象工厂 意图 提供一个接口以创建一系列相关或相互依赖的对象&#xff0c;而无须指定它们具体的类。 适用性 在以下情况下使用抽象工厂模式&#xff1a; 一个系统要独立于它的产…

docker基本命令

1.docker命令图解 2. 从仓库拉取镜像 #下载最新版 docker pull nginx # 镜像名:版本名&#xff08;标签&#xff09; docker pull nginx:1.20.1docker rmi 镜像名:版本号/镜像id3. 容器启动及停止 docker run [OPTIONS] IMAGE [COMMAND] [ARG...] docker run [设置项] 镜…

栈——OJ题

&#x1f4d8;北尘_&#xff1a;个人主页 &#x1f30e;个人专栏:《Linux操作系统》《经典算法试题 》《C》 《数据结构与算法》 ☀️走在路上&#xff0c;不忘来时的初心 文章目录 一、最小栈1、题目讲解2、思路讲解3、代码实现 二、栈的压入、弹出序列1、题目讲解2、思路讲解…

Windows使用VNC Viewer远程桌面Ubuntu【内网穿透】

文章目录 前言1. ubuntu安装VNC2. 设置vnc开机启动3. windows 安装VNC viewer连接工具4. 内网穿透4.1 安装cpolar【支持使用一键脚本命令安装】4.2 创建隧道映射4.3 测试公网远程访问 5. 配置固定TCP地址5.1 保留一个固定的公网TCP端口地址5.2 配置固定公网TCP端口地址5.3 测试…

系列十、存储引擎

一、存储引擎 1.1、概述 大家可能没有听说过存储引擎&#xff0c;但是一定听过引擎这个词&#xff0c;引擎就是发动机&#xff0c;是一个机器的核心组件。 比如&#xff0c;对于舰载机、直升机、火箭来说&#xff0c;它们都有各自的引擎&#xff0c;引擎是它们最为核心的组件。…

【NI-RIO入门】使用LabVIEW进行数据采集测量

于ni kb摘录 选择合适的编程模式 CompactRIO系统具有至少两个用户可选模式。某些CompactRIO型号具有附加的用户可选模式&#xff0c;可以在实时NI-DAQmx中进行编程。请参考本文以判断您的CompactRIO是否能够使用实时NI-DAQmx。将目标添加到项目后&#xff0c;将提示您选择要使…

Jmeter接口测试断言

一、响应断言 对服务器的响应接口进行断言校验&#xff0c;来判断接口测试得到的接口返回值是否正确。 二、添加断言 1、apply to&#xff1a; 通常发出一个请求只触发一个请求&#xff0c;所以勾选“main sampie only”就可以&#xff1b;若发一个请求可以触发多个服务器请…

【C++】标准库中的string类

目录 一、介绍 二、常用接口 1.构造函数和赋值操作 2.string类对象的容量操作 3.string类对象的访问及遍历操作 4.string类对象的修改操作 5.string类非成员函数 放在最后 一、介绍 在C中&#xff0c;std::string 是标准库提供的字符串类。它属于 <string> 头文件…

(已解决)如何使用matplotlib绘制小提琴图

网上很多人使用seaborn绘制小提琴图&#xff0c;本人暂时不想学新的东西&#xff0c;就是懒。本文介绍如何使用matplotlib绘制小提琴图&#xff0c;很多其他博客只是使用最简单的语法&#xff0c;默认小提琴颜色会是蓝色&#xff0c;根本改不了。本文使用了一点高级的用法&…