1.滑动窗口
1.无重复字符的最长子串(leetcode 3)(剑指offer 48)
- 题目描述:
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
- 分析:
首先,我们需要知道什么叫子串。在刷题的过程中,碰到了两个概念:子串和子序列
- 子串:子串要求连续的,也就是连续的几个字符组成。
- 子序列:只要是字符串中的字符就行,不需要连续。
由子串的概念可以知道,我们需要一直维持一个区间搜索,来判断其是否为子串,然后再对其长度进行比较,获得最大的长度。
- 我们需要维持一个区间搜索子串,区间有一个起始点(start)和一个终止点(end).
- 首先,固定start的位置,移动终止点,每移动一次,就需要判断新增加的元素是否和区间内的元素重复。
- 如果重复,那么我们需要找到重复的元素的下一个元素的位置,作为我们新的开始,这是为了维护子串的连续性。
- 当产生了新的子串,我们需要比较新的子串的长度,找到当前最大子串长度。
- 如果不重复,一直搜索到字符串的结尾。
注意:需要查找新元素是否在区间内存在,可以借助hash表的特性(无重复)。
class Solution {
public:
int lengthOfLongestSubstring(string s) {
if(s.empty())
return 0;
unordered_set<char> word;
int slow=0;
int max_val=INT_MIN;
for(int i=0;i<s.length();i++)
{
//移动左指针,直到没有和当前元素重复的位置
while(word.find(s[i])!=word.end())
word.erase(s[slow++]);
max_val=max(max_val,i-slow+1);
word.insert(s[i]);
}
return max_val;
}
};
2. 最小覆盖子串(leetcode 76)
- 题目描述:【难度困难】
给你一个字符串 S、一个字符串 T,请在字符串 S 里面找出:包含 T 所有字符的最小子串。
示例:
输入: S = "ADOBECODEBANC", T = "ABC"
输出: "BANC"
说明:
如果 S 中不存这样的子串,则返回空字符串 ""。
如果 S 中存在这样的子串,我们保证它是唯一的答案。
- 分析:
首先通过测试用例我们知道,这道题中t字符串中的字符可能存在重复,我们要查找的字符对应的数量,需要用到数据结构——map。
- 首先,我们定义两个指针分别指向区间的起、始位置。
- 移动右指针,同时判断右指针指向的元素是否是字符串T中的元素,如果是统计该字符的数量。
- 然后,检查字符串S的统计数量和字符串T的统计数量是否一致,如果不一致,则继续移动右指针。
- 如果一致,则移动左指针,同时保证两个字符串中字符数量的一致性,然后判断子串的长度是否为最小。
class Solution {
public:
unordered_map<char,int> word_s,word_t;
bool check()
{
for(auto p:word_t)
{
if(word_s[p.first]<p.second)
return false;
}
return true;
}
string minWindow(string s, string t) {
int l=0,r=-1;
int minLen=INT_MAX;
int ansL=-1;
string ans;
for(int i=0;i<t.length();i++)
word_t[t[i]]++;
while(r<int(s.size()))
{
if(word_t.find(s[++r])!=word_t.end())
word_s[s[r]]++;
while(check()&&l<=r)
{
if(r-l+1<minLen)
{
minLen=r-l+1;
ansL=l;
}
if(word_t.find(s[l])!=word_t.end())
word_s[s[l]]--;
l++;
}
}
return ansL==-1?string():s.substr(ansL,minLen);
}
};
3. 长度最小的子数组(leetcode 209)
- 题目描述:
给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的连续子数组,并返回其长度。如果不存在符合条件的连续子数组,返回 0。
示例:
输入: s = 7, nums = [2,3,1,2,4,3]
输出: 2
解释: 子数组 [4,3] 是该条件下的长度最小的连续子数组。
进阶:
如果你已经完成了O(n) 时间复杂度的解法, 请尝试 O(n log n) 时间复杂度的解法。
- 分析:
首先,移动右指针,并在移动的过程中求和。
然后,当和大于等于s时,判断当前长度是否为最小长度,移动左指针,使得窗口向前滑动。
class Solution {
public:
int minSubArrayLen(int s, vector<int>& nums) {
if(nums.empty())
return 0;
int l=0;
int r=0;
int sum=0;
int minLen=INT_MAX;
while(r<nums.size())
{
sum+=nums[r];
while(sum>=s)
{
minLen=min(minLen,r-l+1);
sum-=nums[l++];
}
r++;
}
return minLen==INT_MAX?0:minLen;
}
};
4.最大连续1的个数III(leetcode 1004)
- 题目描述:
给定一个由若干 0 和 1 组成的数组 A,我们最多可以将 K 个值从 0 变成 1 。返回仅包含 1 的最长(连续)子数组的长度。
示例 1:
输入:A = [1,1,1,0,0,0,1,1,1,1,0], K = 2
输出:6
解释:
[1,1,1,0,0,1,1,1,1,1,1]
粗体数字从 0 翻转到 1,最长的子数组长度为 6。
示例 2:
输入:A = [0,0,1,1,0,0,1,1,1,0,1,1,0,0,0,1,1,1,1], K = 3
输出:10
解释:
[0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1,1,1]
粗体数字从 0 翻转到 1,最长的子数组长度为 10。
- 分析:
- 首先维持一个连续的区间【start,end】;
- 移动end指针,如果A[end]为0,那么统计可变0的个数,如果超过了K个,那么就移动start指针,使得区间内的可变0,最多为K。
- 然后更新当前的长度。
class Solution {
public:
int longestOnes(vector<int>& A, int K) {
if(A.empty())
return 0;
int start=0;
int ans=0;
int count=0;
for(int end=0;end<A.size();end++)
{
if(A[end]==0)
{
count++;
while(count>K)
{
if(A[start++]==0)
count--;
}
}
ans=max(ans,end-start+1);
}
return ans;
}
};
30. 串联所有单词的子串
159. 至多包含两个不同字符的最长子串
239. 滑动窗口最大值
567. 字符串的排列
632. 最小区间
727. 最小窗口子序列
2.双指针
1.最长回文串(leetcode 409)
- 题目描述:
给定一个包含大写字母和小写字母的字符串,找到通过这些字母构造成的最长的回文串。在构造过程中,请注意区分大小写。比如 "Aa"
不能当做一个回文字符串。
注意:假设字符串的长度不会超过 1010。
示例 1:
输入:
"abccccdd"
输出:
7
解释:
我们可以构造的最长的回文串是"dccaccd", 它的长度是 7。
- 分析:
- 回文,英文palindrome,指一个顺着读和反过来读都一样的字符串,比如madam。
- 回文字符串的里面的字符都是两两配对出现,除了字符串的长度为奇数时,最中间的那个字符只出现一次。
- 这道题是让我们构建回文字符串,我们可以将其分为两部分。
- 第一部分:回文的前半部分(包括中间那个元素),比如:madam的mad;
- 第二部分:回文的后半部分(不包括中间那个元素),比如:madam的am;
- 首先对字符串进行排序,使得重复的字符连续在一起;
- 遍历整个字符串,比较当前字符和下一个字符是否为同一个字符;
- 如果是,则将其分给前半部分,后半部分各一个,然后向后移动两个位置。
- front_s=s[i]+front_s;back_s+=s[i];
- 如果不是,那么这个字符就是单着的,那么就选择第一次出现的单着的字符加到前半部分上去,其余的舍去,然后向后移动一个位置。
- front_s=s[i]+front_s;
- 最后前半部的长度加上后半部分的长度就是回文的总长度。
优化:
- 可以直接统计长度,不构造前半部分,后半部分字符串。
- 可以用hash表:
- 构建一个长为52的hash数组,数组每个位置对应一个字母;
- 统计字符串中每个字母出现的次数;
- 然后遍历hash数组,如果次数为偶数,则加到总长度上;
- 如果次数为奇数,减去1再加到总长度上。
- 判断是否出现过奇数次的字母,如果是,则在总长度上再+1,否则直接输出。
class Solution {
public:
int longestPalindrome(string s) {
if(s.empty())
return 0;
//先排序,重复的字符分布在一起
sort(s.begin(),s.end());
//新的字符串的前半部分
string front_s="";
//新的字符串的后半部分
string back_s="";
bool flag=true;
for(int i=0;i<s.length();)
{
//两两配对
if(i<(s.length()-1)&&s[i]==s[i+1])
{
front_s=s[i]+front_s;
back_s+=s[i];
i=i+2;
}
else
{
if(flag)
{
front_s=s[i]+front_s;
flag=false;
i++;
}
else
{
i++;
}
}
}
// cout<<front_s+back_s<<endl;
return front_s.length()+back_s.length();
}
};
- 优化版本:
class Solution {
public:
int longestPalindrome(string s) {
if(s.empty())
return 0;
//先排序,重复的字符分布在一起
sort(s.begin(),s.end());
int num=0;
bool flag=true;
for(int i=0;i<s.length();)
{
//两两配对
if(i<(s.length()-1)&&s[i]==s[i+1])
{
num+=2;
i=i+2;
}
else
{
if(flag)
{
num++;
flag=false;
i++;
}
else
{
i++;
}
}
}
return num;
}
};
- hash表
class Solution {
public:
int longestPalindrome(string s) {
if(s.empty())
return 0;
int word[52]={0};
int sum=0;
for(int i=0;i<s.length();i++)
{
if(s[i]>='a'&&s[i]<='z')
{
word[s[i]-'a']++;
}
else
{
word[s[i]-'A'+26]++;
}
}
bool flag=false;
for(int i=0;i<52;i++)
{
if(word[i]%2==0)
{
sum+=word[i];
}
else
{
flag=true;
sum+=word[i]-1;
}
}
return flag?sum+1:sum;
}
};
2.验证回文串(leetcode 125)
- 题目描述:
给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。
说明:本题中,我们将空字符串定义为有效的回文串。
示例 1:
输入: "A man, a plan, a canal: Panama"
输出: true
示例 2:
输入: "race a car"
输出: false
- 分析:
首先,需要知道什么是回文,可以参考双指针第一题(最长回文串(leetcode 409)),如果是回文,那么字符串的数量可以是奇数或者偶数,如果为奇数,就多一个字符,因此可以把奇数情况当作偶数情况判断,由于字符串中有非字母和数字的字符,那么就不能从中心向外扩散,因为要判断两个字符是否相等,所有可以用双指针,从左右头部/末尾开始遍历,结束条件为left==right。
遍历过程中,要判断字符是不是非字母和数字,如果不是,则跳过,然后检验左右指针的元素是否相等,在检测字符时,由于不区分大小写,所有要判断,两个字符之差的绝对值是否为32 (32是ASCII码表对应值的差值)。
class Solution {
public:
bool isPalindrome(string s) {
int left=0;
int right=s.length()-1;
while(left<right)
{
cout<<"left: "<<s[left]<<" right: "<<s[right]<<endl;
if(!((s[left]<='9'&&s[left]>='0')||(s[left]>='A'&&s[left]<='Z')||(s[left]<='z'&&s[left]>='a')))
{
cout<<"left: "<<s[left]<<" is false"<<endl;
left++;
continue;
}
else if(!((s[right]<='9'&&s[right]>='0')||(s[right]>='A'&&s[right]<='Z')||(s[right]<='z'&&s[right]>='a')))
{
cout<<"right: "<<s[right]<<" is false"<<endl;
right--;
continue;
}
else
{
if((s[left]<='9'&&s[left]>='0')&&(s[right]<='9'&&s[right]>='0'))
{
cout<<"left: "<<s[left]<<" right: "<<s[right];
if(s[left]==s[right])
{
cout<<" is ="<<endl;
left++;
right--;
}
else
return false;
cout<<endl;
}
else if(((s[left]>='A'&&s[left]<='Z')||(s[left]<='z'&&s[left]>='a'))&&((s[right]>='A'&&s[right]<='Z')||(s[right]<='z'&&s[right]>='a')))
{
cout<<"left: "<<s[left]<<" right: "<<s[right];
if(s[left]==s[right]||abs(s[left]-s[right])==32)
{
cout<<" is ="<<endl;
left++;
right--;
}
else
return false;
cout<<endl;
}
else
return false;
}
}
return true;
}
};
3.最长回文子串(leetcode 5)
- 题目描述:
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。
示例 2:
输入: "cbbd"
输出: "bb"
- 分析:
- 首先,遍历整个数组,每次移动一个位置,就用中心扩散法判断以当前元素为中心的回文字符串。
- 回文字符串有两种,一种是为奇数个,一种是为偶数个,因此我们需要对这两种情况都进行中心扩散。
- 当中心扩散时,我们判断左指针和右指针指向的元素是否相等,如果相等,则移动左右指针,否则直接结束返回。
- 当中心扩散结束后,判断两种情况下最长的回文字符串。
class Solution {
public:
string isPalindrome(string s,int l,int r)
{
while(l>=0&&r<s.length())
{
if(s[l]!=s[r])
break;
l--;
r++;
}
return s.substr(l+1,r-l-1);
}
string longestPalindrome(string s) {
string sub_str1,sub_str2,ans;
if(s.empty())
return ans;
for(int i=0;i<s.length();i++)
{
sub_str1=isPalindrome(s,i,i);
sub_str2=isPalindrome(s,i,i+1);
ans=ans.size()>sub_str1.size()?ans:sub_str1;
ans=ans.size()>sub_str2.size()?ans:sub_str2;
}
return ans;
}
};
- 动态规划:
4.验证回文字符串(leetcode 680)
- 题目描述:
给定一个非空字符串 s,最多删除一个字符。判断是否能成为回文字符串。
示例 1:
输入: "aba"
输出: True
示例 2:
输入: "abca"
输出: True
解释: 你可以删除c字符。
注意:
字符串只包含从 a-z 的小写字母。字符串的最大长度是50000。
- 分析:
这道题和判断回文字符串类似,但是难点在于跳过一个字符,再判断。
- 首先,双指针分别指向字符串的头和尾,然后依次向中间遍历。
- 当左右两边字符相同,两个指针都进行移动。
- 当左右两边字符不同,这时我们需要把这个字符跳过,再判断是否是回文字符串。
- 但是,在跳的时候,我们不知道这个字符是靠近左边一点还是右边一点,所以对两边都进行判断,只要有一边满足就成立。
class Solution {
public:
bool Palindrome(string s,int start,int end)
{
while(start<end)
{
if(s[start]!=s[end])
return false;
start++;
end--;
}
return true;
}
bool validPalindrome(string s) {
int left=0;
int right=s.length()-1;
while(left<right)
{
if(s[left]==s[right])
{
left++;
right--;
}
else
{
return Palindrome(s,left+1,right)||Palindrome(s,left,right-1);
}
}
return true;
}
};
5.每个元音包含偶数次的最长子字符串(leetcode 1371)
- 题目描述:
给你一个字符串 s ,请你返回满足以下条件的最长子字符串的长度:每个元音字母,即 'a','e','i','o','u' ,在子字符串中都恰好出现了偶数次。
示例 1:
输入:s = "eleetminicoworoep"
输出:13
解释:最长子字符串是 "leetminicowor" ,它包含 e,i,o 各 2 个,以及 0 个 a,u 。
示例 2:
输入:s = "leetcodeisgreat"
输出:5
解释:最长子字符串是 "leetc" ,其中包含 2 个 e 。
示例 3:
输入:s = "bcbcbc"
输出:6
解释:这个示例中,字符串 "bcbcbc" 本身就是最长的,因为所有的元音 a,e,i,o,u 都出现了 0 次。
提示:
1 <= s.length <= 5 x 10^5
s 只包含小写英文字母。
- 分析:
这道题开始没啥思路,就暴力解呗,子串就是连续的字符组成,然后还有判断是不是元音,如果是元音,那么所有元音的次数是不是为偶数,如果为偶数,那么判断此时的长度是否是最长的。
这相当于从字符串的起点到终点,固定子串的起始位置,然后移动子串的结束位置,判断此时的子串是否满足条件。
class Solution {
public:
char word[5]={'a','e','i','o','u'};
int findTheLongestSubstring(string s) {
if(s.empty())
return 0;
int maxLength=0;
unordered_map<char,int> map;
for(int i=0;i<s.length();i++)
{
bool flag;
for(int j=i;j<s.length();j++)
{
switch(s[j])
{
case 'a':
map['a']++;
break;
case 'e':
map['e']++;
break;
case 'i':
map['i']++;
break;
case 'o':
map['o']++;
break;
case 'u':
map['u']++;
break;
default:
break;
}
//判断元音字符是否都为偶数
flag=true;
for(int k=0;k<5;k++)
{
if(map[word[k]]%2!=0)
{
flag=false;
break;
}
}
if(flag)
{
maxLength=max(maxLength,j-i+1);
}
}
map.clear();
}
return maxLength;
}
};
注:由于测试数据规模太大,这个O(N^2)的肯定是超出时间限制的。
- 改进版
class Solution {
public:
int findTheLongestSubstring(string s) {
int ans = 0, status = 0, n = s.length();
vector<int> pos(1 << 5, -1);
pos[0] = 0;
for (int i = 0; i < n; ++i) {
if (s[i] == 'a') {
status ^= 1<<0;
} else if (s[i] == 'e') {
status ^= 1<<1;
} else if (s[i] == 'i') {
status ^= 1<<2;
} else if (s[i] == 'o') {
status ^= 1<<3;
} else if (s[i] == 'u') {
status ^= 1<<4;
}
if (~pos[status]) {
ans = max(ans, i + 1 - pos[status]);
} else {
pos[status] = i + 1;
}
}
return ans;
}
};
6.最长公共前缀(leetcode 14)
- 题目描述:
编写一个函数来查找字符串数组中的最长公共前缀。
如果不存在公共前缀,返回空字符串 ""。
示例 1:
输入: ["flower","flow","flight"]
输出: "fl"
示例 2:
输入: ["dog","racecar","car"]
输出: ""
解释: 输入不存在公共前缀。
说明:
所有输入只包含小写字母 a-z 。
- 分析:
最长公共前缀,首先公共前缀是需要所有字符串都有的前缀,我的思路:
- 首先,如果不含有字符串,则公共前缀为空。
- 其次,如果只含有一个字符串,那么公共前缀就是他本身。
- 如果含有两个字符串,那么通过双指针找出其公共前缀部分,当指针指向的字符不等时,结束循环。
- 如果含有两个以上,通过前两个字符串的公共前缀部分和剩余的字符串进行公共前缀判断,直到前缀为空、当前字符串为空或没有了字符串,返回结果。
水平扫描(暴力):
class Solution {
public:
string longestCommonPrefix(vector<string>& strs) {
string ans;
if(strs.size()==0)
return ans;
if(strs.size()==1)
return strs[0];
int i=0;
while(i<strs[0].length()&&i<strs[1].length())
{
if(strs[0][i]!=strs[1][i])
break;
ans=ans+strs[0][i];
i++;
}
cout<<ans<<std::endl;
if(ans.empty())
return ans;
if(strs.size()==2)
return ans;
for(int i=2;i<strs.size();i++)
{
int j=0;
string res;
while(j<ans.length()&&j<strs[i].length())
{
if(ans[j]!=strs[i][j])
break;
res=res+ans[j];
j++;
}
ans=res;
if(ans.empty()||strs[i].empty())
return string();
}
return ans;
}
};
分治法:
class Solution {
public:
string helper(vector<string> strs,int L,int R)
{
if(L==R)
return strs[L];
int mid=(L+R)/2;
string LeftCommonPrefix=helper(strs,L,mid);
string RightCommonPrefix=helper(strs,mid+1,R);
return CommonPrefix(LeftCommonPrefix,RightCommonPrefix);
}
string CommonPrefix(string str1,string str2)
{
if(str1.empty()||str2.empty())
return string();
int i=0;
string res;
while(i<str1.length()&&i<str2.length())
{
if(str1[i]!=str2[i])
break;
res=res+str1[i];
i++;
}
return res;
}
string longestCommonPrefix(vector<string>& strs) {
if(strs.empty())
return string();
else
return helper(strs,0,strs.size()-1);
}
};
7.判断子序列(leetcode 392)
- 题目描述:
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
你可以认为 s 和 t 中仅包含英文小写字母。字符串 t 可能会很长(长度 ~= 500,000),而 s 是个短字符串(长度 <=100)。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
示例 1:
s = "abc", t = "ahbgdc"
返回 true.
示例 2:
s = "axc", t = "ahbgdc"
返回 false.
后续挑战 :
如果有大量输入的 S,称作S1, S2, ... , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?
- 分析:
- 首先,两个指针指向两个字符串的头部;
- 如果s[i]和t[j]相等,那么两个指针都向右移动(i++,j++);
- 如果不等,则移动原字符串的指针 (j++);
- 当j遍历完了,但是i还没遍历完,返回false;
- 当i被遍历完了,返回true;
class Solution {
public:
bool isSubsequence(string s, string t) {
if(s.empty())
return true;
if(t.empty())
return false;
int i=0;
int j=0;
while(i<s.length())
{
if(j==t.length())
return false;
if(s[i]==t[j])
{
i++;
j++;
}
else
j++;
}
return true;
}
};