<冲刺大厂之算法刷题>栈和队列

博客首页:热爱编程的大李子

专栏首页:LeetCode刷题

博主在学习阶段,如若发现问题,请告知,非常感谢

同时也非常感谢各位小伙伴们的支持

每日一语:I walk slowly, but I never walk backwards.

感谢: 我只是站在巨人们的肩膀上整理本篇文章,感谢走在前路的大佬们!

最后,祝大家每天进步亿点点! 欢迎大家点赞➕收藏⭐️➕评论支持博主!

⭐️ ⭐️上篇文章-<冲刺大厂之算法刷题>字符串 ⭐️ ⭐️

文章目录

    • 232. 用栈实现队列
        • 题目描述
        • 思路分析
        • 参考代码
    • 225. 用队列实现栈
        • 题目描述
        • 思路分析
        • 参考代码
    • 20. 有效的括号
        • 题目描述
        • 思路分析
        • 参考代码
    • 1047. 删除字符串中的所有相邻重复项
        • 题目描述
        • 思路分析
        • 参考代码
        • STL中字符串常用方法总结
    • 150. 逆波兰表达式求值
        • 题目描述
        • 思路分析
        • 参考代码
    • 239. 滑动窗口最大值
        • 题目描述
        • 思路分析
        • 参考代码
    • 347. 前 K 个高频元素
        • 题目描述
        • 思路分析
        • 参考代码
    • 总结


232. 用栈实现队列

题目描述

请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):

实现 MyQueue 类:

void push(int x) 将元素 x 推到队列的末尾
int pop() 从队列的开头移除并返回元素
int peek() 返回队列开头的元素
boolean empty() 如果队列为空,返回 true ;否则,返回 false

输入:
["MyQueue", "push", "push", "peek", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 1, 1, false]

思路分析

栈存储元素的顺序和队列正好是相反的,栈:先进后出,队列:先进先出.
所以模拟队列时需要两个栈,一个用于push输入元素:stIn,一个用于pop()操作:stOut.

  • push():直接将元素push到stIn即可.
  • pop():先判断stOut是否为空,如果为空则将stIn的元素移动到stOut中,然后再进行pop()操作 。如果不为空,则直接进行弹出操作==> (stOut.top(),stOut.pop())
  • peek():可以利用pop()获得元素,再将其压入到队列中.
  • empty():需要判断stIn和stOut是否都为空.

图解:

参考代码

#include
using namespace std;

class MyQueue {
	public:
		//栈:先进后出
		//队列:先进先出
		//所以需要两个栈才可以模拟队列的效果
		stack<int> stIn;//输入栈,用于放数据
		stack<int> stOut;//输出栈,用于弹出数据
		MyQueue() {

		}

		void push(int x) {
			stIn.push(x);
		}

		int pop() {
			int x;
			if(!stOut.empty()) {
				x = stOut.top();
				stOut.pop();
				return x;
			} else {
				//将stIn中的元素转移到stOut中,然后再做弹出操作
				while(!stIn.empty()) {
					stOut.push(stIn.top());
					stIn.pop();
				}
				x = stOut.top();
				stOut.pop();
				return x;
			}
		}

		int peek() {
			int x = this->pop();//获取栈顶元素和pop思路类似. 
			stOut.push(x);
			return x;
		}

		bool empty() {
			if(stIn.empty()&&stOut.empty()){
				return true;
			}else{
				return false;
			}
		}
};

/**
 * 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();
 */

225. 用队列实现栈

题目描述

请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。

实现 MyStack 类:

void push(int x) 将元素 x 压入栈顶。
int pop() 移除并返回栈顶元素。
int top() 返回栈顶元素。
boolean empty() 如果栈是空的,返回 true ;否则,返回 false 。

实例

输入:
["MyStack", "push", "push", "top", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 2, 2, false]

思路分析

队列是先进先出的规则 ,把一个队列中的数据导入另一个队列中,数据的顺序并没有变,并没有变成先进后出的顺序。
所以用栈实现队列, 和用队列实现栈的思路还是不一样的.

方法一:两个队列来模拟栈

但是依然还是要用两个队列来模拟栈,只不过没有输入和输出的关系,而是另一个队列完全用又来备份的!

具体实现: 用两个队列que1和que2实现队列的功能,que2其实完全就是一个备份的作用,把que1最后面的元素以外的元素都备份到que2,然后弹出最后面的元素,再把其他元素从que2导回que1。

