stack和queue介绍
stack 和queue 是容器适配器,底层容器可为deque,list,vector来实现,默认是deque双端队列,提供O(1)的push_back(),而vector提供均摊的O(1)
stack常用操作:
stack q; //以int型为例
int x;
q.push(x); //将x压入栈顶
q.top(); //返回栈顶的元素
q.pop(); //删除栈顶的元素
q.size(); //返回栈中元素的个数
q.empty(); //检查栈是否为空,若为空返回true,否则返回false
232.用栈实现队列
方法一:使用一个进栈,一个出栈
没做出来,看的随想录的
反思: 两个栈的作用搞清楚,(原来我就没搞清楚)一个是用来入栈,一个是出栈,还有判断empty时是两个栈都没有元素才是整个为空!注意这里只需要在pop,peek时元素正确即可,不必非得有一个完整的像队列一样存储的栈。
知识点:
1.代码复用,这里使用了本类的pop来实现peek this->pop;
2.stack stl中pop没有返回值,应该是s.top()
class MyQueue {
public:
MyQueue() {
}
void push(int x) {
in.push(x);
//本来想着在push的时候使用两个栈,不可行
// while(!s.empty()){
// s.pop();
// }
// while(!t.empty()){
// int a= t.pop();
// s.push(a);
// }
// t.push(x);
}
int pop() {
if(out.empty()){
while(!in.empty()){
int a=in.top();
in.pop();
out.push(a);
}
}
int b=out.top();
out.pop();
return b;
}
int peek() {
int a=this->pop(); //调用自己的pop,代码复用
// in.push(a);
out.push(a); //恢复
return a;//直接返回out.top()不对,因为可能out为空
}
bool empty() {
if(in.size()==0&&out.size()==0)
return true;
return false;
}
private:
stack<int> in;
stack<int> out;
};
/**
* 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();
*/
方法二:将一个栈作为正确顺序的队列,一个是用来临时存储的即反转元素的
官方题解
class MyQueue {
public:
MyQueue() {
}
void push(int x) {
while(!out.empty()){
int a= out.top();
out.pop();
in.push(a);
}
out.push(x);
while(!in.empty()){
int a=in.top();
in.pop();
out.push(a);
}
}
int pop() {
int a=out.top();
out.pop();
return a;
}
int peek() {
return out.top();
}
bool empty() {
if(out.size()==0)
return true;
return false;
}
private:
stack<int> in;
stack<int> out;
};
/**
* 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. 用队列实现栈
反思:
耽误的时间:逻辑要思考清楚,每个数据结构的作用要思考清楚
这个题目就是想清楚两个队列是干啥用的,一个就是用来备份的,一个是用来输出的,这个和上面模拟栈思路不同
class MyStack {
public:
queue<int> in;
queue<int> out;
MyStack() {
}
//in 是中转站,临时保存的,out是用来输出输入的 ;每次pop时,就将out的数据先保存到in里,再删除只剩下一个元素,再返回
void push(int x) {
out.push(x);
}
int pop() {
int size=out.size()-1;
while(size--){
in.push(out.front());
out.pop();
}
int a=out.front();
out.pop();
while(!in.empty()){
out.push(in.front());
in.pop();
}
return a;
}
int top() {
int a=this->pop();
out.push(a);
return a;
}
bool empty() {
if(out.empty())
return true;
return false;
}
};
/**
* Your MyStack object will be instantiated and called as such:
* MyStack* obj = new MyStack();
* obj->push(x);
* int param_2 = obj->pop();
* int param_3 = obj->top();
* bool param_4 = obj->empty();
*/
优化:只使用一个队列实现栈
【随想录】
对于队列新思路:
在出队列的时候,将前面部分的元素挪到队尾
我看了写的:
class MyStack {
public:
queue<int> out;
MyStack() {
}
void push(int x) {
out.push(x);
}
int pop() {
int size=out.size()-1;
while(size--){
out.push(out.front());
out.pop();
}
int a=out.front();
out.pop();
return a;
}
int top() {
int a=this->pop();
out.push(a);
return a;
}
bool empty() {
if(out.empty())
return true;
return false;
}
};
/**
* Your MyStack object will be instantiated and called as such:
* MyStack* obj = new MyStack();
* obj->push(x);
* int param_2 = obj->pop();
* int param_3 = obj->top();
* bool param_4 = obj->empty();
*/
冗余:
- 直接返回empty()
- 直接返回队尾元素用:out.back()
20. 有效的括号
题目
我写的:
class Solution {
public:
bool isValid(string s) {
//不相同出栈,相同进栈
stack<char> st;
for(int i=0;i<s.size();i++){
if(s[i]=='('||s[i]=='{'||s[i]=='['){
st.push(s[i]);
}
else if((!st.empty()&&s[i]==')'&&st.top()=='(')||(!st.empty()&&s[i]=='}'&&st.top()=='{')||(!st.empty()&&s[i]==']'&&st.top()=='[')) //如果这里不是else if 而是if,样例“()”不通过,因为会顺着往下走到这个if的else return false
{
st.pop();
} else{
return false; //
}
}
if(st.empty())
return true;
return false;
}
};
分好情况,根据不同情况,翻译成代码!!
我第一次每次push的是{( [ ,左边这个,然后出栈看是否与s[i]相同,因为想错了,应该是匹配对应的右括号,不是看是否相等。而随想录是直接push对应的右括号,所以每次就直接比较和top是否相等
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(']');
// 第三种情况:遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号 return false
// 第二种情况:遍历字符串匹配的过程中,发现栈里没有我们要匹配的字符。所以return false
else if (st.empty() || st.top() != s[i]) return false;
else st.pop(); // st.top() 与 s[i]相等,栈弹出元素
}
// 第一种情况:此时我们已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false,否则就return true
return st.empty();
}
};
官方:
一些优化:
注意到有效字符串的长度一定为偶数,因此如果字符串的长度为奇数,我们可以直接返回 \text{False}False,省去后续的遍历判断过程。
使用哈希,栈,匹配括号做判断
class Solution {
public:
bool isValid(string s) {
stack<char> st;
unordered_map<char,char> pairs{
{')','('},
{'}','{'},
{']','['},
};
for(int i=0;i<s.size();i++){
if(pairs.count(s[i])==1){ //you
if(st.empty()||st.top()!=pairs[s[i]]) return false;
else st.pop(); //pipei le
}
else st.push(s[i]);
}
return st.empty();
}
};
1047. 删除字符串中的所有相邻重复项
C++ 代码中,由于std::string 类本身就提供了类似「入栈」和「出栈」的接口,因此我们直接将需要被返回的字符串作为栈即可。
随想录和官方,使用string直接作为栈,注意,s.back(), 出栈,入栈
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;
}
};
对比后自己的有冗余:
class Solution {
public:
string removeDuplicates(string s) {
string ss;
for(int i=0;i<s.size();i++){
if(ss.size()==0||s[i]!=ss[ss.size()-1])
ss.push_back(s[i]);
else
ss.pop_back();
}
return ss;
}
};
150. 逆波兰表达式求值
bug点:
- a/b a-b 是有顺序的!出栈后a,b顺序不要弄错,top应该是b
- 写代码时先把逻辑思考清楚,什么时候进栈,什么时候出栈
知识点:
- 逆波兰表达式相当于二叉树后序遍历
switch语句:
switch语句的执行流程如下:
(1) 执行入口:求条件表达式的值,并在常量表达式中找到与之相等的分支作为执行入口;
(2)出口: 顺序执行该分支的语句序列,直到遇到break语句或开关语句的关括号“}”为止;如果没有遇到break,将继续顺序执行后面的case语句。
(3) 当条件表达式的值与所有常量表达式的值均不相等时,若有default分支,则执行其语句序列,否则跳出switch语句,执行后续语句。
注意: - 如果没有遇到break,将继续顺序执行后面的case语句。
- switch(表达式),表达式必须时整型或者枚举型,或者一个class,该class有一个转化函数可以转化成整型或者枚举型
- case ch: ch必须时常量或者字面值常量
switch(表达式){
case a:
。。。。
break;
case b:
。。。。
break;
default:
。。。。
}
官方中switch的使用:
string& token = tokens[i]; //使用了引用,时间复杂度会提高,不用引用也行
switch (token[0]){} //注意这里是token[0],也就是把字符串转成了一个字符,因为token只有一个字符
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> stk;
int n = tokens.size();
for (int i = 0; i < n; i++) {
string& token = tokens[i];
if (isNumber(token)) {
stk.push(atoi(token.c_str()));
} else {
int num2 = stk.top();
stk.pop();
int num1 = stk.top();
stk.pop();
switch (token[0]) {
case '+':
stk.push(num1 + num2);
break;
case '-':
stk.push(num1 - num2);
break;
case '*':
stk.push(num1 * num2);
break;
case '/':
stk.push(num1 / num2);
break;
}
}
}
return stk.top();
}
bool isNumber(string& token) {
return !(token == "+" || token == "-" || token == "*" || token == "/");
}
};
作者:LeetCode-Solution
链接:https://leetcode.cn/problems/evaluate-reverse-polish-notation/solution/ni-bo-lan-biao-da-shi-qiu-zhi-by-leetcod-wue9/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
这里不能是switch(tokens[i]), 也不能是switch(token) //官方那个token, 表达式必须为整型(char ,short ,int ,long int ,long long int ,布尔类型bool),而tokens[i]是string类型不是char类型(是vector 里的),token是string类型,string不是基本的内置类型
栈的应用:
- 递归和栈一定程度上可以相互转化
- 匹配类的问题
- 逆波兰表达式 RPN(Reverse Polish Notation)
- 逆置
string 与int相互转化
- string转成int
atoi(s.cstr())
stoi(s)
浮点型atof(),long型atol() - int 转成string
int num=99;
string s= to_string(num);
char 和int的转化
- char转int
char ch=‘2’;
int a=ch-‘0’; - int 转char
int i;
char ch=i+‘0’;
char和string的相互转化
- char转化成string
方法一:
char c=‘a’;
string s;
s+=c;
方法二:
s.push_back( c ); - string转化成char:
直接就是s[i];
如果是一个字符的字符串,转成char类型就是s[0];
string s=“a”;
char ch=s[0];
char *和string的相互转化
c_str()函数用于string与const char* 之间的转换,c_str()函数返回一个指向正规C字符串的指针常量, 内容与本string串相同
s.c_str()返回的是指向第一个字符的地址。
string s = “1234”;
const char* c = s.c_str();
239. 滑动窗口最大值
题目
做前疑惑:
第一次用的暴力超时了。
看着标签是可能用优先队列,但是自己只知道加入最大值到v,不知道如何移除元素,因为最大值不一定是滑动窗口内的。
方法一:优先队列
介绍1
2
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
priority_queue<pair<int,int>> p;
vector<int> v;
for(int i=0;i<nums.size();i++){
if(i<k){
p.push({nums[i],i});
}else{
v.push_back(p.top().first);
p.push({nums[i],i});
while(p.top().second<=i-k){ //判断最大值下标不在滑动窗口中就一直移除,也就是说队列中的值不一定是k个,即虽然只能看到vector中的个,但hi队列中可以保存之前的只要每次加入v时,是该滑动窗口的值,并且是最大的即可。
p.pop();
}
}
}
v.push_back(p.top().first);//最后一组的
return v;
}
};
官方:单调队列
算法思路:
使用的是单调队列,从队首到队尾,下标是从小到大的,对应的值是从大到小的。
定义单调队列的原则:下标按照从小到大的顺序被存储,并且它们在数组nums 中对应的值是严格单调递减的。
难点:
构造什么样的单调队列(从小到大还是从大到小),出对入队的规则是什么
思路复盘:
首先是要保证求出滑动窗口内的最大值,因此使用优先队列只可以保证是队首是最大的,单调队列也可以保证值的大小顺序,但同时要保证这些大小顺序的下标也是有序的,即按照数组的顺序一样,所以如何进队呢?如果队伍是从大到小的,即队尾是最小的,那么每次进队的时候一定是新的滑动窗口中的值(这里注意不是每次i+=k,而是会有重复的,相当于每次都增加了一个,移出了一个,因此新增加的一定是在滑动窗口内),但是队尾不一定呀,所以你就算移出了也不会造成损失,就将小于新加入元素的全部移出队列,再加入该新的元素,这里其实队内不一定是k个元素,也不一定都是滑动窗口内的元素,我们只需要保证加入到v中的元素是窗口内且是最大的即可,不要被这个k限制住思想。到这一步之后,我们每次新加入一个元素就相当于更新了一次队列,每次循环都从对首将在窗口内的第一个最大值加入到v,
这样移动一次,之后其实队里最后一个元素是新加入的值(即窗口最右边的值),也是次大值,队首就是窗口内的最大值,即此刻队内只有两个元素,最大和次大。
这个可以看一下样例理解,如 1 2 3 4 5 6 ,窗口为4,第一次窗口滑动:1 2 3 4,max是4, 下一次:2 3 4 5,即2 3 4都与前一个窗口相同,那么我们找下一个窗口的最大值只需要比较新加入的元素与被移除掉的元素大小,以及新加入的元素与2 3 4重合元素中的最大者即可,即找出1 2 3 4的最大值和次大值,所以要记录窗口的这两个元素,保存到单调队列里。这里可以看随想录!
然后下一次循环又加入新元素,又和上一个窗口的次大值相比(即队尾元素),如果大于,则刨除掉该值,自己取而代之,如果甚至大于上一个最大值,那么也将最大值刨除掉,自己加入。
为什么先处理的是前k个?
因为前k个是临界,一定在滑动窗口内,而后面就要判断是否在滑动窗口内。判断条件前后都一致。
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
int n = nums.size();
deque<int> q;
for (int i = 0; i < k; ++i) {
while (!q.empty() && nums[i] >= nums[q.back()]) {
q.pop_back();
}
q.push_back(i);
}
vector<int> ans = {nums[q.front()]};
for (int i = k; i < n; ++i) {
while (!q.empty() && nums[i] >= nums[q.back()]) {
q.pop_back();
}
q.push_back(i);
while (q.front() <= i - k) {
q.pop_front();
}
ans.push_back(nums[q.front()]);
}
return ans;
}
};
作者:LeetCode-Solution
链接:https://leetcode.cn/problems/sliding-window-maximum/solution/hua-dong-chuang-kou-zui-da-zhi-by-leetco-ki6m/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
bug点:
- 保存的是下标,如果是数组值得话,那么将不会知道是否在滑动窗口内
- 先处理前k个,后面循环i是从k开始,不是从1开始
- 注意判空!!!!empty()的判断,每一次取元素时都要判断!
- 我的的单调队列规定:队尾存储最大元素,下标从对首到队尾从小到大,每次和队尾的元素比较,大于的才入队,否则不入队。
这种会漏掉,因为每次都只留下了最大的,下一次要是最大的移除了,但是上一个次大的因为没有进队列,所以就会失去。【3 -1 1 2 0 9】 3
并且这个不能处理滑动窗口大小和数组大小一样的情况。样例:【1 2】2
我写的以下:
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
deque<int> d;
vector<int> v;
d.push_back(0);
for(int i=1;i<nums.size();i++){
if(i<k){
if(nums[i]>=nums[d.back()])
d.push_back(i);
}else{
if(i==k)
v.push_back(nums[d.back()]);
if(nums[i]<nums[d.back()]&&d.back()>i-k)
v.push_back(nums[d.back()]);
else
{
d.push_back(i);
v.push_back(nums[d.back()]);
while(d.front()<=i-k)
d.pop_front();
}
}
}
return v;
}
};
随想录
自己定义用双端队列实现了单调队列,不是模板需要根据不同题目条件来定义单调队列进队出队规则。保证队列里单调递减或递增的原则。
随想录
class Solution {
private:
class MyQueue { //单调队列(从大到小)
public:
deque<int> que; // 使用deque来实现单调队列
// 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。
// 同时pop之前判断队列当前是否为空。
void pop(int value) {
if (!que.empty() && value == que.front()) {
que.pop_front();
}
}
// 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。
// 这样就保持了队列里的数值是单调从大到小的了。
void push(int value) {
while (!que.empty() && value > que.back()) {
que.pop_back();
}
que.push_back(value);
}
// 查询当前队列里的最大值 直接返回队列前端也就是front就可以了。
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++) { // 先将前k的元素放进队列
que.push(nums[i]);
}
result.push_back(que.front()); // result 记录前k的元素的最大值
for (int i = k; i < nums.size(); i++) {
que.pop(nums[i - k]); // 滑动窗口移除最前面元素
que.push(nums[i]); // 滑动窗口前加入最后面的元素
result.push_back(que.front()); // 记录对应的最大值
}
return result;
}
};
347前 K 个高频元素
方法一:小根堆
随想录
刚开始还想着用大根堆对map进行排序,但是作者更加高明,节省了空间!用小根堆只保留前k大个元素!
注意使用的是unordered_map统计频率!
看看随想录的代码格式比我的,更加清晰,变量就列在相关使用的地方
随想录:
// 时间复杂度:O(nlogk)
// 空间复杂度:O(n)
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; // map<nums[i],对应出现的次数>
for (int i = 0; i < nums.size(); i++) {
map[nums[i]]++;
}
// 对频率排序
// 定义一个小顶堆,大小为k
priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparison> pri_que;
// 用固定大小为k的小顶堆,扫面所有频率的数值
for (unordered_map<int, int>::iterator it = map.begin(); it != map.end(); it++) {
pri_que.push(*it);
if (pri_que.size() > k) { // 如果堆的大小大于了K,则队列弹出,保证堆的大小一直为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;
}
};
自己写的:
class Solution {
public:
class cmp{
public:
bool operator()(const pair<int,int>&p,const pair<int,int>&q)
{
return p.second>q.second;
}
};
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int,int> mp;
vector<int> v(k);
priority_queue<pair<int,int>,vector<pair<int,int>>,cmp> pri_que;
for(int i=0;i<nums.size();i++)
{
mp[nums[i]]++;
}
unordered_map<int,int>::iterator it;
for(it=mp.begin();it!=mp.end();it++)
{
pri_que.push(*it);
if(pri_que.size()>k)
pri_que.pop();
}
int j;
for(j=k-1;j>=0;j--)
{
v[j]=pri_que.top().first;
pri_que.pop();
}
return v;
}
};
bug点:
- vector<int<v(size,initalnums);
2.注意自己写的比较类应该写到大类的里面 - priority_queue<calss, container, comparison>
comparison函数按照自定义的优先级,这里是map的second大的优先级高,所以comparison比较为return p.second>q.second
默认是大根堆,现在弄成小根堆,每次只保留k个,如果有新加入的就去除最小的堆顶元素,这样节省了空间
pri_que中只有k个位置,最终保留下来的就是前K个最大值;
而用大根堆,每次都要将输出的对顶元素保存到vector中,并且空间大小是n个,全部元素
4.比较函数的写法:
从大到小return p>q ; 从小到大return p<q;
方法二:大根堆 优先队列
我写的
思路历程:
刚开始想用map统计频率,再使用sort对map的second进行排序后,输出map的前k个,方法问题:
sort对second排序不可行
本来还想用另一个map重新对第一个的second频率进行排序当作key,但是频率可能会相等,不能保证key是唯一的,所以不可行
联想到之前的堆数据结构,想到了priority_queue
即:先对数组进行sort排序,再统计频率,然后使用优先级队列,每次将对顶元素加入到vector中。
bug点:
1.数组边界的问题:在最后一个元素上,单独处理,因为如果最后一个元素与倒数第二个元素不一样,那么就不会被统计到,所以要单独处理
2. 个数边界的问题:当只有一个元素时,没有后面元素和它比较,所以会报错!
3. 统计频率使用map更加简单!我写的这种先排序后判断前后元素是否一样会可能会漏掉最后一个元素!
class Solution {
public:
vector<int> topKFrequent(vector<int>& nums, int k) {
if(nums.size()==1)
return {nums[0]};
priority_queue<pair<int,int>> q;
vector<int> v;
sort(nums.begin(),nums.end());
int n=1;
for(int i=0;i<nums.size()-1;i++){
while(i<nums.size()-1&&nums[i]==nums[i+1])
{
n++;
i++;
}
q.push({n,nums[i]});//频率写在前面!!!因为优先级队列是根据第一个进行的排序
n=1;
}
if(nums[nums.size()-1]!=nums[nums.size()-2])
q.push({1,nums[nums.size()-1]});
while(k--&&!q.empty()){
v.push_back(q.top().second);
q.pop();
}
return v;
}
};
map排序报错:
方法三:基于快速排序
官方
215. 数组中的第K个最大元素
题目
自己写的:
方法一:map统计频率
使用map从大到小进行排列并且计算频率,再累加频率与k进行比较
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
// sort(nums.begin(),nums.end());
// priority_queue<pair<int,int>> pri_que;
if(nums.size()==1&&k==1)
return nums[0];
map<int,int,greater<int>> mp;
map<int,int>::iterator it;
int sum=0;
for(int i=0;i<nums.size();i++)
{
mp[nums[i]]++;
}
for(it=mp.begin();it!=mp.end();it++)
{
if(sum<k){
sum+=it->second;
}else{
it--;
return it->first;
}
}
it--;
return it->first;
}
};
方法二:使用优先级队列
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
priority_queue<int> pri_que;
for(int i=0;i<nums.size();i++)
{
pri_que.push(nums[i]);
}
for(int i=0;i<k-1;i++)
{
pri_que.pop();
}
return pri_que.top();
}
};
官方评论区:
方法一:使用小根堆
空间比大根堆更加优化,,每次保存k个,只需要最后输出对顶元素即可
方法二:直接sort
输出第k-1个元素即第k个最大的,注意是great(),这个和自己写的堆排序一样,都是排序,只不过一个是用优先级队列大根堆,一个使用sort排序,sort不是简单的快速排序
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
sort(nums.begin(),nums.end(),greater<int>());
return nums[k-1];
}
};
补充sort排序原理
原理:sort并不是简单的快速排序,它对快速排序进行了优化。此外,它还结合了插入排序和推排序。系统会根据数据形式和数据量自动选择合适的排序方法。它每次排序中不只选择一种方法,比如给一个数据量较大的数组排序,开始采用快速排序,分段递归,分段之后每一段的数据量达到一个较小值后它就不继续往下递归,而是选择插入排序,如果递归的太深,他会选择推排序。
对比上一个输出前k大的元素:
前k个是需要存储k个元素的,而第k个只需要输出第K大的那一个即可
这里就先从大到小排列,再累计频率,与k进行比较,因为可能有重复元素,所以先用map<int ,int greater>进行频率计数
而前k个使用的优先级队列进行的存储
对unordered_map进行排序会报错!
**反思:**用sort直接就可以得到答案,想复杂了不管简单复杂,只要能AC都是好答案,用最短的时间通过就是目标
378. 有序矩阵中第 K 小的元素
题目
我写的:二维数组转一维数组,再排序
还有其他的方法,待看!归并,二分
class Solution {
public:
int kthSmallest(vector<vector<int>>& matrix, int k) {
vector<int> v;
for(int i=0;i<matrix.size();i++)
for(int j=0;j<matrix.size();j++)
v.push_back(matrix[i][j]);
sort(v.begin(),v.end());
return v[k-1];
}
};
71.简化路径
题目
注意:
路径是从前往后的,试想一下你从根目录开始按照文件的路径去找该文件,不是从后往前,所以使用vector来存储,而不是真正的栈!
反思:
耗时很长,主要原因是没有功能模块化,以及多情况讨论。
模块化:先将逻辑步骤写下来,1,2,…再根据不同得步骤,写不同得函数处理。
- 将 / 转化成""空字符串,只保留/之间的
- 将1中的vector根据…出栈
- 累加vector成为最简的path
1.步骤可以模块化成为一个函数,2,3步骤可以在主函数内进行
- 出栈一定要判断是否为empty, 遍历如果while内部有fast->next,一定要在while条件判断是否fast为空!
- 情况多时学会if语句的分支!本题特殊字符有… . / 文件名字符
/ 替换成空字符串,替换后的空字符串也不用管
… 退栈,推到上一个目录
. 不用管
文件名 入栈
多情况时如何if语句分支?
针对不同的情况分别使用if语句:
第一种写法:这种比第二种时间消耗的更少
if(v[i]=="..")
{
if(!stv.empty())
stv.pop_back();
}else if(v[i]==""||v[i]==".")
continue;
else stv.push_back(v[i]);
或者,第二种写法:将两个并到一个if语句分支里:
if(v[i]=="..")
{
if(!stv.empty())
stv.pop_back();
}else{
if(v[i]==""||v[i]==".")
continue;
else stv.push_back(v[i]);
}
看了官方写的:
class Solution {
public:
void split(string s,char c,vector<string>&v){
string str;
s+=c;
for(int i=0;i<s.size();i++)
{
if(s[i]==c)// wu /?
{
str+="";
v.push_back(str);
str="";
}
else{
str+=s[i];
}
}
}
string simplifyPath(string path) {
//stack<string>st;
vector<string> v;
vector<string> stv;
split(path,'/',v);
for(int i=0;i<v.size();i++)
{
if(v[i]=="..")
{
if(!stv.empty())
stv.pop_back();
}else{
if(v[i]==""||v[i]==".")
continue;
else stv.push_back(v[i]);
}
}
string t;
if(stv.empty())
t="/";
else
{
for(int i=0;i<stv.size();i++)
{
t+="/";
t+=stv[i];
}
}
return t;
}
};
bug点:
- 在去/函数内,必须先加一个/才行,因为你是根据/来push_back到vector的
- 多if语句一定不要乱,根据逻辑写即可
- 最后在累加最终的字符串时,只需要/+字符 即可,不需要最后处理最后一个字符 s.strsub(0,s.size())也可以
官方题解:move的使用!!!!lambda表达式
move
class Solution {
public:
string simplifyPath(string path) {
auto split = [](const string& s, char delim) -> vector<string> {
vector<string> ans;
string cur;
for (char ch: s) {
if (ch == delim) {
ans.push_back(move(cur)); //加入cur后,cur变成空字符串
cur.clear();
}
else {
cur += ch;
}
}
ans.push_back(move(cur)); //最后一个字符串,因为如果是aaa/则aaa加入到v里了,并且cur为空了,这里就算加了cur也是空字符串不影响,如果是aaa,那么最后一个在循环里就没加,所以在这里加
return ans;
};
vector<string> names = split(path, '/');
vector<string> stack; //
for (string& name: names) {
if (name == "..") {
if (!stack.empty()) {
stack.pop_back();
}
}
else if (!name.empty() && name != ".") { //"" no care
stack.push_back(move(name));
}
}
string ans;
if (stack.empty()) {
ans = "/";
}
else {
for (string& name: stack) {
ans += "/" + move(name);
}
}
return ans;
}
};
234 回文链表
题目
方法一:双端队列,前面出,后面进,看是否相等
方法二:栈
思路:先将链表中的数据保存在栈内,然后再依次出,与链表进行比较,
如果最后栈为空,则是回文,相当于用栈将链表中的val进行了反转,再比较,
这个思路是由之前使用一个栈实现队列想到的,当时是top出去放到队尾,这里就是反转的思想
用栈:自己写的
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
bool isPalindrome(ListNode* head) {
if(head->next==NULL)
return true;
stack<int> st;
ListNode *p=head;
while(p){
st.push(p->val);
p=p->next;
}
p=head;
while(p&&p->val==st.top())
{
p=p->next;
st.pop();
}
if(st.empty())
return true;
return false;
}
};
官方答案!很多思路!有助于开阔思路
方法三:递归
可以学习到的:使用的是全局变量保存的从前往后的结点
class Solution {
public:
ListNode *frontNode; //使用的是全局变量保存的从前往后的结点
bool Judge(ListNode *curList)
{
if(curList)
{
if(!Judge(curList->next)) //判断当前结点后面是否是回文,如果是if(!Judge(curList))是不对的,因为如果是这样,递归的尽头是空,而空后面还要执行下面的语句,curList->val!=frontnode->val,会出错,将当前结点的处理和结点的next分别处理
return false;
if(curList->val!=frontNode->val) //判断当前结点和前面的结点是否相等
return false;
frontNode=frontNode->next;
// Judge(frontNode); 不需要这一句,因为frontnode仅仅是用来从前往后遍历的,上面的递归是相当于从后往前的,这里就是前面的比较完了,frontnode指向向下一个,不需要递归frontnode,这是逻辑不清楚才写的!
}
return true;
}
bool isPalindrome(ListNode* head) {
frontNode=head;
return Judge(frontNode);
}
};
方法四:反转链表,快慢指针
- 快慢指针找到链表中间节点,若是奇数这里前半部分包括中间节点,若是偶数,就是第一个中间结点
- 反转后半部分的链表(头插法)
- 一次比较前半部分和后半部分
- 恢复链表(可以不恢复,但是不太好)
bug点:
1… 在找中间结点的时候,奇数偶数都返回的是slow指针,fast=fast->next->next,不是slow的后两个;除了快慢指针找中间结点,也可以用计数的方法,遍历链表计算链表结点个数。
2.关于奇偶数:如何判断是奇偶数
本题中是前半部分包括中间节点,反转的是后半部分,所以当后半部分结束时就直接结束,不用比较中间结点了;
而我如果反转的是前半部分,那么就需要判断奇偶,因为第一个结点就是中间节点
3. 反转链表,找中间结点快慢指针要熟练
class Solution {
public:
ListNode* reverseList(ListNode *head)
{
ListNode *cur=head,*temp;
ListNode *pre=NULL;
while(cur)
{
temp=cur->next;
cur->next=pre;
pre=cur;
cur=temp;
}
return pre;
}
ListNode* middleNode(ListNode* head) {
ListNode *fast=head,*slow=head;
while(fast&&fast->next&&fast->next->next)
{
fast=fast->next->next;
slow=slow->next;
}
return slow;
}
bool isPalindrome(ListNode* head) {
if(head==NULL)
return true;
ListNode *firstMid=middleNode(head);
ListNode *secondMid=reverseList(firstMid->next);
// ListNode *newhead=secondMid;
ListNode *p1=head; //这里不是firstmid!是从头结点开始的
ListNode *p2=secondMid;
while(p2!=NULL&&(p1->val==p2->val))
{
p1=p1->next;
p2=p2->next;
}
head=reverseList(secondMid);
if(p2==NULL)
return true;
return false;
}
};
单调栈题目:
单调栈分为:单调递增,单调递减栈。
从栈头到栈低单调递减,即栈头最大,模板:(即下面的温度题),进出栈都是下标。
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
stack<int> st;
int n=temperatures.size();
vector<int> result(n);
for(int i=0;i<temperatures.size();i++)
{
while(!st.empty()&&temperatures[i]>temperatures[st.top()])
{
int temp=st.top();
result[temp]=i-temp;//根据题目的操作
st.pop();
}
st.push(i);
}
return result;
}
};
739. 每日温度
思路:
使用单调栈,
单调栈解决的问题:
通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了。
单调栈存储的是下标顺序。
主要有单调递增栈,单调递减栈。这里是从栈顶到栈底是从小到大的,即top是最小的。如果遇到比top大的就出栈!
时间复杂度,O(N)
空间复杂度:O(N)
解题思路复盘:
刚开始我使用的是map存储每一个值第一个比它大的天数,有两个问题:一是数组值可以有重复,所以每次map计算会覆盖,如果用[] 的话,如果不适用map的话,就不能找到两个温度之间的天数,也就是下标之间的间隔了,所以使用单调栈直接存储下标,利用数组随机存储的特点,取数组元素时,直接nums[i]即可。
然后就是vector的初始化,默认是0,所以没有给vector赋值那么它直接就是0,否则的话还得将最后在栈内得元素赋值为0.
学习得到的经验:
重点的学习经验:当计算与下标有关系时,使用单调栈存储的是下标,不存储数组值
- 单调递增栈和单调递减栈的选择,根据题目进行条件选择。
看了思路自己写的:
// [73,74,75,71,69,72,76,73]
//down-> top big->small two num distance
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
stack<int> st;
int n=temperatures.size();
vector<int> result(n);
for(int i=0;i<temperatures.size();i++)
{
while(!st.empty()&&temperatures[i]>temperatures[st.top()])
{
int temp=st.top();
result[temp]=i-temp;
st.pop();
}
st.push(i);
}
return result;
}
};
bug点:
- 注意每次都是下标,记得加是比较取值
- map只能存储key唯一的!!!
其他方法:暴力法
双重循环会超时,这里使用的是类似哈希的那种。
思路:
根据温度的范围[30,100],存储每一个温度,反向遍历温度数组,将每一个温度的下标存储在next数组中,同时遍历当前温度到100度之间的next(这样肯定比当前元素温度大),如果next数组不是INT_MAX,则说明该温度在温度数组中,要找到比它大的第一个元素,就得找到next数组中最小的那个值,该next的值也就是第一个大于该元素的下标
**注意点:**一个是反向遍历,一个是要找到最小的那个next下标
学习到的新经验:
下标问题还可以使用另一个对应的数组用哈希来解决下标的问题,需要注意的是如果有相同元素则会覆盖,还有必须知道数组的范围才可以用
看了思路后自己写的:
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
int n=temperatures.size();
vector<int> next(101,INT_MAX);
vector<int> v(n);
for(int i=n-1;i>=0;i--)
{
next[temperatures[i]]=i;
int minnum=INT_MAX;
for(int j=temperatures[i]+1;j<101;j++)
{
if(next[j]!=INT_MAX)
{
minnum=min(next[j],minnum);
v[i]=minnum-i;
}
}
}
return v;
}
};
496. 下一个更大元素 I
题目
自己看了随想录思路写的:
学到的经验:if和while的使用,不要被个例误导使用了if
可以使用unordered_map,底层是哈希,效率更高!
复盘思路:
这里唯一性可以使用map,虽然可以存储值但是使用下标提高了效率
还有这里是一遍就遍历了,单调栈真的提高了时间复杂度,注意遍历的是nums2,然后在看该元素是不是存在nums1里,如果存在则加入栈,这样就会把所有是nums1的都会入栈一次,而不是nums1种的数字则会与栈顶元素进行比较,因为是从前往后的,栈又是顶部最小,所以就会直接和栈顶比较,就会找到第一个大于栈顶元素的那个值,之后依次出栈。
class Solution {
public:
vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
stack<int> st;
vector<int> v(nums1.size(),-1);
map<int,int>mp;
for(int i=0;i<nums1.size();i++)
{
mp[nums1[i]]=i;
}
for(int i=0;i<nums2.size();i++)
{
while(!st.empty()&&st.top()<nums2[i])
{
v[mp[st.top()]]=nums2[i];
st.pop();
}
if(mp.count(nums2[i])==1)
{
st.push(nums2[i]);
}
}
return v;
}
};
随想录
class Solution {
public:
vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
stack<int> st;
vector<int> result(nums1.size(), -1);
if (nums1.size() == 0) return result;
unordered_map<int, int> umap; // key:下标元素,value:下标
for (int i = 0; i < nums1.size(); i++) {
umap[nums1[i]] = i;
}
st.push(0);
for (int i = 1; i < nums2.size(); i++) {
while (!st.empty() && nums2[i] > nums2[st.top()]) {
if (umap.count(nums2[st.top()]) > 0) { // 看map里是否存在这个元素
int index = umap[nums2[st.top()]]; // 根据map找到nums2[st.top()] 在 nums1中的下标
result[index] = nums2[i];
}
st.pop();
}
st.push(i);
}
return result;
}
};
暴力法:
class Solution {
public:
vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
stack<int> st;
vector<int> v(nums1.size(),-1);
for(int i=0;i<nums1.size();i++)
{
int j=0;
while(nums1[i]!=nums2[j])
{
j++;
}
for(int k=j+1;k<nums2.size();k++)
{
if(nums2[k]>nums1[i])
{
v[i]=nums2[k];
break;
}
}
}
return v;
}
};
第一次使用了栈的暴力法,这里还是双重循环,当时没有想到是查看在nums1是否存在,只是暴力双重循环,使用map可以减少一层for循环
class Solution {
public:
vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
vector<int> st;
vector<int> v(nums1.size(),-1);
for(int i=0;i<nums1.size();i++)
{
for(int k=nums2.size()-1;k>=0;--k)
{
if(nums2[k]>nums1[i])
{
st.push_back(nums2[k]);
}
if(nums2[k]==nums1[i])
{
if(st.size()!=0)
{
v[i]=st.back();
st.clear();
}
break;
}
}
}
return v;
}
};
503. 下一个更大元素 II
题目
学到的新经验:处理循环
1. 有限循环:取模 ,拉直
2. 无限循环:根据题目条件跳出
思路复盘:
【看了官方答案才知道我用的是“拉直"这种方法处理的循环】
循环实际上只是两次遍历当前数组,所以扩充了一倍当前数组,然后加入result时只需要判断是不是前半部分的数组,即目标数组中还有没有找到答案的数字,如果是才插入,而复制的后半部分虽然经历进栈出栈,但是不插入到result内,这里与之前的找到最高温度那道题目本质区别就是控制了插入result的条件!
【随想录】新的提示:除了在插入时判断一下是否是在前半部分,也可以直接全部都插入,最后resize(nums.size()/2)即可
本题关键是如何实现循环
bug点:
3. 死循环:
for(int i=0;i<nums.size();i++)
nums.push_back(nums[i]);
在这里插入代码片`因为每次的插入都会使nums的size增加,所以i永远小于nums.size().因为该值是变化的,是一直增加的。应当改成不变的i<2*n
2. 在判断是否是前半部分的数组数字时,与后半部分的下标无关,即不用通过后面部分的下标:i<n 只需要在插入时判断栈内元素即可
class Solution {
public:
vector<int> nextGreaterElements(vector<int>& nums) {
stack<int> st;
int n=nums.size();
//vector<int> nums2(2*n);
vector<int> result(n,-1);
for(int i=0;i<2*n;i++)
{
nums.push_back(nums[i]);
}
st.push(0);
for(int i=1;i<nums.size();i++)
{
while(!st.empty()&&nums[i]>nums[st.top()])
{
if(st.top()<n)
result[st.top()]=nums[i];
st.pop();
}
st.push(i);
}
return result;
}
};
官方题解:
当时我自己写没弄懂取模是放在哪里使用,以为是对for循环i判断跳出条件使用,但是其实是在循环内使用,对下标使用。
自己写了一遍
class Solution {
public:
vector<int> nextGreaterElements(vector<int>& nums) {
stack<int> st;
int n=nums.size();
vector<int> result(n,-1);
for(int i=0;i<2*n;i++){
while(!st.empty()&&nums[i%n]>nums[st.top()])
{
result[st.top()]=nums[i%n];
st.pop();
}
st.push(i%n);
}
return result;
}
};
42. 接雨水
题目
方法一:单调栈
随想录:
思路复盘:
自己写的时候是没有考虑到直接可以三个一组,即每次都是将要进入的元素,栈顶元素,栈顶元素的下一个,这三个构成凹槽。而是用了人的思维,没有严格模拟一次遍历情况,使用了temp累加在两个高柱子之间的小的柱子,复杂化了!然后再减去,实际上是被图像所迷惑,使用了单调栈,每次当栈顶元素小于将要进栈的元素的时候就会进行操作,而不是等到两个高的柱子后才计算面积。
注意:
注意情况的判断,出栈要一定要判断是否为空!!!还有st.top()也要判断是否为空,这里因为先进了第一个元素st.push(0),所以就没有判断是否为空
三个为一组的时候,栈顶元素也就是中间的元素高度是要减去的,并且要比较栈顶给下一个元素的高度和将要进栈的元素高度,使用它俩之间较小的那个,所以每次要先保存栈顶元素的值,再弹出,才能获取到栈顶元素的下一个元素的值
class Solution {
public:
int trap(vector<int>& height) {
stack<int> st;
int result=0;
st.push(0);
for(int i=1;i<height.size();i++)
{
if(height[st.top()]==height[i])
{
st.pop();
st.push(i);
}else if(height[i]<height[st.top()])
st.push(i);
else{
while(!st.empty()&&height[i]>height[st.top()])
{
int topindex=st.top();
st.pop();
if(!st.empty())//出栈后一定要判断一下是否为空
{
int h=(min(height[st.top()],height[i]))-height[topindex];
int w=i-st.top()-1;
result+=h*w;
}
}
st.push(i);
}
}
return result;
}
};
精简之后,看着只处理了两种情况,少了相等,实际上不处理相等也可以。本质就是在单调栈的基础上增加了一些判断和操作的代码,就像是树一样,在遍历的时候根据题目条件增加判断语句。
class Solution {
public:
int trap(vector<int>& height) {
stack<int> st;
int result=0;
st.push(0);
for(int i=1;i<height.size();i++)
{
while(!st.empty()&&height[i]>height[st.top()])
{
int topindex=st.top();
st.pop();
//下面就是在单调栈模板的基础上增加的一些操作语句
if(!st.empty())
{
int h=(min(height[st.top()],height[i]))-height[topindex];
int w=i-st.top()-1;
result+=h*w;
}
}
st.push(i);
}
return result;
}
};
方法二:双指针
解法思路:按列进行,找到当前列左边最大的和右边最大的,然后用它俩中较小的那个与当前列高相减,因为这里按一列一列所以宽度是1。
第一种:随想录暴力法找当前列左右两边最大值超时
注意:不是找第一个比它大的,而是左边右边最大的
class Solution {
public:
int trap(vector<int>& height) {
int sum = 0;
for (int i = 0; i < height.size(); i++) {
// 第一个柱子和最后一个柱子不接雨水
if (i == 0 || i == height.size() - 1) continue;
int rHeight = height[i]; // 记录右边柱子的最高高度
int lHeight = height[i]; // 记录左边柱子的最高高度
for (int r = i + 1; r < height.size(); r++) {
if (height[r] > rHeight) rHeight = height[r];
}
for (int l = i - 1; l >= 0; l--) {
if (height[l] > lHeight) lHeight = height[l];
}
int h = min(lHeight, rHeight) - height[i];
if (h > 0) sum += h;
}
return sum;
}
};
第二种:直接根据right,left指针来找最大值中较小的那个即可(官方题解,可以看评论区更好理解)
新思路:1.按列解还是按行!这种二维的图像类,一个是长即与下标有关,一个是高,即数组值。有这两种的思路。不要只看行。 2.在精进代码时,在已知的解法上,先选用一个大的算法思想,可能还加个数据结构。 如这里题目解法也就是题目的规律,说白了就是你每次循环什么?肯定每次循环都是不变的,循环不变量,所以你要找到题目的规律,比如这里每次都是找到一列中最左边和最右边的最大值,再找这两个中较小的那个,这就是循环不变量就是解题的规律思路,然后再找算法的思想,是使用双指针实现该解法!
前提:
同时接两个列的水:因为由“列”的思想方法,求该列左边的最大值和右边的最大值即可
思路:
left,right指针同时向左,向右移动,同时leftmaxl,leftmaxr,rightmaxl,rightmaxr由left,right来更新数值,如何更新?
每次都比较当前的left与leftmaxl,所以leftmaxl始终维护的是当前列(left列)的最大值,同理rightmaxr始终维护当前列(目标列right)右边的最大值。因为目标列left的左边最大值可以随着遍历left向左移动求出没一个left的左边最大值,那么如何找left右边最大值呢?因为还没有遍历右边(如果遍历了就像上面超时的解法一样),这里不需要遍历知道右边,因为只要保证leftmaxl比当前列右边的最大值小即可,因为只需要知道两者之间的较小那个就行。
如何保证leftmaxl比当前列left右边的最大值小呢,只需要和rightmaxr比较,因为如果rightmaxr>leftmaxrl,则意思就是left右边一定存在比leftmaxl大的值,那么两者之间最小的那个一定是leftmaxl了,然后left++;
这里leftmaxr和rightmaxr隐形了,直接用rightmaxr与leftmaxl比较,用leftmaxl与rightmaxr比较,原因如下:【官方评论区】
left列的左右最大值分别为leftmaxl,leftmaxr;right列的左右最大值分别为rightmaxl,rightmaxr,因为right>=left,所以righrmaxl>=leftmaxl,leftmaxl>=rightmaxr(可以想象成为包含关系,因为right的rmax小区间,left的maxr是大区间,所以leftmaxr>=rightmaxr)
若leftmaxl>=righmaxr时,有rightmaxl>rightmaxr,可以得到right的值,因为要求左右最大值中较小的那个,即rightmaxr-height[right]
若rightmaxr>=leftmaxl时,有leftmaxr>leftmaxl,可以得到left的值,即leftmaxl-height[left]
难点:
在于如何根据rightmaxr来判断当前leftmaxl是left的左右两边的最大值中较小的那个,(right同理)改题目的简化思路破接口也是这个!想清楚后就好了
看了官方答案:
class Solution {
public:
int trap(vector<int>& height) {
int result=0;
int left=0;
int right=height.size()-1;
int leftmaxl=0,rightmaxr=0;
while(left<right)
{
//注意这里不能是int leftmaxl=max(leftmaxl,height[left]),因为要循环求最大最小值,前一个的leftmaxl要保存的!不能是临时变量!
leftmaxl=max(height[left],leftmaxl);
rightmaxr=max(height[right],rightmaxr);
if(rightmaxr>leftmaxl)
{
result+=leftmaxl-height[left];
left++;
}else{
result+=rightmaxr-height[right];
right--;
}
}
return result;
}
};
bug点:
注意循环里不能是int leftmaxl=max(leftmaxl,height[left]),因为要循环求最大最小值,前一个的leftmaxl要保存的!不能是临时变量!
84. 柱状图中最大的矩形
没懂,待做…