3. 无重复字符的最长子串
给定一个字符串 s
,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: s = "abcabcbb" 输出: 3 解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
思路:想象一下我们自己是怎么判断无重复的,先以第K个字符开始,依次加入字符,直到遇到重复的,这里抽象成代码语言就是:需要两个指针(左右指针去移动),还需要一个能存储字符的数据结构,并且可以判断重复。所以我们就可以采用滑动窗口去处理,对于判断重复用的数据结构为哈希集合(即 C++
中的 std::unordered_set
在左指针向右移动的时候,我们从哈希集合中移除一个字符,在右指针向右移动的时候,我们往哈希集合中添加一个字符。
由上面想法可以写出下面代码(个人走的弯路,不想看可以跳过)
class Solution {
public:
int lengthOfLongestSubstring(string s) {
unordered_set<char> str;
int right=0;//右值针
int lenght=0;//最大长度
for(int i=0;i<s.size();i++){
str.insert(s[i]);
right=i+1;
while(!str.count(s[right])&&right<s.size()){
str.insert(s[right]);
++right;
}
lenght=max(lenght,right-i);
right=i;
str.clear();
}
return lenght;
}
};
可以解出来,但是耗时太长没有利用好滑动窗口减少比对次数
我们继续优化,减少比对次数,实际上每次左值针更新时,右指针可以不用左值针处开始比较,可以先将左值针移动向里收缩,右指针不动。
原因:假设我们选择字符串中的第 k 个字符作为起始位置,并且得到了不包含重复字符的最长子串的结束位置为 rk。那么当我们选择第 k+1个字符作为起始位置时,首先从 k+1到 rk的字符显然是不重复的,并且由于少了原本的第 k 个字符,我们可以尝试继续增大 rk (而不是从左值针处开始),直到右侧出现了重复字符为止。
优化代码:
class Solution {
public:
int lengthOfLongestSubstring(string s) {
unordered_set<char> str;
int right=0;//右值针
int lenght=0;//最大长度
for(int i=0;i<s.size();i++){
if(i!=0){
str.erase(s[i-1]);
}
while(!str.count(s[right])&&right<s.size()){
str.insert(s[right]);
++right;
}
lenght=max(lenght,right-i);
}
return lenght;
}
};
438. 找到字符串中所有字母异位词
给定两个字符串 s
和 p
,找到 s
中所有 p
的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。
示例 1:
输入: s = "cbaebabacd", p = "abc" 输出: [0,6] 解释: 起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。 起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。
思路:用滑动窗口在S中找到和p的异位词就行,滑动过程中维持滑动窗口的长度等于P的长度。
PS:这里开始我用49. 字母异位词分组同样的方法,利用排序找出p字符串和滑动窗口的key,再进行比较,发现力扣有些变态测试用例会严重超时。其他测试用例都可以通过。代码如下(仅供参考):
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
vector<int> nums;
sort(p.begin(),p.end());
int left=0,right=p.size();
while((left+right)<=s.size()){
string result=s.substr(left,right);
sort(result.begin(),result.end());
if(result==p){
nums.emplace_back(left);
}
++left;
}
return nums;
}
};
继续优化,耗时的原因就是排序,所以换成通过统计滑动窗口中字母出现的次数来判断是否和P是异位词。最后一个小细节,如果字符串s的长度小于字符串p的长度,直接返回空。
代码:
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
int sL=s.size(),pL=p.size();
if(sL<pL){//匹配字符串没有异位词长度大
return vector<int>();
}
vector<int> nums;
vector<int> snums(26);
vector<int> pnums(26);
for (int i = 0; i < pL; ++i) {//统计滑动窗口刚开始,内部包含的字母数量,相当于初始化滑动窗口
++snums[s[i] - 'a'];
++pnums[p[i] - 'a'];
}
if (snums == pnums) {
nums.emplace_back(0);//如果滑动窗口启动时就匹配,说明位置0就是一个答案
}
for (int i = 0; i < sL - pL; ++i) {
--snums[s[i] - 'a'];//滑动窗口左侧抛出
++snums[s[i + pL] - 'a'];//滑动窗口右侧加入
if (snums == pnums) {
nums.emplace_back(i + 1);//注意i位置是被抛出滑动窗口的,匹配成功应该记录i的下一个位置
}
}
return nums;
}
};
560. 和为 K 的子数组
给你一个整数数组 nums
和一个整数 k
,请你统计并返回 该数组中和为 k
的子数组的个数 。
子数组是数组中元素的连续非空序列。
示例 1:
输入:nums = [1,1,1], k = 2 输出:2
示例 2:
输入:nums = [1,2,3], k = 3 输出:2
思路:注意这里子数组是连续的序列,所以不存在跳着组合加起来等于k。看到连续数组和就用前缀和去处理(前缀和不在这里介绍)。
前缀和数组中有两种情况,一是本身就等于k,二是存在两个前缀和相减等于k。
对于上述两种情况,需要用map数组去统计前缀和出现的次数
为什么呢?
首先对于情况一,直接输出等于k的前缀和出现的次数,所以遍历前缀和时用map去统计出现次数;
对于情况二,我们举一个例子,如下
可以看到对于情况二,如果前缀和出现相等时,那么符合条件的子数组个数就等于该前缀和出现的次数。例如前缀和0出现次数等于2,所以符合结果的子数组次数也是2,到这里就可以发现为什么要用map统计前缀和出现的次数,用代码表示就是
sum//当前前缀和
sum-k//符合条件的前缀和
map.count(sum-k)//map中符合条件的前缀和是否出现
res+=map[sum-k];//统计map中符合条件的前缀和出现次数
最后整合情况一和情况二,对于情况一来说前缀和sum等于K,所以我们初始化时候,map[0]=1,这样只要符合前缀和出现时候,map[sum-k]就是map[0]出现次数,这样就将情况一和情况二整合了。
代码:
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
unordered_map<int,int> map;//存放前缀和
int sum=0;//当前前缀和
int res=0;
map[0]=1;
for(auto n:nums){
sum+=n;//计算前缀和
if(map.count(sum-k)){
res+=map[sum-k];
}
map[sum]++;//当前前缀和次数加一
}
return res;
}
};