【算法深练】双序列双指针:用“双轨并行”思维,高效破解算法难题
目录
双指针
2109. 向字符串添加空格
2540. 最小公共值
88. 合并两个有序数组
2570. 合并两个二维数组 - 求和法
LCP 18. 早餐组合
1855. 下标对中的最大距离
1385. 两个数组间的距离值
925. 长按键入
809. 情感丰富的文字
2337. 移动片段得到字符串
777. 在 LR 字符串中交换相邻字符
844. 比较含退格的字符串
986. 区间列表的交集
面试题 16.06. 最小差
1537. 最大得分
判断子序列
392. 判断子序列
524. 通过删除字母匹配到字典里最长单词
2486. 追加字符以获得子序列
2825. 循环增长使字符串子序列等于另一个字符串
1023. 驼峰式匹配
3132. 找出与数组相加的整数 II
521. 最长特殊序列 Ⅰ
522. 最长特殊序列 II
引言
在前面对于单序列双指针的题目进行了深度的剖析,【入门算法】单序列双指针:从暴力枚举到高效优化的思维跃迁-CSDN博客【烧脑算法】单序列双指针:从暴力枚举到高效优化的思维跃迁-CSDN博客
此篇博客借助leetcode具体题目对双序列双指针进行分析;双指针算法通过遍历数组,能够大大降低时间复杂度,提升算法效率。关于单序列双指针和双序列双指针的区别:单序列只有一个数组需要进行遍历,而双序列需要对两个数组进行遍历,而且是同时遍历,以下将借助具体题目进行分析。PS:本篇博客中的所有题目均来自于灵茶山艾府 - 力扣(LeetCode)分享的题单。
双序列双指针
2109. 向字符串添加空格
题解:使用双指针来记录每个单词的起始和结束位置,将每个单词加入到结果中。
class Solution {
public:string addSpaces(string s, vector<int>& spaces) {int n=s.size(),num=spaces.size();string ret;ret.resize(num+n);int j=0,k=0; //使用j来记录从哪个位置开始输出子字符串,用k来记录到第几个空格位置了for(int i=0;i<n;i++){if(k<num&&i==spaces[k]) {ret[j++]=' ';k++;}ret[j++]=s[i];}return ret;}
};
对上面题解进行优化,上述题解中只有一个循环时间复杂度是O(N),但是在进行单词拷贝的时候
2540. 最小公共值
题解:遍历两个数组,使用双指针分别指向两个数组,判断是否相等,如果不相等将小的一方的下标先后移;
class Solution {
public:int getCommon(vector<int>& nums1, vector<int>& nums2) {int i=0,j=0;int n=nums1.size(),m=nums2.size();while(i<n&&j<m){if(nums1[i]>nums2[j]) j++; //j小,让j向后走else if(nums1[i]<nums2[j]) i++; //i小,让i向后走else return nums1[i];}return -1;}
};
88. 合并两个有序数组
题解:根据题意很容易想到双指针,此题需要采用将最大的值放到后面的方式实现插入,如果从前向后进行覆盖就会导致有一些没有进行比较的值已经被覆盖了。
class Solution {
public:void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {int k=m+n-1;int j=n-1,i=m-1;//从后往前进行插入while(i>=0&&j>=0){if(nums1[i]>nums2[j]) nums1[k--]=nums1[i--]; //将大的插到后面else nums1[k--]=nums2[j--];}while(j>=0) //处理nums2没有插入完的情况nums1[k--]=nums2[j--];}
};
2570. 合并两个二维数组 - 求和法
题解:通过双指针同时遍历两个二维数组,如果两个该位置两个数组的编号相同,将其相加后放入到结果中去,如果不同将小的编号放入到结果中。
class Solution {
public:vector<vector<int>> mergeArrays(vector<vector<int>>& nums1, vector<vector<int>>& nums2) {//使用双指针同时遍历nums1和nums2int n=nums1.size(),m=nums2.size();int i=0,j=0;vector<vector<int>> ret;while(i<n&&j<m){if(nums1[i][0]>nums2[j][0])ret.push_back({nums2[j][0],nums2[j++][1]});else if(nums1[i][0]<nums2[j][0])ret.push_back({nums1[i][0],nums1[i++][1]});else ret.push_back({nums1[i][0],nums1[i++][1]+nums2[j++][1]});}while(i<n)ret.push_back({nums1[i][0],nums1[i++][1]});while(j<m)ret.push_back({nums2[j][0],nums2[j++][1]});return ret;}
};
LCP 18. 早餐组合
题解:类似与滑动窗口,依旧是使用双指针分别指向两个数组,可以枚举饮料判断有多少个主食可以进行选择,但是如果直接进行枚举时间复杂度是O(N^2)会出现超时;
所以可以先对两个数组进行排序,从后往前枚举每一样饮品,遍历主食找到第一个不满足条件的主食,此时前面的所有主食都是满足条件的,直接+下标即可;当枚举下一个饮品的时候可以之间从该位置先后找不满足的主食,不需要再重新开始找。
class Solution {#define MOD 1000000007
public:int breakfastNumber(vector<int>& staple, vector<int>& drinks, int x) {//使用双指针分别指向两个数组int n=staple.size(),m=drinks.size();sort(staple.begin(),staple.end());sort(drinks.begin(),drinks.end());//通过枚举每一种饮料来看有多少种主食可以匹配int j=0,ret=0;for(int i=m-1;i>=0;i--){while(j<n&&staple[j]+drinks[i]<=x) j++;ret=(ret+j)%MOD;}return ret;}
};
1855. 下标对中的最大距离
题解:通过枚举nums2,找nums2中第一个比其小的位置,判断是否能更新答案即可。
class Solution {
public:int maxDistance(vector<int>& nums1, vector<int>& nums2) {//使用双指针每一次将小的一方先后遍历int n=nums1.size(),m=nums2.size();int i=0,j=0;int ret=0;for(int j=0;j<m;j++){while(i<n&&nums2[j]<nums1[i]) i++;if(i==n) break; //如果n已经越界了就不需要继续进行更新了if(j>=i) ret=max(ret,j-i);} return ret;}
};
1385. 两个数组间的距离值
题解:枚举arr1看arr2中是否还存在不满足条件的元素,如果单纯使用遍历的方法时间复杂度O(N^2),能否降低时间复杂度???
看到有区间的题目应当c应当往:只遍历一次数组的方向去思考,不满足arr[j]-d<=arr[i]<=arr[j]+d,应当满足arr[j]-d>arr[i]||arr[i]>arr[j]+d,
对数组进行排序如果arr[j]-d>arr[i]是不是j后面的都成立就不用枚举了,如果arr[j]+d<arr[i]此时j后面的元素还是可能存在不满足条件,将j向后走,如果j走到arr[j]-d<=arr[i]<=arr[j]+d区间内,说明该arr[i]不满足条件,让i++往后继续枚举。
class Solution {
public:int findTheDistanceValue(vector<int>& arr1, vector<int>& arr2, int d) {// arr[j]-d<=arr[i]<=arr[j]+d;//arr[j]-d>arr[i]//arr[i]>arr[j]+dsort(arr1.begin(),arr1.end());sort(arr2.begin(),arr2.end());//使用双指针进行实现,枚举arr1中的左右数据,在arr2中找看是否有不满足条件的元素int n=arr1.size(),m=arr2.size();int i=0,j=0,ret=0;while(i<n&&j<m){if(arr2[j]-d>arr1[i]) ret++,i++;else if(arr2[j]+d<arr1[i]) j++;else i++;}ret+=(n-i);return ret;}
};
925. 长按键入
题解:根据题意要判断typed是否是由name存在重复按键导致的;采用双指针遍历两个之字符串进行判断即可。
class Solution {
public:bool isLongPressedName(string name, string typed) {int n=name.size(),m=typed.size();int i=0,j=0;char prev='0';while(i<n&&j<m){while(i<n&&j<m&&name[i]==typed[j]){prev=name[i]; //保留上一次相同的字符i++,j++;}while(j<m&&typed[j]==prev) j++; //将重复按键去除if(i<n&&j<m&&name[i]!=typed[j]) return false; //判断当前位置是否相同}return i==n&&j==m; //判断两个和字符串是否遍历完}
};
809. 情感丰富的文字
题解:与上一题类似,可以采用枚举words中每一个字符串看有多少个满足题意的;与上一题不同的是此题需要对重复的数据进行计数要保证在增加重复字符之后重复字符数量超过3;
class Solution {
public:int expressiveWords(string s, vector<string>& words) {//枚举words中的每一个单词,使用双指针判断是否满足条件int n=s.size(),ret=0,count=0;for(auto str:words){int m=str.size(),i=0,j=0;char prev='0';while(i<n&&j<m){while(i<n&&j<m&&s[i]==str[j]) {if(i>0&&s[i]==s[i-1]) count++; //与上一个字符相同是重复字符else count=1; //重新开始计数prev=s[i];i++,j++;}while(i<n&&s[i]==prev) count++,i++; //处理新增的重复数if(count<3||(i<n&&j<m&&s[i]!=str[j])) break; }//在更新结果的时候对与count==1||count>3的情况必定都是满足的,但是对于count==2的情况要进行特殊判断if(i==n&&j==m&&(count!=2||s[n-count]==str[m-count])) ret++; }return ret;}
};
2337. 移动片段得到字符串
题解:脑筋急转弯 + 双指针;满足移动片段的条件有两个:1)两个字符串中L和R字符的先后顺序是一样的; 2)start可以通过移动得到target;
class Solution {
public:bool canChange(string start, string target) {//如果两个字符串是构成移动片段的说明:start和target的字符向后顺序是一一对应//所以可以先判断其字符的先后顺序string s=start,t=target;s.erase(remove(s.begin(),s.end(),'_'),s.end());t.erase(remove(t.begin(),t.end(),'_'),t.end());if(s!=t) return false; //如果字符串的向后顺序不一样一定不能构成移动片段//字符的向后顺序一致,判断start时候能通过移动得到targetint i=0,j=0;int n=start.size();while(i<n&&j<n){while(start[i]=='_') i++;while(target[j]=='_') j++;if(start[i]=='L'&&i<j) return false; //当遇到L的时候,start的L在target的前面,此时的L就不能移至与target相同位置else if(start[i]=='R'&&i>j) return false; //当遇到R的时候,start的R在target的后面,此时的R就不能移至与target相同位置i++,j++;}return true;}
};
777. 在 LR 字符串中交换相邻字符
题解:此题与上一题一样,可以拿来练练手。
class Solution {
public:bool canTransform(string start, string result) {//先判断两个字符串中R和L的向后顺序时候相同string s=start,r=result;s.erase(remove(s.begin(),s.end(),'X'),s.end());r.erase(remove(r.begin(),r.end(),'X'),r.end());if(s!=r) return false;//判断能否通过移动得到resultint i=0,j=0;int n=start.size();while(i<n&&j<n){while(start[i]=='X') i++;while(result[j]=='X') j++;//当该位置是L且i<j的时候不能通过移动得到result//同理当该位置是R且i>j的时候也不能通过移动得到resultif(start[i]=='L'&&i<j) return false;else if(start[i]=='R'&&i>j) return false;i++,j++;} return true;}
};
844. 比较含退格的字符串
题解:使用栈进行模拟实现。当遇到#的时候出栈,否则入栈。注意:当栈为空的时候不能再进行出栈了。
class Solution {
public:bool backspaceCompare(string s, string t) {//用栈来实现stack<char> st;stack<char> tt;for(auto e:s) {if(e!='#') st.push(e);else if(!st.empty()) st.pop();}for(auto e:t) {if(e!='#') tt.push(e);else if(!tt.empty()) tt.pop();}while(!st.empty()&&!tt.empty()){if(st.top()!=tt.top()) return false;st.pop(),tt.pop();}return st.empty()&&tt.empty();}
};
优化:对于上面方法使用了O(m+n)的空间,能否进行优化使用常数的空间???此时可以考虑使用双指针来实现,因为是从后往前进行删除的,所以遍历的时候就不能从前往后进行,使用从后往前进行,通过i和j来标记移动到那个位置,通过sdel和ndel来标记前面还有多少字符需要被删除。通过内循环找到不能被删除的位置进行比较即可。
class Solution {
public:bool backspaceCompare(string s, string t) {//使用双指针进行实现int n=s.size(),m=t.size();int i=n-1,j=m-1;int sdel=0,ndel=0;while(i>=0||j>=0) //使用或来进行判断{while(i>=0) {if(s[i]=='#') sdel++; //当是#时将sdel++,表示有一个字符要被删除else if(sdel>0) sdel--;else break; //当当前字符不是#并且没有要删除的字符时就要将该位置字符进行比较i--;}while(j>=0){if(t[j]=='#') ndel++;else if(ndel>0) ndel--;else break;j--;}if(i>=0&&j>=0&&s[i]!=t[j]) return false;if((i<0&&j>=0)||(i>=0&&j<0)) return false; //进行比较时仅有一方没有元素可以进行比较时直接返回i--,j--;}return true;}
};
986. 区间列表的交集
题解:求交集,对题目进行分析对于两个区间:[a1,a2]和[b1,b2],当a2>=b1&&a1<=b2时有交集,找出交集即可;当b1>a2||a1>b2时没有交集,对于a1>b2,将b2增大来靠近a1,对于b1>a2,将a2增大来靠近b1;根据以上方法进行双指针来遍历两个数组。
class Solution {
public:vector<vector<int>> intervalIntersection(vector<vector<int>>& firstList, vector<vector<int>>& secondList) {//[a1,a2]和[b1,b2]a2<=b1||a1<=b2时有交集,找出交集,并将a2和b2中小的向前移//当b1>a2||a1>b2时没有交集//对于a1>b2,将b2增大来实现交集//对于b1>a2,将a2增大来找交集int i=0,j=0;int n=firstList.size(),m=secondList.size();vector<vector<int>> ret;while(i<n&&j<m){if(firstList[i][0]>secondList[j][1]) j++;else if(firstList[i][1]<secondList[j][0]) i++;else {int begin=max(firstList[i][0],secondList[j][0]);int end=min(firstList[i][1],secondList[j][1]);ret.push_back({begin,end});if(firstList[i][1]>secondList[j][1]) j++;else i++;}}return ret;}
};
面试题 16.06. 最小差
题解:经典的找最小差问题,先将数组进行排序,遍历两个数组让数组中的两个数逐渐靠近来实现找最小差。
class Solution {
public:int smallestDifference(vector<int>& a, vector<int>& b) {//通过遍历数组中的所有数来在最小差//直接枚举时间复杂度过高//先对数组进行排序,遍历两个数组让其元素逐渐靠近sort(a.begin(),a.end());sort(b.begin(),b.end());long long ret=INT_MAX;int n=a.size(),m=b.size();int i=0,j=0;while(i<n&&j<m){ret=min(ret,abs((long long)a[i]-b[j]));if(a[i]>b[j]) j++;else i++;}return ret;}
};
1537. 最大得分
题解:根据题目要实现每一个区间中和最大,那直接记录两个数组中每个区间中的和即可,将区间较大的和添加到答案;通过遍历两个数组分别记录每个区间到当前位置的和sum1和sum2,当nums[i]==nums[j]的时候将两个和较大的一个添加到答案中即可ret+=max(sum1,sum2);再让sum1和sum2从当前位置开始重新记录新区间的和。对于找相同值使用双指针即可。题目说明可能出现越界要对结果进行%1000000007,对于结果很大的题目可以直接使用long long来存储结果,返回时%MOD即可。
class Solution {#define MOD 1000000007
public:int maxSum(vector<int>& nums1, vector<int>& nums2) {//找两个数组中的相同位置int i=0,j=0;int n=nums1.size(),m=nums2.size();long long sum1=0,sum2=0; //使用long long来解决越界情况long long ret=0;while(i<n&&j<m){ if(nums1[i]>nums2[j]) sum2+=nums2[j++];else if(nums1[i]<nums2[j]) sum1+=nums1[i++];else //相等{ret=ret+max(sum1,sum2);sum1=nums1[i++],sum2=nums2[j++];}}while(i<n) sum1+=nums1[i++];while(j<m) sum2+=nums2[j++];ret+=max(sum1,sum2);return ret%MOD;}
};
判断子序列
392. 判断子序列
题解:经典的判断字符串的子序列,使用双指针遍历两个字符串即可。
class Solution {
public:bool isSubsequence(string s, string t) {//使用双指针遍历两个字符串int n=s.size(),m=t.size();int i=0,j=0;while(i<n&&j<m){if(s[i]==t[j]) i++;j++;}return i==n;}
};
524. 通过删除字母匹配到字典里最长单词
题解:上一题的拓展,方法一样。枚举dictionary中的每一个字符与s进行比较看是否满足条件即可。
class Solution {
public:string findLongestWord(string s, vector<string>& dictionary) {//将数组中的之字符串与s一一对比int n=s.size();string ret="";for(auto str:dictionary){int m=str.size();int i=0,j=0;while(i<n&&j<m){if(s[i]==str[j]) j++;i++;}if(j==m){if(str.size()>ret.size()) ret=str;else if(str.size()==ret.size()&&str<ret) ret=str;}}return ret;}
};
2486. 追加字符以获得子序列
题解:在字符串后面追加字符,追加的是s中没有t部分的子序列,所以找到s中子序列含有t部分的最长长度即可。
class Solution {
public:int appendCharacters(string s, string t) {//找出s中含有的t的最长子序列int n=s.size(),m=t.size();int i=0,j=0;while(i<n&&j<m){if(s[i]==t[j]) j++;i++;}return m-j;}
};
2825. 循环增长使字符串子序列等于另一个字符串
题解:判断str2是否是str1的子序列,只是判断字符相等时可以将该字符+1;
class Solution {
public:bool canMakeSubsequence(string str1, string str2) {//遍历两个字符串找出是否有子序列int n=str1.size(),m=str2.size();int i=0,j=0;while(i<n&&j<m){//判断字符是否相等,当前字符或者当前字符的下一个if(str1[i]==str2[j]||(str1[i]=='z'&&str2[j]=='a')||str1[i]+1==str2[j]) j++;i++;}return j==m;}
};
1023. 驼峰式匹配
题解:枚举数组中的字符串进行一次匹配。细节:当遇到一个是大写字符但是不匹配可以直接判定该字符串不满足条件;在pattern遍历完后,如果queries后还有大写字母也不满足条件。
class Solution {//判断字符串从pos位置后面还有没有大写字符bool HaveUpper(string& str,int pos){while(pos<str.size()){if(isupper(str[pos])) return false;pos++;}return true;}
public:vector<bool> camelMatch(vector<string>& queries, string pattern) {int n=pattern.size();vector<bool> ret;for(auto str:queries){int m=str.size();int i=0,j=0;while(i<n&&j<m){if(pattern[i]==str[j]) i++; //当前字符匹配else if(isupper(str[j])) break; //当前字符是大写但是不匹配j++;}if(i!=n) ret.push_back(false); else ret.push_back(HaveUpper(str,j)); }return ret;}
};
3132. 找出与数组相加的整数 II
题解:根据题意可以删除nums1中的两个数据,再将其他数据+x得到nums1;能删除2个数据那么三个数据中必定有一个是不会被删除的所以可以通过这一性质枚举数组中前三个数来找到对应的x。细节:为了找到对应关系需要将两个数组进行排序,这样每个nums1中i位置+x应该等于nums2中的j位置。总结:当题目出现可以删除N个数据时,那么N+1个数据中必定有一个是满足条件的,可以对这N+1个数据进行枚举得到答案。
class Solution {
public:int minimumAddedInteger(vector<int>& nums1, vector<int>& nums2) {//只有两个数被删除,这前三个数中必定有一个数是满足条件的//可以直接枚举前三个数,找出最小的x//对数组进行排序得到一一对应关系sort(nums1.begin(),nums1.end());sort(nums2.begin(),nums2.end());int ret=INT_MAX;int n=nums1.size(),m=nums2.size();for(int k=0;k<3;k++){int x=nums2[0]-nums1[k];int i=0,j=0,count=0;while(i<n&&j<m){if(nums2[j]-nums1[i]!=x) {i++;count++;if(count>2) break;}else i++,j++;}if(j==m) ret=min(ret,x);}return ret;}
};
521. 最长特殊序列 Ⅰ
题解:脑筋急转弯;当a和b相等毫无疑问直接返回-1;当a和b不相等:1)当a.length()>b.length那么在b中一定不会出现a这整个字符串,那么a就是最长的子序列,2)同理a.length()<b.length;3)a.length()==b.length相等是,a和b都是最长的特殊序列。
class Solution {
public:int findLUSlength(string a, string b) {if(a==b) return -1;else return max(a.size(),b.size());}
};
522. 最长特殊序列 II
题解:此题是上一题的拓展,根据上一题可以得出:要在数组中找一个最长的字符串,该字符串子出现过一次;如果出现了两次,就要找其次最长的字符串,但是还要保证该字符串不是比它长字符串的子序列。
class Solution {//判断ss字符串是否是其他字符串的子序列bool USlenght(vector<string>& strs,unordered_map<string,int>& mm,string& ss){for(auto str:strs){//只有比ss长且出现了两次的字符串才可能出现子序列是ss的情况if(str.size()>ss.size()&&mm[str]>1){int n=str.size(),m=ss.size();int i=0,j=0;while(i<n&&j<m){if(str[i]==ss[j]) j++;i++;}if(j==m) return false;}}return true;}public:int findLUSlength(vector<string>& strs) {int ret=-1;unordered_map<string,int> mm;for(auto s:strs) mm[s]++;for(auto ss:strs) {//找更长的字符串,该字符串只出现了一次,且不是其他字符串的子序列if(ret<(int)ss.size()&&mm[ss]==1&&USlenght(strs,mm,ss))ret=max(ret,(int)ss.size());}return ret;}
};
总结
对于双序列双指针只是在单序列双指针的基础上遍历两个数组,所以处理双序列双指针的关键还是在于指向两个数组的指针何时进行移动。