图解:

方法二:一个队列来模拟栈

仔细观察方法一, 我们用另一个队列是备份元素的作用,那么我们可以直接把除了最后一个外的元素 都放置到队列的末尾, 此时每次弹出元素的顺序就和栈一样了.

参考代码

方法一:两个队列来模拟栈

class MyStack {
	public:
		queue<int> que1;
		queue<int> que2;//辅助队列,用于备份数据 
		MyStack() {

		}

		void push(int x) {
			que1.push(x) ;
		}

		//弹出元素 
		//思路:弹出最后一个元素前面的元素到que2中,然后 把最后一个元素弹出并返回 
		int pop() {
			//	
			int size = que1.size();
			size--;
			while(size--) {
				que2.push(que1.front());
				que1.pop();
			}
			int x = que1.front();//将目标元素放入结果值,并进行弹出 
			que1.pop();
			//将que2再重新放入到que1中
			while(!que2.empty()){
				que1.push(que2.front());
				que2.pop();
			}
			return x;
		}

		int top() {
			return que1.back(); 
		}

		bool empty() {
			return que1.empty();
		}
};

方法二:一个队列来模拟栈

//方法二:当只有一个队列来进行实现
//弹出:只需要将除了最后一个元素外,其他元素都放到队列的最后即可 
class MyStack {
	public:
		queue<int> que1;
		MyStack() {

		}

		void push(int x) {
			que1.push(x) ;
		}

		//弹出元素 
		//思路:弹出最后一个元素前面的元素到que2中,然后 把最后一个元素弹出并返回 
		int pop() {
			int size = que1.size();
			size--;
			while(size--) {
				que1.push(que1.front());
				que1.pop();
			}
			int x = que1.front();//将目标元素放入结果值,并进行弹出 
			que1.pop();
			return x;
		}

		int top() {
			return que1.back(); 
		}

		bool empty() {
			return que1.empty();
		}
};

20. 有效的括号

题目描述

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

有效字符串需满足:

左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。

示例 1:

输入:s = "()"
输出:true

示例 2:

输入:s = "()[]{}"
输出:true

示例 3:

输入:s = "(]"
输出:false

示例 4:

输入:s = "([)]"
输出:false

示例 5:

输入:s = "{[]}"
输出:true

思路分析

方法一:栈的基本使用

方法二:分情况讨论

字符串不匹配的三种情况:

  • 第一种情况,字符串里左方向的括号多余了 ,所以不匹配。
    <冲刺大厂之算法刷题>栈和队列_第1张图片
  • 第二种情况,字符串里右方向的括号多余了,所以不匹配。
    <冲刺大厂之算法刷题>栈和队列_第2张图片
  • 第三种情况,括号没有多余,但是 括号的类型没有匹配上
    <冲刺大厂之算法刷题>栈和队列_第3张图片

算法步骤:

  • 第一种情况:字符串已经遍历完,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false

  • 第二种情况:遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号return false

  • 第二种情况:遍历字符串匹配的过程中,发现栈头元素和当前元素不匹配。所以return false

<冲刺大厂之算法刷题>栈和队列_第4张图片

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

参考代码

方法一:栈的基本使用

bool isValid(string s) {
	stack<char> tab;
	for(char ch : s){
		if(tab.empty()){
			tab.push(ch);
		}else{
			if((tab.top()=='('&&ch==')') ||(tab.top()=='{'&&ch=='}')|| (tab.top()=='['&&ch==']')){
				tab.pop();
			}else{
				tab.push(ch);
			}
		}
	}
	return s.empty();

}

方法二:分情况讨论

bool isValid(string s) {
	stack<char> st;
	for(char ch : s){
		if(ch=='('){
			st.push(')');
		}else if(ch=='['){
		 st.push(']');
		}else if(ch=='{'){
			st.push('}');
		}else if(st.empty() || st.top()!= ch){//如果栈为空(Case2)或括号不匹配(Case3)或者,则结束 
			return false;
		}else{//如果相等 
			st.pop();
		}
	}
	return st.empty(); //如果元素遍历完毕,而且栈也为空,那就说明括号序列是合法的. 如果不为空则对应着Case1
}

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

题目描述

给出由小写字母组成的字符串 S,重复项删除操作会选择两个相邻且相同的字母,并删除它们。

