本文为学习代码随想录 时所做的笔记,仅供学习参考,不做任何商业用途,若有侵权,请联系删除。
栈与队列理论基础
C++标准库是有多个版本的,要知道我们使用的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是开源软件,源码可读性甚高。
栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能)。
所以STL中栈往往不被归类为容器,而被归类为container
adapter(容器适配器)。
从下图中可以看出,栈的内部结构,栈的底层实现可以是vector,deque,list
都是可以的, 主要就是数组和链表的底层实现。
我们常用的SGI
STL,如果没有指定底层实现的话,默认是以deque为缺省情况下栈的低层结构。
deque是一个双向队列,只要封住一段,只开通另一端就可以实现栈的逻辑了。
SGI STL中
队列底层实现缺省情况下一样使用deque实现的。
我们也可以指定vector为栈的底层实现,初始化语句如下:
1 std::stack<int , std::vector<int > > third;
刚刚讲过栈的特性,对应的队列的情况是一样的。
队列中先进先出的数据结构,同样不允许有遍历行为,不提供迭代器,
SGI STL中队列一样是以deque为缺省情况下的底部结构。
也可以指定list 为起底层实现,初始化queue的语句如下:
1 std::queue<int , std::list<int >> third;
所以STL 队列也不被归类为容器,而被归类为container adapter(
容器适配器)。
用栈实现队列
232.
用栈实现队列 - 力扣(LeetCode)
在push数据的时候,只要数据放进输入栈就好,但在pop的时候,操作就复杂一些,输出栈如果为空,就把进栈数据全部导入进来(注意是全部导入) ,再从出栈弹出数据,如果输出栈不为空,则直接从出栈弹出数据就可以了。
最后如何判断队列为空呢?如果进栈和出栈都为空的话,说明模拟的队列为空了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 class MyQueue {public : stack<int > stIn; stack<int > stOut; MyQueue () { } void push (int x) { stIn.push (x); } int pop () { if (stOut.empty ()) { while (!stIn.empty ()) { stOut.push (stIn.top ()); stIn.pop (); } } int result = stOut.top (); stOut.pop (); return result; } int peek () { int res = this ->pop (); stOut.push (res); return res; } bool empty () { return stIn.empty () && stOut.empty (); } };
定要懂得复用,功能相近的函数要抽象出来,不要大量的复制粘贴,很容易出问题!
用队列实现栈
225.
用队列实现栈 - 力扣(LeetCode)
一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外)
重新添加到队列尾部,此时在去弹出元素就是栈的顺序了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class MyStack {public : queue<int > que; MyStack () { } void push (int x) { que.push (x); } int pop () { int size = que.size (); size--; while (size--) { que.push (que.front ()); que.pop (); } int result = que.front (); que.pop (); return result; } int top () { return que.back (); } bool empty () { return que.empty (); } };
有效的括号
20.
有效的括号 - 力扣(LeetCode)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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 () || st.top () != s[i]) return false ; else st.pop (); } return st.empty (); } };
删除字符串中的所有相邻重复项
1047.
删除字符串中的所有相邻重复项 - 力扣(LeetCode)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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; } };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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; } };
递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中 ,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。
而且在企业项目开发中,尽量不要使用递归 !在项目比较大的时候,由于参数多,全局变量等等,使用递归很容易判断不充分return的条件,非常容易无限递归(或者递归层级过深),造成栈溢出错误(这种问题还不好排查!)
逆波兰表达式求值
150.
逆波兰表达式求值 - 力扣(LeetCode)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution {public : 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 (num2 + num1); if (tokens[i] == "-" ) st.push (num2 - num1); if (tokens[i] == "*" ) st.push (num2 * num1); if (tokens[i] == "/" ) st.push (num2 / num1); } else { st.push (stoi (tokens[i])); } } int result = st.top (); st.pop (); return result; } };
滑动窗口最大值
239.
滑动窗口最大值 - 力扣(LeetCode)
思路
此时我们需要一个队列,这个队列呢,放进去窗口里的元素,然后随着窗口的移动,队列也一进一出,每次移动之后,队列告诉我们里面的最大值是什么。
这个队列应该长这个样子:
1 2 3 4 5 6 7 8 9 10 class MyQueue {public : void pop (int value) { } void push (int value) { } int front () { return que.front (); } };
每次窗口移动的时候,调用que.pop(滑动窗口中移除元素的数值),que.push(滑动窗口添加元素的数值),然后que.front()就返回我们要的最大值。
其实队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队里里的元素数值是由大到小的。
那么这个维护元素单调递减的队列就叫做单调队列,即单调递减或单调递增的队列。C++中没有直接支持单调队列,需要我们自己来一个单调队列
不要以为实现的单调队列就是
对窗口里面的数进行排序,如果排序的话,那和优先级队列又有什么区别了呢。
对于窗口里的元素{2, 3, 5, 1 ,4},单调队列里只维护{5, 4}
就够了,保持单调队列里单调递减,此时队列出口元素就是窗口里最大元素。
此时大家应该怀疑单调队列里维护着{5, 4} 怎么配合窗口经行滑动呢?
设计单调队列的时候,pop,和push操作要保持如下规则:
pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作
push(value):如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止
保持如上规则,每次窗口移动的时候,只要问que.front()就可以返回当前窗口的最大值。
nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class MyQueue { public : deque<int > que; void pop (int value) { if (!que.empty () && value == que.front ()) { que.pop_front (); } } void push (int value) { while (!que.empty () && value > que.back ()) { que.pop_back (); } que.push_back (value); } int front () { return que.front (); } };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 class Solution {private : class MyQueue { public : deque<int > que; void pop (int value) { if (!que.empty () && value == que.front ()) { que.pop_front (); } } 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]); } result.push_back (que.front ()); 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个高频元素
347.
前 K 个高频元素 - 力扣(LeetCode)
堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。
如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。
有的同学一想,题目要求前 K 个高频元素,那么果断用大顶堆啊。
那么问题来了,定义一个大小为k的大顶堆,在每次移动更新大顶堆的时候,每次弹出都把最大的元素弹出去了,那么怎么保留下来前K个高频元素呢。
而且使用大顶堆就要把所有元素都进行排序,那能不能只排序k个元素呢?
所以我们要用小顶堆,因为要统计最大前k个元素,只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 class Solution {public : class mycomparison { 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 = 0 ; i < nums.size (); i++) { map[nums[i]]++; } priority_queue<pair<int , int >, vector<pair<int , int >>, mycomparison> pri_que; for (unordered_map<int , int >::iterator it = map.begin (); it != map.end (); it++) { pri_que.push (*it); if (pri_que.size () > k) { pri_que.pop (); } } vector<int > result (k) ; for (int i = k - 1 ; i >= 0 ; i--) { result[i] = pri_que.top ().first; pri_que.pop (); } return result; } };