【LeetCode】7.栈与队列
总目录:
0.理论基础
0.1.要点
栈:先进后出
队列:先进先出
双端队列:两端都可以增删
优先队列:大根堆,压入的数据自动排序,较大值在前;小根堆反之
1.用栈实现队列
1.1.问题描述
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):
实现 MyQueue 类:
void push(int x) 将元素 x 推到队列的末尾
int pop() 从队列的开头移除并返回元素
int peek() 返回队列开头的元素
boolean empty() 如果队列为空,返回 true ;否则,返回 false
说明:
你 只能 使用标准的栈操作 —— 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。
链接:https://leetcode.cn/problems/implement-queue-using-stacks
1.2.要点
1模拟法,用1个栈存顺序的数据,另一个栈用作缓存
1.3.代码实例
模拟法
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class MyQueue { 2 private: 3 bool inCatch=false; 4 stack<int> s1,s2; 5 public: 6 MyQueue() { 7 while(!s1.empty()){ 8 s1.pop(); 9 } 10 while(!s2.empty()){ 11 s2.pop(); 12 } 13 } 14 15 void push(int x) { 16 if(inCatch){ 17 while(!s2.empty()){ 18 s1.push(s2.top()); 19 s2.pop(); 20 } 21 inCatch=false; 22 } 23 s1.push(x); 24 } 25 26 int pop() { 27 int ret=0; 28 if(!inCatch){ 29 while(!s1.empty()){ 30 s2.push(s1.top()); 31 s1.pop(); 32 } 33 inCatch=true; 34 } 35 36 if(s2.empty()){ 37 return ret; 38 } 39 ret=s2.top(); 40 s2.pop(); 41 return ret; 42 } 43 44 int peek() { 45 int ret=0; 46 if(!inCatch){ 47 while(!s1.empty()){ 48 s2.push(s1.top()); 49 s1.pop(); 50 } 51 inCatch=true; 52 } 53 54 if(s2.empty()){ 55 return ret; 56 } 57 ret=s2.top(); 58 return ret; 59 } 60 61 bool empty() { 62 return s1.empty()&&s2.empty(); 63 } 64 }; 65 66 /** 67 * Your MyQueue object will be instantiated and called as such: 68 * MyQueue* obj = new MyQueue(); 69 * obj->push(x); 70 * int param_2 = obj->pop(); 71 * int param_3 = obj->peek(); 72 * bool param_4 = obj->empty(); 73 */
2.用队列实现栈
2.1.问题描述
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。
实现 MyStack 类:
void push(int x) 将元素 x 压入栈顶。
int pop() 移除并返回栈顶元素。
int top() 返回栈顶元素。
boolean empty() 如果栈是空的,返回 true ;否则,返回 false 。
注意:
你只能使用队列的基本操作 —— 也就是 push to back、peek/pop from front、size 和 is empty 这些操作。
链接:https://leetcode.cn/problems/implement-stack-using-queues
2.2.要点
1模拟法,用1个队列存数据,另外一个队列作缓存
2.3.代码实例
模拟法
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class MyStack { 2 private: 3 bool inCatch=false; 4 queue<int> q1,q2; 5 public: 6 MyStack() { 7 while(!q1.empty()){ 8 q1.pop(); 9 } 10 while(!q2.empty()){ 11 q2.pop(); 12 } 13 } 14 15 void push(int x) { 16 if(inCatch){ 17 while(!q2.empty()){ 18 q1.push(q2.front()); 19 q2.pop(); 20 } 21 inCatch=false; 22 } 23 q1.push(x); 24 } 25 26 int pop() { 27 int ret=0; 28 if(inCatch){ 29 if(q2.empty()){ 30 return ret; 31 } 32 while(q2.size()>1){ 33 q1.push(q2.front()); 34 q2.pop(); 35 } 36 ret=q2.front(); 37 q2.pop(); 38 inCatch=false; 39 } 40 else{ 41 if(q1.empty()){ 42 return ret; 43 } 44 while(q1.size()>1){ 45 q2.push(q1.front()); 46 q1.pop(); 47 } 48 ret=q1.front(); 49 q1.pop(); 50 inCatch=true; 51 } 52 53 return ret; 54 } 55 56 int top() { 57 int ret=0; 58 if(inCatch){ 59 if(q2.empty()){ 60 return ret; 61 } 62 while(q2.size()>1){ 63 q1.push(q2.front()); 64 q2.pop(); 65 } 66 ret=q2.front(); 67 q1.push(q2.front()); 68 q2.pop(); 69 inCatch=false; 70 } 71 else{ 72 if(q1.empty()){ 73 return ret; 74 } 75 while(q1.size()>1){ 76 q2.push(q1.front()); 77 q1.pop(); 78 } 79 ret=q1.front(); 80 q2.push(q1.front()); 81 q1.pop(); 82 inCatch=true; 83 } 84 85 return ret; 86 } 87 88 bool empty() { 89 return q1.empty()&&q2.empty(); 90 } 91 };
3.有效的括号
3.1.问题描述
给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
每个右括号都有一个对应的相同类型的左括号。
链接:https://leetcode.cn/problems/valid-parentheses
3.2.要点
当遇到闭合符号时,前一个符号必须是与其对应的左侧起始符号。
1辅助栈
检查字符串长度是否是奇数,奇数则直接返回false
建立右侧符号与其左侧符号的key-value集合,遍历字符串,当遇到非右侧符号时直接压栈,当遇到右侧符号时检查栈顶是否为其对应的左侧符号、不是则返回false
最后检查栈是否为空,否则为false
3.3.代码实例
辅助栈
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class Solution { 2 public: 3 bool isValid(string s) { 4 int n = s.size(); 5 if (n % 2 == 1) { 6 return false; 7 } 8 9 unordered_map<char, char> pairs = { 10 {')', '('}, 11 {']', '['}, 12 {'}', '{'} 13 }; 14 stack<char> stk; 15 for (char ch: s) { 16 if (pairs.count(ch)) { 17 if (stk.empty() || stk.top() != pairs[ch]) { 18 return false; 19 } 20 stk.pop(); 21 } 22 else { 23 stk.push(ch); 24 } 25 } 26 return stk.empty(); 27 } 28 };
4.删除字符串中所有相邻重复项
4.1.问题描述
给出由小写字母组成的字符串 S
,重复项删除操作会选择两个相邻且相同的字母,并删除它们。
在 S 上反复执行重复项删除操作,直到无法继续删除。
在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
链接:https://leetcode.cn/problems/remove-all-adjacent-duplicates-in-string/
4.2.要点
1栈
压入数据时检查栈顶元素是否相同,相同则弹栈,不同则入栈,最后拼接字符串
4.3.代码实例
由于 std::stringstd::string 类本身就提供了类似「入栈」和「出栈」的接口,因此我们直接将需要被返回的字符串作为栈即可
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class Solution { 2 public: 3 string removeDuplicates(string s) { 4 string stk; 5 for (char ch : s) { 6 if (!stk.empty() && stk.back() == ch) { 7 stk.pop_back(); 8 } else { 9 stk.push_back(ch); 10 } 11 } 12 return stk; 13 } 14 };
辅助栈
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class Solution { 2 public: 3 string removeDuplicates(string s) { 4 stack<char> st; 5 for(int i=0;i<s.length();i++){ 6 if(!st.empty()&&st.top()==s[i]){ 7 st.pop(); 8 continue; 9 } 10 11 st.push(s[i]); 12 } 13 int nums=0; 14 stack<char> catchS; 15 while(!st.empty()){ 16 catchS.push(st.top()); 17 st.pop(); 18 nums++; 19 } 20 21 string strRet; 22 for(int i=0;i<nums;i++){ 23 strRet+=catchS.top(); 24 catchS.pop(); 25 } 26 return strRet; 27 } 28 };
5.逆波兰表达式求值
5.1.问题描述
根据 逆波兰表示法,求表达式的值。
有效的算符包括 +、-、*、/ 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。
注意 两个整数之间的除法只保留整数部分。
可以保证给定的逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。
示例 1:
输入:tokens = ["2","1","+","3","*"]
输出:9
解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9
链接:https://leetcode.cn/problems/evaluate-reverse-polish-notation
5.2.要点
适合栈的特点,根据后面遇到的元素选择策略来处理前面相邻元素。
遇到运算符则将前两个数字拿出来作运算,并将结果压回栈里。
5.3.代码实例
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class Solution { 2 public: 3 int evalRPN(vector<string>& tokens) { 4 stack<long long> stk; 5 int n = tokens.size(); 6 for (int i = 0; i < n; i++) { 7 string& token = tokens[i]; 8 if (isNumber(token)) { 9 stk.push((long long)atoi(token.c_str())); 10 } else { 11 long long num2 = stk.top(); 12 stk.pop(); 13 long long num1 = stk.top(); 14 stk.pop(); 15 switch (token[0]) { 16 case '+': 17 stk.push(num1 + num2); 18 break; 19 case '-': 20 stk.push(num1 - num2); 21 break; 22 case '*': 23 stk.push(num1 * num2); 24 break; 25 case '/': 26 stk.push(num1 / num2); 27 break; 28 } 29 } 30 } 31 return (int)stk.top(); 32 } 33 34 bool isNumber(string& token) { 35 return !(token == "+" || token == "-" || token == "*" || token == "/"); 36 } 37 };
6.问滑动窗口最大值
6.1.问题描述
给你一个整数数组 nums
,有一个大小为 k
的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k
个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
6.2.要点
1大根堆,如何处理还在堆中、滑出了窗口的元素?
2单调队列
3分块+预处理
这题得好好学习一下了。
法一中,去除最大值在窗口左边的方法要学;
法二中,虽然知道类似的单调栈这种东西,但是实际应用要学,这题可以作为一个例子;
法三的分块的做法也曾经见过一次,但是没注意,以为是偏方,现在又出现了,看来也得学
6.3.代码实例
大根堆
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class Solution { 2 public: 3 vector<int> maxSlidingWindow(vector<int>& nums, int k) { 4 int n = nums.size(); 5 priority_queue<pair<int, int>> q; 6 for (int i = 0; i < k; ++i) { 7 q.emplace(nums[i], i); 8 } 9 vector<int> ans = {q.top().first}; 10 for (int i = k; i < n; ++i) { 11 //入堆 12 q.emplace(nums[i], i); 13 14 //清除失效元素 15 while (q.top().second <= i - k) { 16 q.pop(); 17 } 18 19 //取最大值 20 ans.push_back(q.top().first); 21 } 22 return ans; 23 } 24 };
单调队列小说:
单调队列真是一种让人感到五味杂陈的数据结构,它的维护过程更是如此.....就拿此题来说,队头最大,往队尾方向单调......有机会站在队头的老大永远心狠手辣,当它从队尾杀进去的时候,如果它发现这里面没一个够自己打的,它会毫无人性地屠城,把原先队里的人头全部丢出去,转身建立起自己的政权,野心勃勃地准备开创一个新的王朝.....这时候,它的人格竟发生了一百八十度大反转,它变成了一位胸怀宽广的慈父!它热情地请那些新来的“小个子”们入住自己的王国......然而,这些小个子似乎天性都是一样的——嫉妒心强,倘若见到比自己还小的居然更早入住王国,它们会心狠手辣地找一个夜晚把它们通通干掉,好让自己享受更大的“蛋糕”;当然,遇到比自己强大的,它们也没辙,乖乖夹起尾巴做人。像这样的暗杀事件每天都在上演,虽然王国里日益笼罩上白色恐怖,但是好在没有后来者强大到足以干翻国王,江山还算能稳住。直到有一天,闯进来了一位真正厉害的角色,就像当年打江山的国王一样,手段狠辣,野心膨胀,于是又是大屠城......历史总是轮回的。
有那么一位国王足够强大,经历过层层血腥屠杀之后坐上了王座、镇压了次次颠覆行动,但他已经坐了足够久,久到时间的洪流滚滚向前把他抛弃而自然死去,帝国平平静静地迎来了第一顺位的新王登基,历史翻开了新的一页、添上新的一笔。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class Solution { 2 public: 3 vector<int> maxSlidingWindow(vector<int>& nums, int k) { 4 int n = nums.size(); 5 deque<int> q; 6 for (int i = 0; i < k; ++i) { 7 while (!q.empty() && nums[i] >= nums[q.back()]) { 8 q.pop_back(); 9 } 10 q.push_back(i); 11 } 12 13 vector<int> ans = {nums[q.front()]}; 14 for (int i = k; i < n; ++i) { 15 while (!q.empty() && nums[i] >= nums[q.back()]) { 16 q.pop_back(); 17 } 18 q.push_back(i); 19 while (q.front() <= i - k) { 20 q.pop_front(); 21 } 22 ans.push_back(nums[q.front()]); 23 } 24 return ans; 25 } 26 };
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class Solution { 2 public: 3 vector<int> maxSlidingWindow(vector<int>& nums, int k) { 4 int n = nums.size(); 5 vector<int> prefixMax(n), suffixMax(n); 6 for (int i = 0; i < n; ++i) { 7 if (i % k == 0) { 8 prefixMax[i] = nums[i]; 9 } 10 else { 11 prefixMax[i] = max(prefixMax[i - 1], nums[i]); 12 } 13 } 14 for (int i = n - 1; i >= 0; --i) { 15 if (i == n - 1 || (i + 1) % k == 0) { 16 suffixMax[i] = nums[i]; 17 } 18 else { 19 suffixMax[i] = max(suffixMax[i + 1], nums[i]); 20 } 21 } 22 23 vector<int> ans; 24 for (int i = 0; i <= n - k; ++i) { 25 ans.push_back(max(suffixMax[i], prefixMax[i + k - 1])); 26 } 27 return ans; 28 } 29 };
7.前K个高频元素
7.1.问题描述
给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2;输出: [1,2]
链接:https://leetcode.cn/problems/top-k-frequent-elements
7.2.要点
1哈希+大根堆
先用map统计出现次数,再用大根堆排序
2哈希+小根堆
先用map统计出现次数,再用小根堆,这里的优化之处在于,先将小根堆塞满k个,然后小于堆顶的元素不必再入堆,大于等于堆顶的元素在入堆后要弹堆,始终保持堆中只有k个元素,减轻堆排序计算量。
另外,注意学习小根堆比较的重载。
7.3.代码实例
哈希+大根堆
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class Solution { 2 public: 3 vector<int> topKFrequent(vector<int>& nums, int k) { 4 int dataLen=nums.size(); 5 map<int,int> numsMap; 6 for(int i=0;i<dataLen;i++){ 7 numsMap[nums[i]]++; 8 } 9 10 priority_queue<pair<int, int>> q; 11 for(auto& p:numsMap){ 12 q.emplace(p.second,p.first); 13 } 14 15 vector<int> ret; 16 int cnt=0; 17 while(cnt<k && !q.empty()){ 18 ret.push_back(q.top().second); 19 q.pop(); 20 cnt++; 21 } 22 23 return ret; 24 } 25 };
哈希+小根堆
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class Solution { 2 public: 3 // 小顶堆 4 class mycomparison { 5 public: 6 bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) { 7 return lhs.second > rhs.second; 8 } 9 }; 10 vector<int> topKFrequent(vector<int>& nums, int k) { 11 // 要统计元素出现频率 12 unordered_map<int, int> map; // map<num[i], 对应出现次数> 13 for (int i = 0; i < nums.size(); i++) { 14 map[nums[i]]++; 15 } 16 17 // 对频率排序 18 // 定义一个小顶堆,大小为k 19 priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparison> pri_que; 20 21 // 用固定大小为k的小顶堆,扫描所有频率的数值 22 for (unordered_map<int, int>::iterator it = map.begin(); it != map.end(); it++) { 23 pri_que.push(*it); 24 if (pri_que.size() > k) {// 如果堆的大小大于k,则队列弹出,保证堆的大小一直为k 25 pri_que.pop(); 26 } 27 } 28 29 // 找出前k个高频元素,因为小顶堆先弹出的是最小的,所以倒序输出到数组 30 vector<int> ans(k); 31 for (int i = k - 1; i >= 0; i--) { 32 ans[i] = pri_que.top().first; 33 pri_que.pop(); 34 } 35 return ans; 36 } 37 };
8.总结
8.1.栈和队列的底层问题
(1)C++中stack 是容器么?
不是,栈和队列被归类为container adapter(容器适配器)。
(2)我们使用的STL中stack是如何实现的?
默认以deque作为底层容器。
(3)stack 提供迭代器来遍历stack空间么?
栈提供push 和 pop 等等接口,所有元素必须符合先进后出规则,所以栈不提供走访功能,也不提供迭代器(iterator),队列同理。
不像是set 或者map 提供迭代器iterator来遍历所有元素。
8.2.栈的经典问题
(1)递归的实现
(2)括号匹配问题
(3)字符串相邻字符去重问题
(4)逆波兰表达式计算问题
8.3.队列的经典问题
(1)滑动窗口最大值,使用大根堆、单调队列等等
(2)求前K个高频元素
(3)在树中层序遍历、广度搜索
xxx.问题
xxx.1.问题描述
111
xxx.2.要点
222
xxx.3.代码实例
333