在 S 上反复执行重复项删除操作,直到无法继续删除。

在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。

示例:

输入:"abbaca"
输出:"ca"

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

思路分析

栈的使用

参考代码

string removeDuplicates(string s) {
	string res;
	stack<char> st;
	for(char ch : s){
		if(!st.empty()&&st.top()==ch){//栈不为空,别切栈顶元素和遍历元素相当,则弹出栈 
			st.pop();
		}else{//栈为空/栈顶元素和遍历元素 
			st.push(ch);
		}
	}
	while(!st.empty()){
		res+=st.top();
		st.pop();
	}
	reverse(res.begin(),res.end());
	return res;
}

补充:直接拿字符串作为栈

string removeDuplicates(string s) {
	string res;
	for(char ch : s){
		if(res.empty()|| res.back()!= ch){
			res.push_back(ch);
		}else{
			res.pop_back();
		}
	} 
	return res;
}

STL中字符串常用方法总结

  • begin(),end()
  • size(),length()
  • resize()
  • empty()
  • back() :获取最后一个字符
  • front():获取第一个字符
  • push_back() 增加一个字符到末尾
  • pop_back() 删除最后一个字符
  • substr(pos,size)

150. 逆波兰表达式求值

题目描述

根据 逆波兰表示法,求表达式的值。

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

说明:

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

示例 1:

输入:tokens = ["2","1","+","3","*"]
输出:9

解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9

示例 2:

输入:tokens = ["4","13","5","/","+"]
输出:6

解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6

示例 3:

输入:tokens = ["10","6","9","3","+","-11","*","/","*","17","+","5","+"]
输出:22

解释:
该算式转化为常见的中缀算术表达式为:
((10 * (6 / ((9 + 3) * -11))) + 17) + 5
= ((10 * (6 / (12 * -11))) + 17) + 5
= ((10 * (6 / -132)) + 17) + 5
= ((10 * 0) + 17) + 5
= (0 + 17) + 5
= 17 + 5
= 22

逆波兰表达式介绍:

逆波兰表达式是一种后缀表达式,所谓后缀就是指算符写在后面。

  • 平常使用的算式则是一种中缀表达式,如 ( 1 + 2 ) * ( 3 + 4 ) 。
  • 该算式的逆波兰表达式写法为 ( ( 1 2 + ) ( 3 4 + ) * ) 。

逆波兰表达式主要有以下两个优点:

  • 去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。
  • 适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中。

思路分析

栈的使用

  • 如果是数字则压入栈
  • 如果是运算符,则从栈中弹出两个数字进行运算,运算结果再重新压入栈中
  • 当数组遍历完毕后,返回栈中的最后一个数字即是结果.

参考代码

int evalRPN(vector<string>& tokens) {
	stack<int> st;
	for(int i = 0;i < tokens.size();i++) {
		if(tokens[i]=="+" || tokens[i]=="-" || tokens[i]=="*" || tokens[i]=="/"){
			int num1 = st.top();
			st.pop();
			int num2 = st.top();
			st.pop();
			if(tokens[i]=="+") {
				st.push(num1+num2);
			} else if(tokens[i]=="-") {
				st.push(num1-num2);
			} else if(tokens[i]=="*") {
				st.push(num1*num2);
			} else {
				st.push(num1/num2);
			}
		}else{
			st.push(stoi(tokens[i]));
		}
	}
	return st.top();
}

239. 滑动窗口最大值

题目描述

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

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

示例 1:

输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]

