明显的双指针
做了那么多双指针,其一般的写法都是具有一个从前向后和一个从后向前的指针
因为这样比较容易达成遍历O(n)的时间复杂度,而如果指针同时向一个方向移动
就容易造成时间复杂度为O(n^2)或者O(2n)的情况
做题思路
:暴力算法—找单调性—优化代码
显然这题的单调性是前指针 i 每前进一步,总和就会扩大,而后指针 j应该后退或者至少不动来使总和减小
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
for(int i=0,j=nums.size()-1;i<j;i++){
while(nums[j]+nums[i]>target)j--; //后指针减少到总和<=target的位置
if(nums[j]+nums[i]==target)return {i+1,j+1};
}
return {-1,-1};
}
};
经典归并排序
只不过这题要求合并到第一个数组里,如果不开一个新的数组,那要重新改变一下写法,考虑到第一个数组已经开辟了足够的空间,我们从大往小归并即可
//从大到小的归并
class Solution {
public:
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
int i=m-1,j=n-1,k=nums1.size()-1; //从大到小归并,位置从大到小填空
while(i>=0&&j>=0){
if(nums1[i]<=nums2[j])nums1[k--]=nums2[j--];
else nums1[k--]=nums1[i--];
}
while(j>=0)nums1[k--]=nums2[j--]; //扫尾,由于nums1[i]已经按照顺序存在于nums1内部,所以不需要扫尾
}
};
//正常的从小到大的归并,需要开辟额外的空间
class Solution {
public:
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
vector<int> res;
int i=0,j=0;
while(i<m&&j<n){
while(i<m&&nums1[i]<=nums2[j])res.push_back(nums1[i++]);
while(j<n&&nums2[j]<nums1[i])res.push_back(nums2[j++]);
}
while(i<m)res.push_back(nums1[i++]);
while(j<n)res.push_back(nums2[j++]);
nums1=res;
}
};
实现一下c语言里面的unique函数
也是使用的双指针算法
前指针i,后指针j
倘若j遇到与当前i不相等的数,将其值赋给nums[++i]即可
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
if(!nums.size())return 0;
int i=0;
for(int j=1;j<nums.size();j++)
if(nums[j]!=nums[i])nums[++i]=nums[j];
return i+1;
}
};
难度暴增,思路很好理解,写法很有技巧性,反正我想不到
思路:
首先维护一个前后指针i,j,并用哈希表对t串的所有字符个数作统计,并累计字符种类,然后维护前后指针所指的区间( i , j ),每当有符合字符入内时,哈希表累计减一,那么前指针就可以向前移动(至少不动)每当有符合字符出去时,哈希表累计加一,只要总数符合字符个数以及种类,就可以更新符合条件的区间
class Solution {
public:
string minWindow(string s, string t) {
unordered_map<char,int> hash;
string res;
for(auto item:t)hash[item]++; //记录t里每个元素的数量
int cnt=hash.size(); //记录t元素种类
for(int i=0,j=0,sum=0;j<s.size();j++){
hash[s[j]]--; //遇到一个元素让其在hash表中的值-1,无用的数会被减到小于0,当然如果有用的数出现超过需求它的次数,也会被减到小于0
if(hash[s[j]]==0)sum++; //如果有值为0的元素出现,说明区间出现了所有这类数,
while(hash[s[i]]<0)hash[s[i++]]++; //尝试改变满足条件的区间,如果前指针指向的数的hash小于0,代表它是没有用的数或者是满足条件但区间内已经有足够多该数,那么把区间缩短,并且使该数在哈希中的值加一(回溯)
if(cnt==sum){ //如果已经满足了所有种类的数,可以更新当前区间
if(!res.size()||j-i+1<res.size())res=s.substr(i,j-i+1);
}
}
return res;
}
};
搞了半天什么是合法的括号序列
合法括号序列的重要性质
假设 ’ ( ’ 值是1 ’ ) ’ 值是-1
所有前缀和始终大于等于0,并且总和等于0时是一个合法的括号序列
如果小于0的时候要截断一次,因为小于0说明出现了)并且没有(与它匹配,相当于把当前合法的括号序列截断,需要重新统计
大于0继续做,直到找到前缀和等于0时更新答案
(((()))如果有左括号数量大于右括号的时候,找不到前缀和等于0的情况,此时我们从右到左遍历一遍,再更新答案即可
class Solution {
public:
//函数实现,便于正反做一遍
int helper(string s){
int res=0;
for(int i=0,start=0,cnt=0;i<s.size();i++){
if(s[i]=='(')cnt++;
else{
cnt--; //遇到')',cnt--
if(cnt<0)start=i+1,cnt=0; //如果cnt小于0,说明遇到多余的')'截断了当前连续括号序列
else if(cnt==0) res=max(res,i-start+1); //当前是有效括号序列,更新答案
}
}
return res;
}
int longestValidParentheses(string s) {
int res=helper(s);
reverse(s.begin(),s.end()); //翻转字符串
for(auto &c:s)c^=1; //左右括号ascll码值只差1,用异或1的方式将左括号变成右括号,右括号变成左括号,实现对称翻转
return max(res,helper(s)); //返回正反做一遍后的最大值
}
};
实现一个栈,这个栈多了一种功能可以返回栈中的最小元素
方法是直接开两个栈,其中一个栈正常存储,第二个栈存储最小元素(如第一个数存前一个数的最小值,第二个数存前两个数的最小值,第三个数存前三个数的最小值…)
class MinStack {
public:
stack<int> stk;
stack<int> stk_min;
/** initialize your data structure here. */
MinStack() {
//初始化不写
}
void push(int x) {
stk.push(x);
if(stk_min.size())stk_min.push(min(stk_min.top(),x)); //如果不为空,存放栈顶和入栈元素的较小值
else stk_min.push(x); //为空直接存放
}
void pop() {
stk.pop();
stk_min.pop();
}
int top() {
return stk.top();
}
int getMin() {
return stk_min.top();
}
};
用一个res存储水滴的总量
整个过程我们维护一个从大到小的单调栈,对于每一个进栈的柱,如上图的i为例:
一旦他入栈,那么增加的水滴数量因当取决于他左边最高的柱子,我们将小于等于他的柱子全部出栈,
并且根据当前出栈柱子的高度以及上一个出栈柱子的高度last计算出每一层增加的水滴数量,
(无论有没有柱子出栈)最后都要根据左边最高的柱子计算出最顶层水滴的数量,加起来就是一个柱子进栈所带来的水滴总量,循环遍历做一遍即可
class Solution {
public:
int trap(vector<int>& height) {
stack<int> stk; //递减的单调栈
int n=height.size();
int res=0;
for(int i=0;i<n;i++){
int last=0;
while(stk.size()&&height[stk.top()]<=height[i]){ //分层计算水滴数
int t=stk.top();
stk.pop();
res+=(i-t-1)*(height[t]-last);
last=height[t];
}
if(stk.size())res+=(i-stk.top()-1)*(height[i]-last);
stk.push(i);
}
return res;
}
};
算法二
class Solution {
public:
int trap(vector<int>& height) {
int n=height.size();
if(!n)return 0;
vector<int > left(n),right(n);
int q[n];
int tt=-1,hh=0; //因为要找每个柱子左右两边最高的矩形,所以用队列存储下标
//找左边最高矩形
for(int i=0;i<n;i++){
while(hh<=tt&&height[q[tt]]<=height[i])tt--;
if(hh<=tt)left[i]=height[q[hh]];
else left[i]=0;
q[++tt]=i;
}
tt=-1,hh=0;
//找右边最高矩形
for(int i=n-1;i>=0;i--){
while(hh<=tt&&height[q[tt]]<=height[i])tt--;
if(hh<=tt)right[i]=height[q[hh]];
else right[i]=0;
q[++tt]=i;
}
//对于每个矩形,如果它上面存储了水滴(>0),则统计入内
int res=0;
for(int i=0;i<n;i++){
int t=min(left[i],right[i])-height[i];
if(t>0)res+=t;
}
return res;
}
};
即对于全部的矩形,找到它的左右边界(距离它最近的最小值),计算出面积,更新最大值
找左右边界即用单调栈左右各做一次即可
class Solution {
public:
int largestRectangleArea(vector<int>& h) {
stack<int> stk;
int n=h.size();
vector<int> left(n),right(n); //存储每个矩形的左右边界
//找左边界
for(int i=0;i<n;i++){
while(stk.size()&&h[stk.top()]>=h[i])stk.pop(); //单调栈操作
if(stk.empty())left[i]=-1; //如果左边界是坐标轴,默认是-1
else left[i]=stk.top(); //否则是最近的小于它的值
stk.push(i);
}
while(stk.size())stk.pop(); //清空
//倒序找右边界
for(int i=n-1;i>=0;i--){
while(stk.size()&&h[stk.top()]>=h[i])stk.pop();
if(stk.empty())right[i]=n; //最右边界默认是n
else right[i]=stk.top();
stk.push(i);
}
int res=0;
for(int i=0;i<n;i++)res=max(res,h[i]*(right[i]-left[i]-1)); //计算底边长为right-left-1
return res;
}
};
经典滑动窗口
通过维护一个队列来确保队头是当前窗口的最大值
时间复杂度降低到O(n)
平常使用数组来模拟的队列,由于队头和队尾都涉及到了插入删除操作,我们用deque来写这题
//数组模拟队列
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
int n=nums.size();
vector<int> res;
if(!n)return {};
int q[n];
int tt=-1,hh=0;
for(int i=0;i<n;i++){
while(q[hh]<i-k+1)hh++; //如果队头在窗口头之外应当删除
while(hh<=tt&&nums[q[tt]]<=nums[i])tt--; //队列有元素并且入队元素较大,删除前面的元素(使其单调递减)
q[++tt]=i;
if(i-k+1>=0)res.push_back(nums[q[hh]]); //如果窗口大于等于三,输出当前最大元素
return res;
}
};
//使用双端队列
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
int n=nums.size();
deque<int> q;
vector<int> res;
for(int i=0;i<n;i++){
while(q.front()<i-k+1&&q.size())q.pop_front();
while(q.size()&&nums[q.back()]<=nums[i])q.pop_back();
q.push_back(i);
if(i-k+1>=0)res.push_back(nums[q.front()]);
}
return res;
}
};
- 对于环状问题,我们将它展开为一个2n长度的链,这样做的好处是,可以从2n长度的链中找到所有的长度为1~n的子集的情况
- 我们利用前缀和的性质解决这个问题
- 首先求出这个数组的前缀和,对于前缀和数组,使之构成一个长度为n的滑动窗口,对于每一个入栈的前缀和si,都可以在窗口中找到最小值sj,使得si-sj的值为最大值,最后更新答案即可
- 注意边界情况s0=0
class Solution {
public:
int maxSubarraySumCircular(vector<int>& A) {
int n=A.size();
for(int i=0;i<n;i++)A.push_back(A[i]); //拓展为2n长的链
deque<int> q;
vector<int> sum(2*n+1); //前缀和通常从下标1开始(省去边界条件),所以多开一个空间
for(int i=1;i<=2*n;i++)sum[i]=sum[i-1]+A[i-1]; //对应A的下标要错开一位
int res=INT_MIN;
q.push_back(0); //枚举从sum[1]开始,提前将下标0加入deque,省去边界条件判断
for(int i=1;i<=2*n;i++){
while(q.size()&&q.front()<i-n)q.pop_front();
res=max(res,sum[i]-sum[q.front()]); //pop前先更新res的值,避免队列空而无法更新答案
while(q.size()&&sum[q.back()]>=sum[i])q.pop_back();
q.push_back(i);
}
return res;
}
};