本文记录我学习双指针的一些笔记。
双指针并不是一个具体的具体的算法,而是一种思路,是相对于两次遍历而言的。通常可以把两重 for 循环优化到一次遍历。
双指针的范围非常的广,通常在链表,数组,字符串中使用比较广泛。
双指针本质上还是一个搜索算法,相对于穷举,很多优化都是剪枝,压缩搜索空间。二分、滑动窗口就是很典型的双指针,它们基本都是通过一些技巧进行剪枝从而达到优化的目的的。
根据指针的移动步长,移动方式,对应的时间复杂度也不同,如果步长是 2,或者像二分那样对撞并且跳着移动,就是 ,常规的移动方式则是 O(n),而空间复杂度是 O(1)。
由于双指针的空间复杂度为 ,因此在一些原地算法中,双指针就比较常见。
心得:
很多时候我们刷题容易陷入这种情况:不知道为什么出了错,一个测试过不了,
就单步或者手动模拟这个测试的执行过程,但是总是有测试出了问题,根据测试数据修改代码,修修改改的,
即使最后 ac l,也感觉自己好像并没有学到些什么,下一次遇到还是不会,总感觉是硬凑出来的。
我就经常遇到这种情况。
根据我的经验:这是因为自己对这个题理解不够深,没有从合适的角度去分析和理解这个题,要知道
刷题是需要动脑子的,上面那种一直单步调试似乎很努力,但是一点作用都没有,因为我们的大脑根本
没有动起来。
这是正常的,大脑本来就会主动去寻找消耗低的路径。
那么要怎么改善呢?
其实很简单,就是样对一个题型养成思维定势,然后固定几个问题,拿到问题就先从这些角度分析,而不是
动手就写代码。
这是我刷滑动窗口的经验,每拿到一个题我就会问滑动窗口的一些要点,比如这题窗口内的状态是什么?
什么时候扩展?什么时候收缩?窗口是固定大小的吗?就这样一步一步的细化题目,之后分析清楚后,
再开始刷题。
而最近刷二分我又陷入那种低效率的方式,反思一下发现自己并没有养成思维定势那样去分析,甚至
有些题动手就刷,有种为了 ac 而 ac 的怠卷期了。这时如果比较累了,建议先休息一下,比较刷题
动脑是非常重的。
根据指针移动方向是否相同
移动的方式:跳跃还是移动,两者相同还是不同,每次一样还是不同等等。
快慢指针:不定长滑动窗口,链表反转,环,倒数第 k 个
对撞指针(左右指针):反转,数组和,有序,二分
固定间隔指针:固定滑动窗口
快慢指针
// 1.快慢指针
l =
r =
while 没有遍历完
if 一定条件
l +=
r +=
return 合适的值对撞指针
//2. 左右端点指针
l = 0
r = n - 1
while l < r
if 找到了
return 找到的值
if 一定条件
l +=
else if 一定条件
r -=
return 没找到固定间隔指针
//3. 固定间距指针
l = 0
r = k
while 没有遍历完
自定义逻辑
l +=
r +=
return 合适的值list
list
题意
判断一个字符串是否是另一个字符串的子序列
分析
子序列就是不连续的子串,但是这题字符要顺序相同,如 abc 就是 sssagsbasc 的子序列。
那我们开两个指针,分别指向两字符串的当前字符,然后遍历判断即可。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
bool isSubsequence(string s, string t) {
int i=0;
int j=0;
for (;j<t.size();j++){
if (s[i]==t[j]){
i++;
}
if (i==s.size()){
break;
}
}
return i==s.size() || j != t.size();
}
};题意
求最后一个字符的长度
分析
开两个指针,从后往前遍历,找到单词边界即可。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
int lengthOfLastWord(string s) {
int l=0;
int r=s.size()-1;
while(r>=0 && s[r]==' '){
r--;
}
l=r;
while(l>=0 && s[l]!=' '){
l--;
}
return r-l;
}
};题意
将连续字符变成字符+出现次数
分析
要将字符变成连续字符,那么需要找到连续字符的次数,故开快慢指针,快指针扫描计算次数,慢指针为字符,统计结束后转换为字符+数字即可。
优化
本题需要注意几个坑:
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
string compressString(string s) {
if (s.empty()){
return s;
}
string res;
int l=0;
int r=0;
while(r<s.size()-1) {
if (s[r]==s[r+1]) {
r++;
continue;
}
res+=s[l];
res+=to_string(r-l+1);
r++;
l=r;
}
res+=s[l];
res+=to_string(r-l+1);
return res.size()<s.size()?res:s;
}
};题意
比较去除退格字符后的字符是否相等
分析
由于退格字符时,前面的字符可能会被删掉,所以不能从前往后看,故开双指针,从后往前扫描。
基本思想也很简单:
从后往前扫描,分别找到第一个非退格字符即可,如果是退格字符,则需要记录一下个数,然后跳过相应个数的普通字符,之后在比较即可。
上面是基本扫描一遍,问题在于,可能只有一个字符被扫描结束了,而另一字符可能没有扫描完,故还要继续判断。由于这个字符串已经扫描完,故如果要对比成功,那么另一个字符串余下的字符必然全部会被退格清掉。
则有下面这些情况:
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
bool backspaceCompare(string s, string t) {
int i=s.size()-1;
int j=t.size()-1;
bool iFast=false;
int cnti=0;
int cntj=0;
while(i>-1&&j>-1){
if (s[i]=='#'){
i--;
cnti++;
continue;
}
if (cnti>0){
i--;
cnti--;
continue;
}
if (t[j]=='#'){
j--;
cntj++;
continue;
}
if (cntj>0){
j--;
cntj--;
continue;
}
if (s[i]!=t[j]){
return false;
}
i--;
j--;
}
// 如果两者都扫描完了,则成功
if (i==-1 && j==-1){
return true;
}
// 第一个字符不是退格字符,前面也没有积累退格字符
if ((j==-1) && (cnti==0) && (s[i]!='#')){
return false;
}
if ((i==-1) && (cntj==0) && (t[j]!='#')){
return false;
}
if (i==-1){
iFast=true;
}
while(i>-1){
if (s[i]=='#'){
cnti++;
}else {
cnti--;
}
i--;
}
while(j>-1){
if (t[j]=='#'){
cntj++;
} else {
cntj--;
}
j--;
}
// 如果最后退格字符大于等于 0,说明普通字符被清空了
if (iFast){
return cntj>=0;
} else {
return cnti>=0;
}
}
};题意
求连续 0 和 1 数量相等的子串
分析
要求 0 和 1 数量相等的子串,那么假设子串是由 n 个 0 和 m 个 1 组成的,即 0001111 或 111110000 等,那么显然,满足条件的子串长度等于 min(n,m).
换一句话说,我们只要知道相邻 0 和 1 个数的较小值,就是最长的子串长度。如是 000111,那么这个字符串中满足条件的子串有 01,0011,000111,即长度的一半,也就是相邻 0 和 1 个数的较小值。
那么我们只需要计算出相邻 0,1 的个数,再计算相邻两者的较小值之和即可。如 00110001 ,个数数组就是 2231,值就为 min(2,2)+min(2,3)+min(3,1).
故我们预先计算出该数组,再遍历一次即可。
优化
根据上面的分析,我们知道其实某一时刻计算时,我们只需要知道相邻两数的个数即可,而和其它个数无关,那么我们可以用快慢指针来维护这两个值。快指针是当前种类的个数,慢指针是前一个种类的个数,然后遍历即可。
效率
时间复杂度:
空间复杂度:优化前 ,优化后
代码
class Solution {
public:
int countBinarySubstrings(string s) {
vector<int> nums;
int cnt;
int res=0;
for (int i=0;i<s.size();i++){
cnt=1;
while (s[i]==s[i+1]) {
cnt++;
i++;
}
nums.push_back(cnt);
}
for (int i=0;i<nums.size()-1;i++) {
res+=(min(nums[i],nums[i+1]));
}
return res;
}
};class Solution {
public:
int countBinarySubstrings(string s) {
int cur=0;
int pre=0;
int cnt=0;
int res=0;
for (int i=0;i<s.size();i++){
cnt=1;
while (s[i]==s[i+1]) {
cnt++;
i++;
}
pre=cur;
cur=cnt;
res+=(min(cur,pre));
}
return res;
}
};题意
合并两个有序数组
分析
由于最后的结果数组在 nums1 中,故可能会涉及到 nums1 中数组的移动,如果从前后往后遍历的话,就会出现移动的情况,因此开双指针从后往前移动,一个开始指向 m-1,一个指向 n-1,再开一个指向 nums1 中当前应该存放的位置,那么每次把双指针中大的一个放到指定位置中即可。
最后,可能 nums2 数组并没有遍历完,因为里面的元素可能比 nums1[0] 的元素还要小,故最后再遍历余下的 nums2 数组,存放到指定位置即可。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
int i=m-1;
int j=n-1;
int k=nums1.size()-1;
while(i>-1 && j>-1){
if (nums1[i]>nums2[j]){
nums1[k]=nums1[i];
i--;
} else {
nums1[k]=nums2[j];
j--;
}
k--;
}
while(j!=-1){
nums1[k]=nums2[j];
k--;
j--;
}
}
};题意
将 nums 中的数全排列,问当前排序的下一个排序是什么
分析
首先,我们要知道什么是字典序全排序:对于数字来说,字典序全排序就是从小到大的数字。
如 1,2,3,4 组成的全排列就是
1234
1243
1324
1342
...我们要找任意一个数的下一个排列,自然需要知道两个排列之间的关系。
先说结论,那就是对于相邻的两排列,我们一定能找到某个分界点,使得小的排列分界点右边是最大值,而新的排列分界点右边是最小值。
如排列 5,1,4,3,2 和 5,2,1,3,4
我们能找到分割点 i,使得数 a 的右边是数字 4,3,2 组成的最大值 432,而 b 的右边是组成的最小值 1,2,3.
知道这个性质之后,这个题就很简单了。其实这个性质也很好理解,就是类似于数字进位,进位前低位必然是最大值,进位后低位自然是最小值。而分割点就是进的位。
首先,显然我们需要找到这个分界点,对于数 a 来说,分界点 i 右边的数显然是单调递减的,所以我们从右边开始遍历,找到第一个非递增的元素即可,即数 1。数 1 要进位的话,自然是进到刚刚比 1 大的数。
故我们要在分割点右边,即 1 的右边三个数 1,3,4 中,找到第一个比分割点 1 大的数。而这就是一个在递减数组中找第一个大于的数,是个典型的二分操作。
找到数后,在这是 2,然后和分割点那个数交换,即进位。
交换后,我们再把分割点右边的数变成组合的最小值,在这就是 134.最简单的 sort 递增排列一下即可。不过注意观察,我们知道原先右边的数是递递减的,交换分割点后,其实仍没有改变递减的顺序。所以要变成最小值,即递增的,之间反转 reverse 下即可。
所以总结一下,算法步骤就是:
需要注意的是,就是原先数组完全递增,是排列的最大值了。就找不到递减的元素,所以要判断一下,直接反转即可。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
void nextPermutation(vector<int>& nums) {
int i=nums.size()-1;
// 从右向左找到第一个递减的元素
while(i-1>-1 && nums[i]<=nums[i-1]){
i--;
}
i--;
int l=i+1;
int r=nums.size()-1;
int mid;
// 原先数组完全递增,直接反转
if (i==-1){
reverse(nums.begin(),nums.end());
return;
}
// 在右边二分该元素,找到第一个大于它的元素
while(l<r) {
mid=(r-l)/2+l;
if (nums[mid]>nums[i]) {
l=mid+1;
} else {
r=mid;
}
}
if (nums[l]<=nums[i]){
l--;
}
// 交换两元素的值
swap(nums[i],nums[l]);
// 反转右部
reverse(nums.begin()+i+1,nums.end());
}
};题意
将 nums 中的数全排列,问当前排序的下一个排序是什么
分析
类似于 31,只是给定的数不是数组,而是一个数的各个位,并且最大全排列不是继续循环,而是返回 -1.同时溢出还要注意一下而已。
参考 31
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
int nextGreaterElement(int n) {
vector<int> nums;
int m;
while (n!=0){
m=n%10;
n=n/10;
nums.insert(nums.begin(),m);
}
int i=nums.size()-1;
while(i-1>-1 && nums[i]<=nums[i-1]){
i--;
}
i--;
int l=i+1;
int r=nums.size()-1;
int mid;
if (i==-1){
return -1;
}
while(l<r) {
mid=(r-l)/2+l;
if (nums[mid]>nums[i]) {
l=mid+1;
} else {
r=mid;
}
}
if (nums[l]<=nums[i]){
l--;
}
swap(nums[i],nums[l]);
reverse(nums.begin()+i+1,nums.end());
long long res=0;
for (int i=0;i<nums.size();i++){
res=res*10+nums[i];
}
if (res<=INT_MAX){
return (int)res;
}
return -1;
}
};list
题意
使数组 i 和 nums[i] 同奇偶
分析
对撞指针,左指针扫奇数,找到不满足的。右指针扫偶数,找到不满足的,交换即可。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
vector<int> sortArrayByParityII(vector<int>& nums) {
int i=0;
int j=nums.size()-1;
while(i<nums.size() && j>-1){
if (nums[i]%2==0){
i+=2;
continue;
}
if (nums[j]%2!=0){
j-=2;
continue;
}
swap(nums[i],nums[j]);
i=i+2;
j=j-2;
}
return nums;
}
};题意
问是否存在一个子数组,使得排列这个数组后,整个数组就是有序的。求子数组的最短长度。
分析
这题有点麻烦,没有思路,主要参考这个题解 。
这个题虽然没给条件,不过测试下来数据就是升序,故按升序来算。
实际上,这个题只有两种情况,即找得到或找不到。当子数组为空,或者已经升序以后,就找不到,或者说不用找。否则一定找得到答案。
如果数组有序的话,那么 nums[i] 左边的元素一定比 nums[i] 小,右边的元素一定比 nums[i] 大。这一点没有问题。
那么反过来,我们怎么判断一个数是否应该被排序呢?很简单,只要满足上面的条件即可。而由于我们要找子数组,那么理论上就能分成三个部分:
b 就是要找的数组,而 a,c 已经有序。我们要找的,应该就是数组 b 的两个端点。
我们说过,只要一个数是拍好序的,那么它左边的元素一定比它小,右边的元素一个比它大。换句话说,只有存在左边元素比它大,或者存在右边元素比它小,这个元素就不满足有序,就应该被排序。
而存在左边元素比 nums[i] 大,等价于 max(left) > nums[i],存在右边元素比它小,等价于 min(right) < nums[i] .
也就是说,对于一个元素 nums[i],只要 max(left) < nums[i] 或者 min(right) > nums[i],那么它就应该被排序。那么要找到未排序的两个边界,只需遍历一遍,然后找最大和最小的两个即可。
但实际上,我们还能优化一下。因为满足 max(left)>nums[i] 中最左边的值,一定小于等于 min(right)<nums[i] 最左边的值。满足 min(right)<nums[i] 中最右边的值,一定大于 max(left)>nums[i] 最右边的值。
故,我们只需找到,满足 max(left)>nums[i] 中最右边的值,就是右边界,和满足 min(left)<nums[i] 中最左边的值,就是左边界。
算法步骤如下:
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
vector<int> subSort(vector<int>& nums) {
if (nums.empty()){
return {-1,-1};
}
int leftMax=nums[0];
int rightMin=nums[nums.size()-1];
int l=-1;
int r=-1;
for (int i=0;i<nums.size();i++){
if (leftMax>nums[i]){
r=i;
} else {
leftMax=nums[i];
}
}
for (int j=nums.size()-1;j>-1;j--){
if (rightMin<nums[j]){
l=j;
} else {
rightMin=nums[j];
}
}
return {l,r};
}
};原地反转通常用于反转字符串,即在 空间复杂度的基础上操作
list
题意
原地反转字符串
分析
我们直到所有的数据结构最终在内存中都只有两个组织方式:
而对于这两种组织方式中,我们反转这些元素,就是反转字符串和反转链表这两题。
而要在原地反转的话,那么对于每一个字符,它至少应该从源地址 → 目标地址,比如第一个字符和最后一个字符,第二个字符和倒数第二个字符,那么,这就是一个典型的双指针使用场景,可以说,这种思路就是一个简单的模拟题,遍历所有字符,然后对于该字符,它应该怎么移动,直接模拟即可。
优化
这题已经空间复杂度为 ,那么能优化的就只有时间复杂度了,而这题前后指针移动时,显然没有什么可优化的,所以只有交换元素可以考虑优化一下。
而交换元素一般有两种方式:
临时变量很好理解,而异或运算其实也很简单:
s[l]^=s[r]; // s[l]=s[l]^s[r]
s[r]^=s[l]; // s[r]=s[r]^s[l]^s[r]=s[l]
s[l]^=s[r]; // s[l]=s[l]^s[r]^s[l]=s[r]
当然,也可以使用库函数。
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
void reverseString(vector<char>& s) {
int l=0;
int r=s.size()-1;
while(l<r) {
swap(s[l],s[r]);
l++;
r--;
}
}
};class Solution {
public:
void reverseString(vector<char>& s) {
int l=0;
int r=s.size()-1;
char t;
while(l<r) {
t=s[l];
s[l]=s[r];
s[r]=t;
l++;
r--;
}
}
};class Solution {
public:
void reverseString(vector<char>& s) {
int l=0;
int r=s.size()-1;
while(l<r) {
s[l]^=s[r];
s[r]^=s[l];
s[l]^=s[r];
l++;
r--;
}
}
};题意
反转非字母字符
分析
类似于 344,只是这题多了非字母字符限制,相对于 344,我们只需跳过非字符字符即可。即使用对撞指针,然后找到字母字符,再交换即可。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
string reverseOnlyLetters(string s) {
int l=0;
int r=s.size()-1;
while(l<r){
while(l<r && !isChar(s[l])){
l++;
}
while(r>=0&&!isChar(s[r])){
r--;
}
if (l>s.size() || r<0 || l>=r){
return s;
}
swap(s[l],s[r]);
l++;
r--;
}
return s;
}
bool isChar(char c) {
return (c>='a' && c<='z')||(c>='A'&&c<='Z');
}
};题意
反转元音字母
分析
类似于 917,只是要反转的字符变了而已,使用对撞指针,然后找到字母字符,再交换即可。甚至直接 copy 917 的改下条件就能跑。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
string reverseVowels(string s) {
int l=0;
int r=s.size()-1;
while(l<r){
while(l<r && !isChar(s[l])){
l++;
}
while(r>=0&&!isChar(s[r])){
r--;
}
if (l>s.size() || r<0 || l>=r){
return s;
}
swap(s[l],s[r]);
l++;
r--;
}
return s;
}
bool isChar(char c) {
return ((c=='a')||(c=='e')||(c=='i')||(c=='o')||(c=='u')||(c=='A')||(c=='E')||(c=='I')||(c=='O')||(c=='U'));
}
};题意
每隔 k 个字符,翻转 k 个字符,最后小于 k 个也反转。
分析
每隔 k 个字符反转 k 个,那么我们可以每次移动 2k 个字符,如果没有范围,那么就反转 k 个即可。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
string reverseStr(string s, int k) {
int l=0;
int r=2*k;
while(r<s.size()) {
reverse(s.begin()+l,s.begin()+l+k);
l+=2*k;
r+=2*k;
}
if (s.size()-l < k) {
reverse(s.begin()+l,s.end());
} else {
reverse(s.begin()+l,s.begin()+l+k);
}
return s;
}
};class Solution {
public:
string reverseStr(string s, int k) {
for (int i=0;i<s.size();i+=2*k){
if (i+k>s.size()){
reverse(s.begin()+i,s.end());
return s;
}
reverse(s.begin()+i,s.begin()+i+k);
}
return s;
}
};题意
反转字符串中的单词
分析
首先反转单词这个参考 344,原地反转,以 344 为基础。
然后这题要反转所有单词,那么自然首先需要找到每个单词的起始位置,然后反转即可,故最简单的就是遍历一遍,然后找到为空字符的索引,显然两个索引就是单词,反转即可。我们可以边扫描边反转,没必要扫两边。
优化
起始我们没有必要引入数组来存索引,因为每次我们要反转的其实只有一个单词,也就是,每一时刻只需直到两个索引即可,而用数组来存的话,那些已经被反转过的索引就没有用了,显然再存起来就很浪费了,所以我们可以用两个前后指针,分别存当前单词的前后索引即可。
至于怎么寻找也很简单,右指针找到空字符,反转了左指针直接跳到右指针处即可。
效率
时间复杂度:优化前后都是
空间复杂算:优化前 ,优化后
代码
class Solution {
public:
string reverseWords(string s) {
vector<int> bank(1,-1);
int j=0;
for (int i=0;i<s.size();i++){
if (s[i]==' '){
bank.push_back(i);
reverse(s.begin()+bank[j]+1,s.begin()+bank[j+1]);
j++;
}
}
reverse(s.begin()+bank[j]+1,s.end());
return s;
}
};class Solution {
public:
string reverseWords(string s) {
int l=0;
int r=0;
while(r<s.size()){
if (s[r]!=' '){
r++;
continue;
}
reverse(s.begin()+l,s.begin()+r);
r++;
l=r;
}
reverse(s.begin()+l,s.end());
return s;
}
};题意
反转单词并去除多余的空格
分析
这题只是在 557 的基础上增加了多余的空格操作,因此,问题就在于,怎么消除多余的空格,消除多余空格之后,就和 557 一样,先反转整个字符,然后找到单词,再部分反转。
问题在于怎么消除空格呢?
如果使用自带的 erase 函数,那么是很容易消除的,只需找到连续两个空格,然后 erase 一个即可。需要注意 erase 一个之后,索引就变短了,因此如果是从左向右遍历,需要自减一次,而如果从右往左遍历,则没有这个限制。
但是需要注意的是,erase 函数的时间复杂度就已经是 的了,因此,总的复杂度就变成 了,这比较慢。
优化
既然 erase 这么慢,而且调用库函数也没有提高编程能力,那么我们就来想办法优化并手动实现一下。
问题在于,我们该怎么做呢?使用 erase 时,我们每找到一次就删除一次,那么后面的元素都要前移,所以整个复杂度才那么高,实际上我们真的有必要每找到一个空格就删除一次吗?
其实没有必要,我们可以把空格全部集中在一起,然后之后在一次性删除即可。这样就减少了元素整体移动的情况,那么我们该怎么做呢?
其实很简单,类似于冒泡排序,我们把空格全部冒到后面即可,之后把后面的所有空格删除就行了。做法也很简单,用对撞指针,分别找到空格和非空格字符即可,而这,就是 27 题的做法。并且也使用到了双指针。
效率
时间复杂度:优化前由于 erase 是 的,故总 ,优化后
空间复杂度:
代码
优化前
class Solution {
public:
string reverseWords(string s) {
// 消除空格,除了第一个和最后一个
for (int i=s.size()-1;i>0;i--){
if (s[i]==' ' && s[i-1]==' '){
s.erase(s.begin()+i);
}
}
// 消除第一个空格
if (s[0]==' '){
s.erase(s.begin());
}
// 防止全为空格
if (s.empty()) {
return "";
}
// 消除最后一个空格
if (s[s.size()-1]==' ') {
s.erase(s.end()-1);
}
// 反转整个字符串
reverse(s.begin(),s.end());
int l=0;
int r=0;
// 反转部分字符串
while(r<s.size()){
if (s[r]!=' '){
r++;
continue;
}
reverse(s.begin()+l,s.begin()+r);
l=r+1;
r++;
}
reverse(s.begin()+l,s.end());
return s;
}
};优化后
class Solution {
public:
string reverseWords(string s) {
remove(s);
if (s.empty()){
return s;
}
reverseW(s);
return s;
}
void remove(string &s){
int l=0;
int r=0;
while(r<s.size() && s[r]==' '){
r++;
}
if (r==s.size()){
s.resize(0);
return;
}
while(r<s.size()){
if (r>0 && s[r]==s[r-1] && s[r]==' '){ // 如果遇到两个空格,则跳过
r++;
continue;
}
s[l]=s[r]; // 如果不是两个空格,则将其前移
l++;
r++;
}
if (s[l-1]==' '){
l--;
}
s.resize(l);
}
void reverseW(string &s){
int l=0;
int r=0;
reverse(s.begin(),s.end());
while(r<s.size()){
if (s[r]!=' '){
r++;
continue;
}
reverse(s.begin()+l,s.begin()+r);
l=r+1;
r++;
}
reverse(s.begin()+l,s.end());
}
};同 151
题意
将前 k 个字符反转到后方
分析
只需全部反转字符串,然后分别反转前 k 个字符和后面所有字符即可。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
string reverseLeftWords(string s, int n) {
reverse(s.begin(),s.begin()+n);
reverse(s.begin()+n,s.end());
reverse(s.begin(),s.end());
return s;
}
};题意
反转第一个 ch 字符前的字符
分析
找到第一个字符的索引,然后反转即可。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
string reversePrefix(string word, char ch) {
int i=0;
while(i<word.size()&& word[i]!=ch){
i++;
}
if (i<word.size()){
reverse(word.begin(),word.begin()+i+1);
}
return word;
}
};题意
分析
提示说得很清楚了,字符串 xy 经过旋转得到 yx,那么 xy+xy=xyxy 就必然包含了 yx。但是包含了 yx 就一定成立吗?必要性呢?这个不知道怎么证明。赞踩回复分享举报编辑删除
优化
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
bool rotateString(string s1, string s2) {
if (s1.size()!=s2.size()){
return false;
}
return (s1+s1).find(s2)!=-1;
}
};同 796
题意
分析
优化
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
bool repeatedSubstringPattern(string s) {
return -1!=(s+s).substr(1,s.size()*2-2).find(s);
}
};主要是检测回文串问题,主要有对撞指针和中心探测两种方法,而两者都是双指针的典型应用。
list
题意
只考虑数字和字母,并且忽略大小写,判断是否是回文串。
分析
最暴力的自然是从回文串的定义着手,即逆转后判断,但是这样空间复杂度太高,可以优化。
优化的思路也很简单,我们不是直接比较字符串,而是比较字符,即找到对称的两个字符,然后比较两者是否相等即可。
而对称的字符也有两种方式,即从串首位开始往中心走,或者中心往两边走。第一种即对撞指针,第二种即中心探测。
对撞指针
对撞指针非常简单,需要注意的点有两个:
跳过字符这个好判断,而忽略大小写可以有多种方式,最简单的是利用大小写字符间 ASCII 码的规律,即 字符 & 0b11011111= 对应的大写字符。
class Solution {
public:
bool isPalindrome(string s) {
int l=0;
int r=s.size()-1;
while(l<=r) {
if (!isChar(s[l])) {
l++;
continue;
}
if (!isChar(s[r])) {
r--;
continue;
}
if ((s[l] & 0b11011111) != (s[r] & 0b11011111)){
return false;
}
l++;
r--;
}
return true;
}
bool isChar(char c){
return (c>='0' && c<='9') || (c>='a' && c<='z') || (c>='A' && c<='Z');
}
};对撞指针的时间复杂度为 ,空间复杂度为 。
中心探测
中心探测是指从字符串中心开始向两侧探测判断,由于字符串分奇偶,故中心也可能有两个。
故我们编写分别从 l,r 向两侧拓展的函数,然后根据奇偶设置中心 l、r。
对了,由于中心探测需要提前知道字符串长度奇偶,而原题又要跳过特殊字符,比较麻烦,故提前预处理了下。去掉特殊字符。
class Solution {
public:
bool isPalindrome(string s) {
s=regex_replace(s,regex("[^0-9a-zA-Z]"),"");
if (s.size()%2==0){
return expandAroundCenter(s,s.size()/2-1,s.size()/2);
} else {
return expandAroundCenter(s,s.size()/2,s.size()/2);
}
}
bool expandAroundCenter(string s,int l,int r){
while (l>-1 && r<s.size()){
if ((s[l] & 0b11011111) != (s[r] & 0b11011111)){
return false;
}
l--;
r++;
}
return true;
}
};中心探测的时间复杂度为 ,空间复杂度为 。
题意
删除一个字符后,判断是不是回文串
分析
先对撞指针,找到不等的两字符后,拆分成两个字符串,再分别判断即可。判断方式有对撞指针和中心探测。
优化
无
效率
时间复杂度:
空间复杂度:
代码
中心探测
class Solution {
public:
bool validPalindrome(string s) {
int l=0;
int r=s.size()-1;
int cnt=0;
while(l<=r) {
if (s[l]==s[r]){
l++;
r--;
continue;
}
return isPalindrome(s.substr(l,r-l))||(isPalindrome(s.substr(l+1,r-l)));
}
return true;
}
bool isPalindrome(string s) {
if (s.size()%2==0){
return expandAroundCenter(s,s.size()/2-1,s.size()/2);
} else {
return expandAroundCenter(s,s.size()/2,s.size()/2);
}
}
bool expandAroundCenter(string s,int l,int r){
while (l>-1 && r<s.size()){
if ((s[l] & 0b11011111) != (s[r] & 0b11011111)){
return false;
}
l--;
r++;
}
return true;
}
};对撞指针
class Solution {
public:
bool validPalindrome(string s) {
int l=0;
int r=s.size()-1;
int cnt=0;
while(l<=r) {
if (s[l]==s[r]){
l++;
r--;
continue;
}
return expandAroundCenter(s,l+1,r) || expandAroundCenter(s,l,r-1);
}
return true;
}
bool expandAroundCenter(string s,int l,int r){
while (l<r){
if (s[l] != s[r]){
return false;
}
l++;
r--;
}
return true;
}
};题意
判断字符串是否是回文串的排列。
分析
回文串要么奇数长度,要么偶数长度。奇数长度时,只有中心个数为 1,其它都是对称的。偶数所有字符都是对称的。所以,回文串中,字符是奇数的个数要么是 1,或者 0.
那最简单的自然是统计奇数字符个数了,直接开 map 即可。
优化
我们实际上根本不需要知道字符的个数,没必要统计个数再判断奇偶。我们只需在遍历的过程中就维护字符奇偶性即可。而奇偶性只有两种状态,故我们可用位图 bitset 来维护。0 表示偶数,1 表示计数。
那么每增加一个字符,它的状态就在 0 和 1 间交替。即 0←→1取反操作。故我们每增加一个字符,将其对应位取反即可。
最后再统计计数个数,即 1 的个数是否小于 2 即可。
效率
时间复杂度:
空间复杂度:
代码
map
class Solution {
public:
bool canPermutePalindrome(string s) {
int cnt=0;
unordered_map<char,int> m;
for (int i=0;i<s.size();i++){
m[s[i]]++;
}
for (auto k:m){
cnt+=(k.second%2);
}
return cnt<=1;
}
};优化
class Solution {
public:
bool canPermutePalindrome(string s) {
bitset<127> b;
for (int i=0;i<s.size();i++){
b.flip(s[i]);
}
return b.count()<2;
}
};题意
在字符串中任取字符,问最长能组成的回文串长度是多少
分析
这题和 面试题 01.04. 回文排列 类似,都是考回文串的性质,即:
回文串要么奇数长度,要么偶数长度。奇数长度时,只有中心个数为 1,其它都是对称的。偶数所有字符都是对称的。所以,回文串中,字符是奇数的个数要么是 1,或者 0.
那么我们要取字符的话,最后要么都取偶数,要么再取一个奇数。
思路有了后,实现就很简单了,由于要知道字符是奇偶数,所以用 map 计算下奇偶(并且由于要用到个数,所以不能像 01.04 那样用位图了)。
然后取所有的偶数,如果有奇数,再从所有奇数里面取余下一个,最后再从奇数里去一个,这就是偶数情况。如果没有奇数,那就取所有偶数就行了。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
int longestPalindrome(string s) {
unordered_map<char,int> m;
int res=0;
int hasOdd=0;
for (int i=0;i<s.size();i++){
m[s[i]]++;
}
for (auto k:m){
if (k.second%2==0){
res+=(k.second);
} else {
res+=(k.second-1);
hasOdd=1;
}
}
return res+hasOdd;
}
};题意
每次删除一个回文子序列,问多少次可以删光
分析
这个题在题意上有点坑,因为是回文子序列而不是子串,也就是说,可以不连续。
那么一共三种情况:
综上,这题就是个判断回文操作而已,使用简单的对撞指针即可。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
int removePalindromeSub(string s) {
if (s.empty()){
return 0;
}
int l=0;
int r=s.size()-1;
while(l<r){
if (s[l]!=s[r]){
return 2;
}
l++;
r--;
}
return 1;
}
};题意
相同索引分割两字符串,问两字符串前后缀分开组成的新字符串是否有回文串
分析
最简单的自然是把所有生成的字符串枚举出来,然后判断是否有回文串,判断的方式用对撞指针或中心探测都行。
不过数量级在 10^5,显然暴力 会超时。
优化
优化的思想也很简单,充分利用到了回文串的回文特性,即:从中心到两边探测,生成的子串也是回文串。反过来一样的,要是回文串,那么中心到两侧的子串需要是回文串才行。
先两字符串两侧往中间判断
假设这时 s1[l]≠s2[r]
而最终要形成回文串,显然只有 xmy 或 xny 这两种情况
现在 x 和 y 已经对称,那么要使得总的字符串成回文,必有中间的子串 n 或 m 是回文。所以这时我们再判断 m 或 n 是否是回文即可。
上面是 s1 取前缀,s2 取后缀,反过来也是一样的。
效率
时间复杂度:暴力 优化
空间复杂度:
代码
暴力
class Solution {
public:
bool checkPalindromeFormation(string s1, string s2) {
// 枚举生成的字符串
for (int i=0;i<s1.size();i++){
if (isPalindrome(s1.substr(0,i)+s2.substr(i,s1.size()-i)) || isPalindrome(s2.substr(0,i)+s1.substr(i,s1.size()-i))){
return true;
}
}
return false;
}
// 中心探测判断回文串
bool isPalindrome(string s) {
if (s.size()%2==0){
return expandAroundCenter(s,s.size()/2-1,s.size()/2);
} else {
return expandAroundCenter(s,s.size()/2,s.size()/2);
}
}
bool expandAroundCenter(string s,int l,int r){
while (l>-1 && r<s.size()){
if (s[l] != s[r]){
return false;
}
l--;
r++;
}
return true;
}
};优化
class Solution {
public:
bool checkPalindromeFormation(string s1, string s2) {
return check(s1,s2) || check(s2,s1);
}
bool check(string s1,string s2){
int l=0;
int r=s1.size()-1;
// 先两侧判断
while(l<r && s1[l]==s2[r]){
l++;
r--;
}
// 再判断中间的子串是否是回文
return isPalindrome(s1,l,r) || isPalindrome(s2,l,r);
}
// 对撞指针法判断回文串
bool isPalindrome(string s,int l,int r){
while(l<r){
if (s[l]!=s[r]){
return false;
}
l++;
r--;
}
return true;
}
};题意
找到字符串的最长回文子串
分析
最暴力的解法自然是枚举每个字符串,然后判断是否是回文子串。不过枚举的复杂度太高,我们没必要枚举字符串,根据判断回文子串的两种方式,即对撞指针和中心探测,我们枚举边界或者中心即可。枚举边界的话,显然复杂度为 ,而枚举中心只要 ,故我们枚举中心。
枚举中心,然后用中心探测法找到最长的回文子串即可。需要注意的是,由于可能有两个中心的情况,故当相邻两字符相等时,则要判断双中心的情况。
优化
我们可以利用回文串的特性来优化,即马拉车算法。todoing
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
string longestPalindrome(string s) {
string res;
string even,odd;
for (int i=0;i<s.size();i++){
even=expandAroundCenter(s,i,i);
if (res.size()<even.size()){
res=even;
}
if (i+1<s.size() && s[i]==s[i+1]){
odd = expandAroundCenter(s,i,i+1);
}
if (res.size()<odd.size()){
res=odd;
}
}
return res;
}
string expandAroundCenter(string s,int l,int r){
while (l-1>-1 && r+1<s.size() && s[l-1]==s[r+1]){
l--;
r++;
}
return s.substr(l,r-l+1);
}
};题意
找到字符串中子串是回文串的数量
分析
这个题就是 5 题+696 题的结合,即枚举中心,然后用中心探测法求最长回文子串,而最长回文子串又包含 length/2 个回文子串,最后求和即可,需要注意一下双中心的情况。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
int countSubstrings(string s) {
int res=0;
int even,odd;
for (int i=0;i<s.size();i++){
even=expandAroundCenter(s,i,i);
res+=even;
if (i+1<s.size() && s[i]==s[i+1]){
odd = expandAroundCenter(s,i,i+1);
res+=odd;
}
}
return res;
}
int expandAroundCenter(string s,int l,int r){
int res=1;
while (l-1>-1 && r+1<s.size() && s[l-1]==s[r+1]){
res++;
l--;
r++;
}
return res;
}
};元素移动即要求移动元素中的元素,并且通常要求原地移动。那么双指针分别指向两个元素然后交换元素,移动的元素和双指针的移动策略相同,是典型的双指针用法。
元素移动题非常典型,对撞指针和快慢指针都很常见。
很多需要数组扩充的题,基本都是后面扩充,然后从后往前遍历。这样避免整体的元素移动,而且交换元素时也比较方便。
list
题意
移除等于 val 的元素
分析
这题其实不应该叫移除 ,因为实际上只需前面的元素没有 val 即可,也不看元素的位置。而且真正意义上的移除也无法原地修改。
实际上,既然这题只需前面没有 val 的话,那我们直接冒泡,把它冒到后面去即可。
所以开两个指针,一个从前扫描找到 val,一个从后扫描找到非 val 的,然后交换即可。
一个小技巧:
当外部一个循环,要在内部查到特定元素时,没必要内部也开一个循环,只需判断条件,不符合就 continue 即可,这样代码要精简许多,而且也不用遗漏条件。
优化
实际上我们没有必要交换元素,因为后面的元素等于什么根本不重要,所以只需把前面的 val 更改为后面非 val 元素即可。
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int l=0;
int r=nums.size()-1;
int res=0;
while(l<=r){
if (nums[l]!=val){
l++;
continue;
}
if (nums[r]==val){
r--;
continue;
}
// swap(nums[l],nums[r]);
nums[l]=nums[r];
res++;
l++;
r--;
}
return l;
}
};题意
将空格替换成 %20
分析
最简单的自然是遍历然后用 replace 库函数了,当然也可以直接实现 replace 函数。
优化
replace 函数时间复杂度 ,效率太低,考虑优化。
由于原题要求原地修改,那么就需要扩充字符串,所以我们统计 0 的个数,然后在后面增加空格。
比如 a_b( _ 是空格,无视真正的空格),就应该扩充为 a_b_ _
然后显然对于非空格元素,需要右移,那么我们开两个指针,从后往前扫描。快指针扫描字符,慢指针用于定位替换的位置,如果是非空格,那么快指针元素后移,如果是空格,则更改为 %20.
如 a_b_ _ ,快指针 i 指向扩充前的元素末尾 b,慢指针 j 指向扩张后元素末尾 _
步骤 1
i j
a_b_ _
步骤 2,由于快指针指向非空格字符,故和慢指针元素元素交换(当然其实没必要交换,只更改慢指针指向的元素即可)
i j
a_b_ b
步骤 3,快慢指针左移
i j
a_b_ b
步骤 4.由于快指针指向空格,故慢指针前 3 元素替换为 %20
i j
a%20b
步骤 5,快指针左移 1 次,慢指针由于替换过了,故左移 3 次
j
i
a%20b
步骤 6.结束效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
string replaceSpace(string s) {
for (int i=0;i<s.size();i++){
if (s[i]==' '){
s.replace(i,1,"%20");
}
}
return s;
}
};class Solution {
public:
string replaceSpace(string s) {
int cnt=0;
for (int i=0;i<s.size();i++){
if (s[i]==' '){
cnt++;
}
}
int l=s.size()-1;
int r=s.size()+2*cnt-1;
s.resize(s.size()+2*cnt);
while(l>=0){
if (s[l]!=' '){
s[r]=s[l];
l--;
r--;
continue;
}
s[r]='0';
s[r-1]='2';
s[r-2]='%';
l--;
r-=3;
}
return s;
}
};题意
将数组中的零复制一遍
分析
这题增加 val 元素,27 题则删除 val 元素。
这题要增加元素,虽然最后结果不管后面的数组,但是为了方便编程,故我们先把整个复写的数组弄出来,然后再裁剪数组。
要扩充数组,需要先统计扩展的长度,故统计原数组中 0 的个数,然后再扩充。要在原数组后扩充,这样可以避免整体元素的后移。
然后由于要增加元素,就涉及元素整体右移,所以有两种移动方式:
如果是从左扫描的话,那么后面的所有元素都要后移,这样总体移动的次数太多,故我们从右向左扫描,可以减少移动次数。
则开快慢指针,快指针从原数组末尾开始,慢指针从新数组末尾开始,快指针扫描,遇到非 0 元素则赋值到慢指针处,遇到 0,则慢指针前两个元素都赋值为 0.
比如 a0b
1. 先扩充为 a0b_
lr
2. 设置快慢指针 a0b_ lr r
3. 快指针指向非 0,则慢指针赋值 a0bb l
4. 快指针指向 0,则慢指针赋值两个 0 a00b总体来说,这题跟 剑指 Offer 05. 替换空格 题差不多,先向后扩展,然后从后往前快慢指针移动元素。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
void duplicateZeros(vector<int>& nums) {
int cnt=0;
for (int i=0;i<nums.size();i++){
if (nums[i]==0){
cnt++;
}
}
int l=nums.size()-1;
int r=l+cnt;
nums.resize(nums.size()+cnt);
while(l>-1){
if (nums[l]!=0){
nums[r]=nums[l];
r--;
l--;
continue;
}
nums[r]=0;
nums[r-1]=0;
r-=2;
l--;
}
nums.resize(nums.size()-cnt);
}
};题意
删除重复元素,使得元素只出现一次
分析
已知数组有序,那么判断重复元素就很简单了,相邻相等即有重复元素。那么,从左到右,遇到有重复元素的必然就要替换掉。
因此,我们开快慢指针,慢指针指向需要被替换的元素,快指针则指向比替换的元素。
由于最后要消除重复元素,那么得到的序列必然严格单调递增,故快指针指向的替换元素必然比慢指针指向的被替换元素大。
那么慢指针依次指针,快指针找到比慢指针前面的元素大于的元素即可。(是比慢指针前一个元素大,不是慢指针指向的元素,比如 2,2,2,3,3,重复元素大于 2 次时,替换到
2,3,2,2,3,3 的时候,慢指针还是指向第二个 2,但是实际上我们要寻找的元素缺应该比前一个元素 3 更大)
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
if (nums.empty()){
return 0;
}
int l=1;
int r=1;
while(r<nums.size()){
if (nums[r]<=nums[l-1]){
r++;
continue;
}
nums[l]=nums[r];
l++;
r++;
}
return l;
}
};题意
删除重复元素,使得元素只出现一次
分析
26 题的进阶版,重复次数变成了 2.
要原地修改,又典型的元素移动题,故开快慢指针。
慢指针指向什么?
nums[slow] 表示 slow 指向的字符该什么处理,而前面的字符全都处理结束了。
快指针指向什么?
和慢指针元素相比较,用来寻找后面需要前移的元素。
快指针什么时候移动?
当 fast=slow,且 nums[fast]==nums[slow-2] 时,说明 slow 前两个元素重复,而快指针要移动,只需继续重复,故 fast++ 后 nums[fast]==nums[slow-2] 仍然满足,则快指针移动。
慢指针什么时候移动?
当 nums[fast]==nums[slow-2] 不满足时,说明重复的字符到底了,所以需要更新字符,故 slow++;
快慢指针初始值应该是什么?
由于要和 slow 前两个元素比较,故初始值为重复次数 2
优化
引申:重复次数变成 n,只需 nums[fast]==nums[slow-n] 比较即可,即快指针和慢指针前面第 n 的个数比较即可。
数组有序,相同元素保留前 k 位,快慢指针都从 k 开始从左向右遍历
每次快指针和慢指针左边第 k 个元素比较,如果相等,则快指针继续右移,否则慢指针直接赋值为快指针的值。
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
if (nums.size()<=2){
return nums.size();
}
int fast=2;
int slow=2;
while(fast<nums.size()){
if (nums[fast]==nums[slow-2]){
fast++;
continue;
}
nums[slow]=nums[fast];
fast++;
slow++;
}
return slow;
}
};题意
将元素中的 0 移动末尾
分析
开两个指针,快指针前移找到非 0 的,那么这时慢指针和快指针间就一定是元素 0,于是两元素交换,再同时前移,一直这么做即可。
重点就是:快慢指针间任意时刻都是元素 0,所以交换元素才不影响相对位置。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int l=0;
int r=0;
while(r<nums.size()){
if (nums[r]==0){
r++;
continue;
}
swap(nums[l],nums[r]);
l++;
r++;
}
}
};题意
判断一个字符串能否经过移动成为零一个字符串
分析
根据题意,可用一个 "LX" 替换一个 "XL",或者用一个 "XR" 替换一个 "RX" 。换句话说,就是 X 可用视为空格,而 L 只能右移,R 只能左移。
那么两字符串中,字符 X 一定个数相等,这是第一点。
并且由于只是和 X 交换,故 L 和 R 的相对位置一定不变,这是第二点。
L 只能右移,R 只能左移,这是第三点。
根据上面三点,我们可以如此规划:
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
bool canTransform(string start, string end) {
int i=0;
int ix=0;
int j=0;
int jx=0;
while(i<start.size() && j<end.size()){
if (start[i]=='X'){
i++;
ix++;
continue;
}
if (end[j]=='X'){
j++;
jx++;
continue;
}
if (start[i]!=end[j]){
return false;
}
if (start[i]=='L' && i<j){
return false;
}
if (start[i]=='R' && i>j){
return false;
}
i++;
j++;
}
while (i<start.size() && start[i]=='X'){
ix++;
i++;
}
while (j<end.size() && end[j]=='X'){
jx++;
j++;
}
return ix==jx;
}
};题意
移动元素使得奇数在左边,偶数在右边
分析
开对撞指针,从左找偶数,从右找奇数,然后交换即可。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
vector<int> exchange(vector<int>& nums) {
int l=0;
int r=nums.size()-1;
while(l<r) {
if (nums[l] %2 != 0){
l++;
continue;
}
if (nums[r] %2 == 0){
r--;
continue;
}
swap(nums[l],nums[r]);
}
return nums;
}
};题意
将偶数移动到前面,奇数移动到后面
分析
开对撞指针,从左找奇数,从右找偶数,然后交换即可。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
vector<int> sortArrayByParity(vector<int>& nums) {
int l=0;
int r=nums.size()-1;
while(l<r){
if (nums[l]%2==0){
l++;
continue;
}
if (nums[r]%2!=0){
r--;
continue;
}
swap(nums[l],nums[r]);
}
return nums;
}
};利用数组的有序性,(或者预排序),然后前后指针
有序:对撞指针或二分
无序:哈希表
如果要去重的话,对撞指针比较简单,哈希较麻烦。而如果不去重,是统计个数的话,哈希表则要简单一些。
list
题意
无序数组中找到和等于 target 的两数索引
分析
由于原数组无序,因此无法二分,更无法双指针,二分即固定一数,二分另一个数,复杂度为 ,而双指针则同样需要有序,使用对撞指针 nums[l]+nums[r] 与 target 比较进行剪枝,复杂度为 。但是这题无序,当然可以先排序,排序后复杂度就是 。
或者也可以暴力双重遍历,
优化
实际上,要寻找 nums[i]+nums[j]=target,固定 i 后寻找 j,是一个典型的存在性问题,我们可以利用哈希表来查询,可以优化到 。这种思想在前缀和题型中使用也非常广泛,前缀和很多时候也是固定 presum[i] 求 presum[j]。
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int,int> m;
vector<int> res;
for (int i=0;i<nums.size();i++){
if (m.count(target-nums[i])>0){
res.push_back(m[target-nums[i]]);
res.push_back(i);
return res;
}
m[nums[i]]=i;
}
return res;
}
};题意
有序数组中找到和等于 target 的两数索引
分析
和 1 题类似,不过这题已经有序了,1 题无序使用哈希表,这个题自然也可以使用哈希表,不过没有使用到有序的特性,故更改思路。
有序自然也能二分,不过复杂度有点高,再换一个思路。
我们可以使用对撞指针来剪枝,复杂度最低。
使用 l,r 分别从两边往中间移动,这时通过它们的和来判断该怎么移动。 即 nums[l]+nums[r] 与 target 的大小。
如果 nums[l]+nums[r]==target,自然找到结果。
如果 nums[l]+nums[r] > target,说明和太大,要减小。但是注意,l 在最左端,只能右移,而 r 在最右端,只能左移。而数组又有序,所以,nums[l] 是单调递增了,而 nums[r] 是单调递减的。所以和太大的话,就只能 r 左移了。
同理,nums[l]+nums[r] < target,说明和太小,要增加,只能 l 右移了。
需要注意的点,就是返回的不是索引,而是索引+1.
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
int l=0;
int r=nums.size()-1;
int sum;
vector<int> res;
while(l<r){
sum=nums[l]+nums[r];
if (sum==target){
res.push_back(l+1);
res.push_back(r+1);
return res;
} else if (sum >target){
r--;
} else if (sum <target){
l++;
}
}
return res;
}
};题意
有序数组中找到和等于 target 的两数索引
分析
同 167
优化
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
int l=0;
int r=nums.size()-1;
vector<int> res;
int sum=0;
while(l<r){
sum=nums[l]+nums[r];
if (sum==target){
res.push_back(nums[l]);
res.push_back(nums[r]);
return res;
} else if (sum <target){
l++;
} else {
r--;
}
}
return res;
}
};题意
有序数组中找到和等于 target 的两数
分析
同 167
优化
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
vector<vector<int>> pairSums(vector<int>& nums, int target) {
int l=0;
int r=nums.size()-1;
vector<vector<int>> res;
int sum=0;
sort(nums.begin(),nums.end());
while(l<r){
sum=nums[l]+nums[r];
if (sum==target){
res.push_back({nums[l],nums[r]});
l++;
r--;
} else if (sum <target){
l++;
} else {
r--;
}
}
return res;
}
};题意
无序数组中找到和等于 target 的两数
分析
同 1,167 题
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
int maxOperations(vector<int>& nums, int k) {
int res=0;
int l=0;
int r=nums.size()-1;
int sum;
sort(nums.begin(),nums.end());
while(l<r){
sum=nums[l]+nums[r];
if (sum==k){
res++;
l++;
r--;
} else if (sum<k){
l++;
} else {
r--;
}
}
return res;
}
};题意
无序数组中寻找三个数,使得和为 target
分析
最简单的暴力自然是三重循环,不过显然会超时。并且还需要去重。
优化
对于求数组和,基本上也就对撞指针,哈希表和二分几种用法。
要求 a+b+c=target,即求 a+b=target-c,所以我们可以先固定 c,然后就可以将其转换为两数和问题,问题就在于,怎么固定这个 c。
当然,我们可以遍历 c,然后求两数和的话,显然方法就很多了,暴力就是三重暴力,无序可以用哈希表,当然也可以双指针,复杂度分别为 .
而哈希和双指针需要有序数组,故我们需要先预排序一下。
效率最高的自然是双指针,故我们可以使用双指针。双指针的具体原理看 167 即可,问题就在于,怎么去重。
由于数组已经有序,并且,我们固定遍历 a,然后双指针求 b+c,所以最后的结果 (a,b,c) 中,每个元素 a 或 b 或 c 都是递增的。
所以如果结果重复的话,那么必然是 a 或 b 或 c 相邻。那么我们要去重的话,只需分别判断 a 或 b 或 c 相邻重复即可,重复就跳过,那么必然结果就不会重复。
而如果使用哈希表的话,原理见第 1 题,只是固定 nums[i],target 变成 0-nums[i] 了而已。nums[i]+nums[j]+m[] = 0 → nums[j]+m[]=0-nums[i].
同样的要注意去重,nums[i] 每次遍历使用的哈希表应该都是新的,而且每找到答案后,后面的重复元素要跳过,当前已经使用的元素都要从哈希表中去除。
效率
时间复杂度:
空间复杂度:
代码
暴力
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> res;
for (int i=0;i<nums.size();i++){
for (int j=i+1;j<nums.size();j++){
for (int k=j+1;k<nums.size();k++){
if (nums[i]+nums[j]+nums[k]==0){
res.push_back(vector<int>{nums[i],nums[j],nums[k]});
}
}
}
}
return res;
}
};哈希表
a+b+c,a 遍历,b 用哈希表暂存,c 遍历
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
int l;
int r;
int target;
vector<vector<int>> res;
unordered_map<int,int> m;
if (nums.size()<3){
return res;
}
sort(nums.begin(),nums.end());
if (nums[nums.size()-1]<0){
return res;
}
for (int i=0;i<nums.size();i++){
if (nums[i]>0){
return res;
}
if ((i>0) && (nums[i-1]==nums[i])){
continue;
}
target=0-nums[i];
l=i+1;
r=nums.size()-1;
m.clear();
for (int j=i+1;j<nums.size();j++){
if (m.count(target-nums[j])>0){
res.push_back({nums[i],nums[j],target-nums[j]});
m.erase(target-nums[j]);
while (j+1<nums.size() && nums[j]==nums[j+1]){
j++;
}
}
m[nums[j]]=j;
}
}
return res;
}
};双指针
a+b+c,a 遍历,而 b和c 用双指针求
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
int l;
int r;
vector<vector<int>> res;
vector<int> v;
if (nums.size()<3){
return res;
}
sort(nums.begin(),nums.end());
if (nums[nums.size()-1]<0){
return res;
}
for (int i=0;i<nums.size();i++){
if (nums[i]>0){
return res;
}
if ((i>0) && (nums[i-1]==nums[i])){
continue;
}
l=i+1;
r=nums.size()-1;
while(l<r){
if (nums[l]+nums[r]+nums[i]<0) {
l++;
} else if (nums[l]+nums[r]+nums[i]>0){
r--;
} else {
res.push_back({nums[i],nums[l],nums[r]});
while (l<r && nums[l]==nums[l+1]){
l++;
}
while (l<r && nums[r]==nums[r-1]){
r--;
}
l++;
r--;
}
}
}
return res;
}
};题意
找到三个数,使之和最接近 target
分析
类似于 15 题,不过这题只需找到一个结果即可,因此也没有 15 去重那种复杂的操作。
最简单的的自然是三重循环,不过显然会超时。
优化
要求 a+b+c =sum,即 b+c=sum-a,故我们可以固定 a,就可以将之转换为两数和问题,而两数和问题则根据有序无序,可以使用哈希表,对撞指针来做。
为了便捷,我们将之排序,然后用对撞指针即可。即根据 b+c 和 sum-a 的大小关系,以及 b,c 的单调性,就可以固定移动策略。然后每次更新一下和即可。当和等于 target 的时候,显然可以直接剪枝返回。
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
int threeSumClosest(vector<int>& nums, int target) {
sort(nums.begin(),nums.end());
int l=0;
int r=0;
int sum=nums[0]+nums[1]+nums[2];
int res=sum;
int t;
for (int i=0;i<nums.size();i++){
l=i+1;
r=nums.size()-1;
while(l<r){
sum=nums[i]+nums[l]+nums[r];
t=target-sum;
if (abs(target-res)>abs(t)) {
res=sum;
}
if (t==0){
return target;
} else if (t > 0){
l++;
} else if (t < 0){
r--;
}
}
}
return res;
}
};题意
找到所有 a+b+c =target,不去重。
分析
和 15 类似,不过 15 要去重,而这题要找到所有的可能。最简单的自然是暴力,不过显然会超时。
优化
参考 15 题,a+b+c=target →b+c=target-a ,固定 a,然后对撞指针求 b 和 c。基本思路和 15,16 题都差不多,不过在找到 a+b+c=target 之后的处理有所不同。15 要去重,所以 b 重复元素,c 重复元素都要剔除。而 16 题则计算具体,本题则是同样要计算 b,c 附近的重复元素,不过不是去重,而是排列组合。
如果 b,c 不等,如 2,2,2,3,3 要统计 (2,3) 的个数,显然要分别统计 2 和 3 的个数,然后相乘。
如果 b,c 相等,如 1,1,1,1,1,1 要统计 (1,1) 的个数,则要统计 1 的个数,然后 .
本题同样有一个剪枝的点,如果 a 大于 target 后,由于排好序了,b 和 c 都比 a 大,那么一定后面都不可能 a+b+c=target,故直接剪枝。
同样本题还有一个剪枝的易错点,即 a+b+c=target,固定 a 后,统计上一次 b c 组合的个数,下一次 a 重复,然后也等于上一次的个数。如 1,1,2,2,3,3,要统计 (1,2,3),第一个 1 满足的个数是 4,然后直接第二个个数也是 4.虽然看上去好像正确,但是像 0,0,0,0,0 这种统计 (0,0,0) 的个数,下一次 0 的统计个数就比上一次少,故不能这样剪枝。
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
int threeSumMulti(vector<int>& nums, int target) {
long res=0;
int l;
int r;
int sum;
int cntl;
int cntr;
bool b;
sort(nums.begin(),nums.end());
for (int i=0;i<nums.size();i++){
if (nums[i]>target){
return res;
}
l=i+1;
r=nums.size()-1;
while(l<r){
sum=nums[i]+nums[l]+nums[r];
if (sum==target){
b=false;
cntl=1;
cntr=1;
if (nums[l]==nums[r]){
b=true;
}
while ((l+1<r) && (l+1<nums.size()) && (nums[l]==nums[l+1])){
l++;
cntl++;
}
while ((l+1<r) && (r+1>-1) && (nums[r]==nums[r-1])){
r--;
cntr++;
}
if (b){
res+=((cntl*(cntl+1))/2);
} else {
res+=(cntl*cntr);
}
l++;
r--;
} else if (sum<target){
l++;
} else if (sum>target){
r--;
}
}
}
return res%1000000007;
}
};题意
求出所有 a+b+c+d=target.并且去重。
分析
类似于 15 三数求和,这题也是一样的,遍历 a 和 b,然后 c d 用对撞指针求,可以把复杂度降低一个维度。
去重也是一样的,遍历 a 时,如果相邻重复,跳过即可,b,c,d 同理。
优化
需要注意的是几个剪枝操作,在三数和时,a+b+c,当我们判断 a > target 时,就可以判定这个数不可能成立。
但是在四数和中,a+b+c+d,我们缺不能直接根据 a>target 或者 a+b>target 就剪枝。
比如 [-5,-4,-3,-2,1,3,3,5] -11 中,当 a=-5 时就 > -11,但是如果直接返回的话,就遗漏了 (-5,-4,-3,1) 这个解,这是因为 a 虽然大于 target,但是 a+b 却不一定大于 target.
每一个数都递增,它们的和却不一定递增,这就是为什么不能这样剪枝。
要剪枝的话,直接判断相邻 4 个数是否大于 target,因为相邻 4 数才是最大值。
还需要注意的是就是防止溢出,可以把 a+b+c+d=target →a+b=target-c-d.
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> res;
sort(nums.begin(),nums.end());
for (int i=0;i<nums.size();i++){
// 剪枝错误,如 [-5,-4,-3,-2,1,3,3,5]
// if (nums[i]>target){
// cout<<nums[i]<<endl;
// return res;
// }
if (i>0 && nums[i]==nums[i-1]){
continue;
}
for (int j=i+1;j<nums.size();j++){
// 剪枝错误,如 [-3,-2,-1,0,0,1,2,3]
// if (nums[i]+nums[j]>target){
// return res;
// }
if (j>i+1 && nums[j]==nums[j-1]){
continue;
}
int l=j+1;
int r=nums.size()-1;
while(l<r){
if (nums[i]+nums[j]==target-nums[r]-nums[l]){
res.push_back({nums[i],nums[j],nums[l],nums[r]});
while(l<r && nums[l]==nums[l+1]){
l++;
}
while(l<r && nums[r]==nums[r-1]){
r--;
}
l++;
r--;
} else if (nums[i]+nums[j]<target-nums[r]-nums[l]){
l++;
} else if (nums[i]+nums[j]>target-nums[r]-nums[l]){
r--;
}
}
}
}
return res;
}
};class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> res;
sort(nums.begin(),nums.end());
for (int i=0;i<nums.size();i++){
if ((i+3<nums.size()) && (nums[i+2]+nums[i+3])>(target-nums[i]-nums[i+1])){
return res;
}
if (i>0 && nums[i]==nums[i-1]){
continue;
}
for (int j=i+1;j<nums.size();j++){
if ((j+2<nums.size()) && (nums[j+1]+nums[j+2]>(target-nums[i]-nums[j]))){
break;
}
if (j>i+1 && nums[j]==nums[j-1]){
continue;
}
int l=j+1;
int r=nums.size()-1;
while(l<r){
if (nums[i]+nums[j]==target-nums[r]-nums[l]){
res.push_back({nums[i],nums[j],nums[l],nums[r]});
while(l<r && nums[l]==nums[l+1]){
l++;
}
while(l<r && nums[r]==nums[r-1]){
r--;
}
l++;
r--;
} else if (nums[i]+nums[j]<target-nums[r]-nums[l]){
l++;
} else if (nums[i]+nums[j]>target-nums[r]-nums[l]){
r--;
}
}
}
}
return res;
}
};题意
求四个数组中,a+b+c+d=target 的个数
分析
无序数组,又要求和的个数,显然要用哈希表了,类似于 1 题,哈希表预处理一半,然后余下的一半遍历即可。而 a+b+c+d 四个数,预处理几个数放进哈希表呢?显然两个最好,这样复杂度只有 ,放 1 个或 3 个都是 .
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
unordered_map<int,int> m;
int res=0;
int i,j;
for (i=0;i<nums1.size();i++){
for (j=0;j<nums2.size();j++){
m[nums1[i]+nums2[j]]++;
}
}
for (i=0;i<nums3.size();i++){
for (j=0;j<nums4.size();j++){
res+=m[-nums3[i]-nums4[j]];
}
}
return res;
}
};题意
在两数组中寻找最接近的两个数
分析
最简单的自然是暴力 了,不过数量级太大会超时,故得换个思路。
优化
要求最接近的数,又无序,显然只能暴力,要优化只能排序了。
自然的想法,可以对 nums1 排序,然后遍历 nums2,再对 nums1 二分找点。并且要找两个点。
当然也可以用有序的数据结构,如红黑树来维持有序性,原理和二分差不多。这两种方式复杂度显然应该是排序 加上二分 .
实际上可以两个数组都排序,然后快慢指针来做,可以优化到线性的复杂度。
假设 i 指向数组 a,j 指向数组 b,那么就只有两种移动策略,要么 i++,要么 b++。
如
i
a1,a2,a3,a4
j
b1,b2,b3,b4当前具体为 abs(a1-b1),下一个距离有 abs(a1-b2),abs(a2-b1) 两种可能,那么选择小的可能去移动指针。谁的下一个更小,就哪个指针移动。
需要注意的是这个题有溢出问题,由于要计算距离,涉及 a-b 的值,如果下溢的话,说明距离超出了 INT_MAX,由由于结果范围最大就是 INT_MAX,故设为 INT_MAX 即可。
效率
时间复杂度:排序 ,二分 ,快慢指针 .
空间复杂度:
代码
class Solution {
public:
int smallestDifference(vector<int>& nums1, vector<int>& nums2) {
sort(nums1.begin(),nums1.end());
sort(nums2.begin(),nums2.end());
int i=0;
int j=0;
long a1,a2;
int res=2147483647;
while(i<nums1.size() && j<nums2.size()){
int t=nums1[i]-nums2[j];
if (t==INT_MIN){ // 如果下溢,则距离大于 INT_MAX,使之为 INT_MAX 即可。
t=INT_MAX;
} else {
t=abs(t);
}
res=min(res,t);
if (res==0){
return 0;
}
if (j+1<nums2.size()){
a1=abs((long)nums1[i]-nums2[j+1]);
} else {
a1=abs(nums1[i]-nums2[nums2.size()-1]);
}
if (i+1<nums1.size()){
a2=abs(nums1[i+1]-nums2[j]);
} else {
a2=abs(nums1[nums1.size()-1]-nums2[j]);
}
if (a1>a2){
i++;
} else {
j++;
}
}
return res;
}
};接雨水问题也是典型的对撞指针使用常见,题型不多,但是面试频率非常的高。
list
题意
在数组中找到两数 nums[i],nums[j] 使得 min(nums[i],num[j])*(j-i) 最大
分析
最简单的思路,自然是两层暴力枚举,只是复杂度有点高。
优化
由于面积有高和宽两个变量,故为了降维优化,我们将其中一个变量规律化,然后再找另一个变量的最优解。
思路如下:让宽从最大值开始,每次递减,然后寻找高的最大值。
具体实现就是,开对撞指针,然后每次移动一个指针。由于只移动一个指针,故无论谁移动都是宽减少 1,而我们的最终目的是找到面积的最大值,故前后指针移动策略应该是,使得移动后的面积更大。即低的一边移动。
虽然我们使用了双指针,但是移动指针时,是一个贪心思想,即局部求面积最大值。但是贪心也有问题,即怎么证明局部最优等于总体最优呢?即证明证明每次得到的面积最大值,一定是最后的最大值呢?
换句话说,剪枝掉的解,有可能包含最优解吗?
答案是,不可能。因为我们移动的是低的一方向高的一方移动,假设是高的一方移动,面积不可能比低的一行移动大。因为高的一方移动时,高已经被低的一方确定了,无论证明移动,高也不会增加。
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
int maxArea(vector<int>& nums) {
int res=0;
int l=0;
int r=nums.size()-1;
while(l<r){
res=max(res,min(nums[l],nums[r])*(r-l));
if (nums[l]<nums[r]) {
l++;
} else {
r--;
}
}
return res;
}
};题意
什么时候才能接到雨水?显然只有两边的柱子比当前柱子高时,才能接到雨水。而要求的就是中间凹下去那部分的面积。
分析
这个题看上去比较复杂,为了简化问题,我们先来从暴力思考,如果我们要暴力去做,那么该怎么做?
我们可以对每一根柱子进行分析,看这根柱子能接到多少水。什么意思呢,就是指,最后水都接完了之后,看水在这个柱子的高度是多少,如果比柱子要高,那么高出的部分就是接到的雨水。
那么问题来了,什么时候这个水在这根柱子的高度比这跟柱子要高呢?假设最终水高如图所示:
而中间的某根柱子,显然只有两种情况,要么比水高,要么比水低
而我们又要求水比柱子高的情况,显然只有第一种符合条件。即柱子高度比水低时,而水的高度又是谁来决定的?显然是两边界的高度。
而要作为边界,显然它们应该是两边界中最高的,不然
如果两边有更高的柱子,那么水高度显然要变化。
如果我们可以下结论:一根柱子要能接到水的前提是,这根柱子两边的柱子中的最大值都比这个柱子要高。
现在假设柱子能接到水,第二个问题是:这跟柱子能够接到的水高度是多少?
如上图所示,显然应该是水高-柱子高度。而水高又是由两边最高柱子中,比较小的一个决定的。
那么现在我们能下结论了:
对于每一个柱子接的水,那么它能接的水 = min (左右两边最高柱子)- 当前柱子高度
转换成代码的话,对于 nums[i] 接到的水,就是要找到 nums[i] 两边中最大值的较小值,最后再减去 nums[i],即 value=min(leftMax,rightMax)-nums[i].
暴力
上面我们已经知道怎么求每一根柱子的接水量,而问题的重点就是,怎么找到 nums[i] 的 leftMax 和 rightMax.
如果暴力的话,显然就是两边枚举找最值。所以我们枚举柱子,再枚举找两边最值即可。
class Solution {
public:
int trap(vector<int>& nums) {
int leftMax;
int rightMax;
int res=0;
// 枚举柱子 nums[i]
for (int i=0;i<nums.size();i++){
leftMax=0;
rightMax=0;
// 找到 leftMax
for (int j=0;j<=i;j++){
leftMax=max(leftMax,nums[j]);
}
// 找到 rightMax
for (int k=i;k<nums.size();k++){
rightMax=max(rightMax,nums[k]);
}
if ((leftMax>nums[i]) && (rightMax>nums[i])){
res+=(min(leftMax,rightMax)-nums[i]);
}
}
return res;
}
};显然时间复杂度为 ,空间复杂度为 .由于数量级在 10^5 级别,故暴力会超时。
动态规划、预处理
上面我们再找最大值时,是每次重新计算的,实际上,我们进行了一些重复的比较,我们没有必要重新计算,可以提前把两边的最大值先求出来,类似于前缀和数组和后缀和数组,只是不再是和,而是最大值而已。
我们开 leftMax 和 rightMax 数组,leftMax[i] 表示 nums[0]~nums[i] 的最大值,rightMax[i] 表示 nums[i]~nums[length] 的最大值。
最大值数组的求法显然就是个递推的过程,即 leftMax[i]=max(leftMax[i-1],nums[i])。
class Solution {
public:
int trap(vector<int>& nums) {
vector<int> leftMax(nums.size());
vector<int> rightMax(nums.size());
int res=0;
leftMax[0]=nums[0];
rightMax[nums.size()-1]=nums[nums.size()-1];
// 提前计算最大值数组
for (int i=1;i<nums.size()-1;i++){
leftMax[i]=max(leftMax[i-1],nums[i]);
rightMax[nums.size()-1-i]=max(rightMax[nums.size()-1-i+1],nums[nums.size()-1-i]);
}
for (int i=0;i<nums.size();i++){
if (leftMax[i]>nums[i] && rightMax[i]>nums[i]){
res+=(min(leftMax[i],rightMax[i])-nums[i]);
}
}
return res;
}
};显然时间复杂度为 ,空间复杂度也为 .
双指针
在动态规划中,我们提前计算了 leftMax 数组和 rightMax 数组,但是实际上,我们真正对于 nums[i] 用到的只有 leftMax[i] 和 rightMax[i],前面的值已经没用了,这稍微有些浪费,而实际上我们可以只维护某点的 leftMax 和 rightMax 即可。不过不是一个柱子的,而是两个柱子的。
假设两柱子分别为 i,j,且 j>i。那么就有 iLeftMax,iRightMax,jLeftMx,jRightMax 这个变量。
由上图可知,由于 j>i ,故 jLeftMax>=iLeftMax,iRigthMax>=jRightMax. 即子区间的最大值大于 m 后,那么区间的最大值肯定也大于 m。
那么,如果 iLeftMax>jRightMax,则必有 jLeftMax >= jRightMax,所以我们能接 j 点的水。即 jRightMax-nums[j].这时 j 柱子满足条件,故 j 柱子左移。
如果 jRightMax>iLeftMax,则必有 iRightMax >= iLeftMax,所以我们能接 i 点的水。即 iLeftMax-nums[i].这时 i 柱子满足条件,故 i 柱子右移。
而上面我们实际上只用到了 iLeftMax,jRightMax 两个变量,故我们维护这两个即可。根据上图,那么只要我们开对撞指针往中间走,就能覆盖所有的柱子。
class Solution {
public:
int trap(vector<int>& nums) {
int l=0;
int r=nums.size()-1;
int lLeftMax=nums[l];
int rRightMax=nums[r];
int res=0;
while(l<r){
lLeftMax=max(lLeftMax,nums[l]);
rRightMax=max(rRightMax,nums[r]);
if (lLeftMax<rRightMax){
res+=(lLeftMax-nums[l]);
l++;
} else {
res+=(rRightMax-nums[r]);
r--;
}
}
return res;
}
};单调队列
其实要维护两边的最值,是一个典型的滑动窗口最大值问题,维护两个单调递减队列即可,复杂度和 dp 差不多。
单调栈
单调栈和上面几种方法都不一样,主要是计算接水量的方法不一样,单调栈是横着计算的,而上面是竖着计算的。
同 42.
题意
分析
优化
效率
时间复杂度:
空间复杂度:
代码
题意
找到柱状图中,矩形的最大面积。
分析
类似于 42 题,我们也通过遍历每根柱子,然后以找到以这根柱子为高度的面积。显然,我们需要左右拓展,找到宽。
而最简单的思路,自然是遍历柱子的时候,再两边拓展。
优化
实际上,我们在两边寻找柱子的时候,两边柱子只有两种情况,即比当前柱子 nums[i] 高,或者低。而高的话,显然能够形成矩阵,故继续。而如果比要 nums[i] 柱子低,说明矩形的边界就在这跟柱子。所以,换一句话说,我们就是从 nums[i] 向两侧拓展时,要找到 nums[i] 两边,第一个比 nums[i] 的数。而这,是典型的单调栈的使用场景。
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
int largestRectangleArea(vector<int>& nums) {
int leftIndex=0;
int rightIndex=0;
int j,k;
int res=0;
for (int i=0;i<nums.size();i++){
// 找到 nums[i] 矩阵左边界
j=i;
while(j > 1 && nums[j-1] >= nums[i]){
j--;
}
if (j==1 && nums[0]>=nums[i]){
j=0;
}
// 找到 nums[i] 矩阵右边界
k=i;
while(k+1 < nums.size() && nums[k+1] >= nums[i]){
k++;
}
res=max(res,nums[i]*(k-j+1));
}
return res;
}
};双指针总的来说并不复杂,思想也比较简单。但是效率较高,通常能将时间复杂度降一个维度,空间复杂度也为 ,根据移动方向又分成快慢指针和对撞指针,前者有典型应用场景,如滑动窗口,字符移动等,而否则有二分,回文串检测,字符串反转,接雨水等典型应用场景。做双指针的题时,要搞清楚两个指针的指向什么东西,什么时候移动等问题,搞懂则几个问题之后,双指针就迎刃而解了。