解释:

 [1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7

示例 2:

输入:nums = [1], k = 1
输出:[1]

示例 3:

输入:nums = [1,-1], k = 1
输出:[1,-1]

示例 4:

输入:nums = [9,11], k = 2
输出:[11]

示例 5:

输入:nums = [4,-2], k = 2
输出:[4]

思路分析

单调队列的使用
这道题不复杂,难点在于如何在 O(1) 时间算出每个「窗口」中的最大值,使得整个算法在线性时间完成。就需要「单调队列」这种特殊的数据结构来辅助了。

C++普通队列:

class Queue {
    void push(int n);
    // 或 enqueue,在队尾加入元素 n
    void pop();
    // 或 dequeue,删除队头元素
}

单调队列:

class MonotonicQueue {
    // 在队尾添加元素 n
    void push(int n);
    // 返回当前队列中的最大值
    int max();
    // 队头元素如果是 n,删除它
    void pop(int n);
}

实现单调队列数据结构
单调队列的 push 方法依然在队尾添加元素,但是要把前面比新元素小的元素都删掉; 要用到双端队列:deque

完整代码:

class MonotonicQueue {
	private:
		deque<int> Q;
	public:
		void push(int n) {
			while(!Q.empty() && Q.back() < n) { //把 < n的元素都压扁
				Q.pop_back();//从后面弹出..
			}
			Q.push_back(n);
		}
		int max() {
			return Q.front();//最大的便是对头元素
		}

		void pop(int n) {
			if(!Q.empty()&& Q.front()==n) { //如果窗口移除的元素= 最大值, 则单调队列中删除该元素.
				Q.pop_front();//从前面弹出.
			}
		}
};

功能演示:

<冲刺大厂之算法刷题>栈和队列_第5张图片

**注:**关于单调栈的原理请看:LeetCode刷题day31

参考代码

#include
using namespace std;

class MyQueue { //单调队列
	public:
		deque<int> que;//使用deque来实现单调队列
		//弹出元素,比较当前队列头部的值和窗口的最左边的值(即将移除窗口) 是否相等,如果相等则弹出. 
		void pop(int value) {
			if(!que.empty()&& value == que.front()) {
				que.pop_front();
			}
		}
		
		//push压入元素 (保证que中元素是从大到小排列的) 
		//先将 
		void push(int value) {
			while(!que.empty() && value > que.back()){
				que.pop_back();
			}
			que.push_back(value);
		}
		
		//查询当前队列里的最大值,直接返回队头元素 就OK了 
		int front() {
			return que.front();
		}
};

vector<int> maxSlidingWindow(vector<int>& nums, int k) {
	MyQueue que;
	vector<int>  result;
	for(int i = 0;i < k;i++){//将前k个元素加入到单调队列中 
		que.push(nums[i]);
	}
	result.push_back(que.front());//记录前k个元素的 最大值
	for(int i = k; i < nums.size(); i++) {
		que.pop(nums[i-k]);//弹出窗口最左边元素  注意这里是弹出之前的窗口最左边值所以不用+1了哦. 
		que.push(nums[i]);//压入窗口右边的新元素 
		result.push_back(que.front()) ;//将当前窗口的最大值加入结果集 
	}
	return result;
}

347. 前 K 个高频元素

题目描述

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

示例 1:

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

示例 2:

输入: nums = [1], k = 1
输出: [1]

思路分析

这个题考察内容:

  • 要统计元素出现频率==>使用map
  • 对频率排序=>使用优先队列
  • 找出前K个高频元素

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

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

所以我们可以用小顶堆,因为要统计最大前k个元素,只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素。(当然如果是大根堆,那么弹出前k个元素就行.)

<冲刺大厂之算法刷题>栈和队列_第6张图片

参考代码

//定义优先队列的排序规则(优先队列和常规的排序规则(如快排)正好相反
class mycomparison {
	public:
		bool operator(const pair<int,int>& l,const pair<int,int>& r) {
			return l.second > r.second;
		}
};

class Solution {

	public:
		//小顶堆
		vector<int> topKFrequent(vector<int>& nums, int k) {
			//要统计元素出现的频率
			unordered_map<int,int> map;
			for(int i = 0;i < nums.size();i++){
				map[nums[i]]++;
			}
			//对频率排序
			//定义一个小根堆,大小为k
		   priority_queue<pair<int,int>,vector<pair<int,int>>, mycomparison> Q;
			//用固定大小为k的小根堆,扫到所有频率的数值
			for(unordered_map<int,int>::iterator it = map.begin(); it!= map.end(); it++){
				Q.push(*it);
				if(Q.size()>k){
					Q.pop();
				}
			} 
			
			//找出前k个高频元素,因为小根堆先弹出的是最小的. 
			vector<int> res(k);
			for(int i = k-1; i>=0;i--){
				res[i] = Q.top().first;
				Q.pop();
			} 
			return res;
		}
};

注:pair可看做map的一个子元素模板,可以对map进行赋值,初始化等操作.

总结

OK,今天关于栈和队列算法整理就到这里的,希望本篇文章能够帮助到大家,同时也希望大家看后能学有所获!!!

好了,我们下期见~
<冲刺大厂之算法刷题>栈和队列_第7张图片

你可能感兴趣的:(LeetCode刷题,算法,c++,leetcode)