3. 无重复字符的最长子串——数组快很多因为不需要有set的存取操作
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
输入: s = “abcabcbb”
输出: 3
思路:滑动窗口
队列也好数组也好,其实就是一个控制左右的东东,右边满足条件就扩一下,左边不满足了就踢出去,然后注意边界条件!
time On
A1.unordered_set
class Solution {
public:
int lengthOfLongestSubstring(string s) {
if(s.size() == 0) return 0;
unordered_set<char> st;//hashset作为滑动窗口
int maxStr = 0;//res
int left = 0;
for(int i = 0; i < s.size(); i++){
//注意是个while,如果是abcc他会一直把左边删到只剩c
while (st.find(s[i]) != st.end()){//如果有不满足的
st.erase(s[left]);//erase
left ++;//维护左指针
}
maxStr = max(maxStr,i-left+1);//res要放外面,不能放while里,不然如果没有重复字符res就一直是0了
st.insert(s[i]);//右边插入
}
return maxStr;
}
};
A2.fre[256]
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int n = s.size();
if(n==0) return 0;
int fre[256] = {0};//妙就妙在这里,用一个fre记录出现的频次来决定右边怎么扩
int l =0,r=-1,res=0;
while(l<n){
if(r+1<n&&fre[s[r+1]]==0){//注意边界条件
fre[s[r+1]]++;
r++;
}
else{
fre[s[l]]--;
l++;
}
res = max(res,r-l+1);
}
return res;
}
};
219. 存在重复元素 II
给定一个整数数组和一个整数 k,判断数组中是否存在两个不同的索引 i 和 j,使得 nums [i] = nums [j],并且 i 和 j 的差的 绝对值 至多为 k。
输入: nums = [1,2,3,1], k = 3
输出: true
思路不能说和第三题差不多,只能说一模一样,这是固定窗口滑动,怎么实现是关键!
class Solution {
public:
bool containsNearbyDuplicate(vector<int>& nums, int k) {
unordered_set<int> st;
for(int i =0;i<nums.size();i++){
if(st.find(nums[i])!=st.end()){
return true;
}
st.insert(nums[i]);
if(st.size()>k){
st.erase(nums[i-k]);
}
}
return false;
}
};
220. 存在重复元素 III
给你一个整数数组 nums 和两个整数 k 和 t 。请你判断是否存在 两个不同下标 i 和 j,使得 abs(nums[i] - nums[j]) <= t ,同时又满足 abs(i - j) <= k 。
如果存在则返回 true,不存在返回 false。
输入:nums = [1,2,3,1], k = 3, t = 0
输出:true
思路
1.滑动窗口+set
就是上一题多了个条件,对应修改就好,有+法要考虑整形溢出问题所以要用long long
class Solution {
public:
bool containsNearbyAlmostDuplicate(vector<int>& nums, int k, int t) {
set<long long> st;//set是二叉树实现的Onlogn
int left = 0;
for(int right=0;right<nums.size();++right){
// if(st.find(nums[i])!=st.end()){
// return true;
// }
//由|nums[i]-num[j]|<=t可以推出
//nums[i]-t<=nums[j]<=nums[i]+t
//set的内置函数lower_bound可以找到第一个大于等于nums[i]-t的数,则这个数再满足小于等于nums[i]+t即可
auto a = st.lower_bound((long long)nums[right]-(long long)t);
if(a!=st.end() && *a <= (long long)nums[right]+(long long)t){
return true;
}
st.insert(nums[abs(*a - nums[right]) <= t]);
if(st.size()>k){
st.erase(nums[left]);
}
}
return false;
}
};
2.桶排序(还需打磨)
上述解法无法做到线性的原因是:我们需要在大小为 k 的滑动窗口所在的「有序集合」中找到与 u 接近的数。
如果我们能够将 k 个数字分到 kk 个桶的话,那么我们就能 O(1)O(1) 的复杂度确定是否有 [u - t, u + t][u−t,u+t] 的数字(检查目标桶是否有元素)。
具体的做法为:令桶的大小为 size = t + 1size=t+1,根据 u 计算所在桶编号:
如果已经存在该桶,说明前面已有 [u - t, u + t][u−t,u+t] 范围的数字,返回 true
如果不存在该桶,则检查相邻两个桶的元素是有 [u - t, u + t][u−t,u+t] 范围的数字,如有 返回 true
建立目标桶,并删除下标范围不在 [max(0, i - k), i)[max(0,i−k),i) 内的桶
#define LL long long
class Solution {
public:
LL size;
bool containsNearbyAlmostDuplicate(vector <int> & nums, int k, int t) {
int n = nums.size();
unordered_map<LL, LL> m;
size = t + 1L;
for (int i = 0; i < n; i++) {
LL u = nums[i] * 1L;
LL idx = getIdx(u);
// 目标桶已存在(桶不为空),说明前面已有 [u - t, u + t] 范围的数字
if (m.find(idx) != m.end()) return true;
// 检查相邻的桶
LL l = idx - 1, r = idx + 1;
if (m.find(l) != m.end() && abs(u - m[l]) <= t) return true;
if (m.find(r) != m.end() && abs(u - m[r]) <= t) return true;
// 建立目标桶
m.insert({idx, u});
// 移除下标范围不在 [max(0, i - k), i) 内的桶
if (i >= k) m.erase(getIdx(nums[i - k]));
}
return false;
}
LL getIdx(LL u) {
return u >= 0 ? u / size : ((u + 1) / size) - 1;
}
};
【重点】如何理解 getIdx() 的逻辑
为什么 size 需要对 t 进行 +1 操作?
目的是为了确保差值小于等于 t 的数能够落到一个桶中。
举个 🌰,假设 [0,1,2,3],t = 3,显然四个数都应该落在同一个桶。
如果不对 t 进行 +1 操作的话,那么 [0,1,2] 和 [3] 会被落到不同的桶中,那么为了解决这种错误,我们需要对 t 进行 +1 作为 size 。
这样我们的数轴就能被分割成:
0 1 2 3 | 4 5 6 7 | 8 9 10 11 | 12 13 14 15 | …
总结一下,令 size = t + 1 的本质是因为差值为 t 两个数在数轴上相隔距离为 t + 1,它们需要被落到同一个桶中。
当明确了 size 的大小之后,对于正数部分我们则有 idx = nums[i] / size。
如何理解负数部分的逻辑?
由于我们处理正数的时候,处理了数值 0,因此我们负数部分是从 -1 开始的。
还是我们上述 🌰,此时我们有 t = 3 和 size = t + 1 = 4。
考虑 [-4,-3,-2,-1] 的情况,它们应该落在一个桶中。
如果直接复用 idx = nums[i] / size 的话,[-4] 和 [-3,-2,-1] 会被分到不同的桶中。
根本原因是我们处理整数的时候,已经分掉了数值 0。
这时候我们需要先对 nums[i] 进行 +1 操作(即将负数部分在数轴上进行整体右移),即得到 (nums[i] + 1) / size。
这样一来负数部分与正数部分一样,可以被正常分割了。
但由于 0 号桶已经被使用了,我们还需要在此基础上进行 -1,相当于将负数部分的桶下标(idx)往左移,即得到 ((nums[i] + 1) / size) - 1。
Leecode 340 至多包含 K 个不同字符的最长子串 Leecode 1004 最大连续1的个数 III Leecode 1208 尽可能使字符串相等 Leecode 1493 删掉一个元素以后全为 1 的最长子数组 Leecode 3 无重复字符的最长子串 [30. 串联所有单词的子串] Leecode 76 最小覆盖子串 [159. 至多包含两个不同字符的最长子串] Leecode 209 长度最小的子数组 [239. 滑动窗口最大值] Leecode 438 找到字符串中所有字母异位词 Leeocde 567 字符串的排列 [632. 最小区间] [727. 最小窗口子序列]
30. 串联所有单词的子串
-
最小覆盖子串
-
至多包含两个不同字符的最长子串
209. 长度最小的子数组
给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
思路:
暴力法就是两个for,On2的time
滑动窗口就是要想好怎么滑,什么情况下滑,怎么创建这个滑动窗口,On
其实滑动窗口核心还是双指针,左右边界不就是两个指针决定的嘛
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int n = nums.size();
int l = 0,res = 1e7,sum=0;
for(int r = 0;r<n;++r){
sum+=nums[r];//第三题也是这样,先由右指针去搞出窗口来
while(sum>=target){//这个就是判断左边能不能进来
res = min(res,r-l+1);
sum-=nums[l++];//这题的核心在这里,也就是怎么滑
}
}
return res==1e7?0:res;//要注意边界
}
};
- 滑动窗口最大值
567. 字符串的排列
给定两个字符串 s1 和 s2,写一个函数来判断 s2 是否包含 s1 的排列。
换句话说,第一个字符串的排列之一是第二个字符串的 子串 。
输入: s1 = “ab” s2 = “eidbaooo”
输出: True
解释: s2 包含 s1 的排列之一 (“ba”).
思路:滑动窗口
由于是 s2 中判断是否包含 s1 的排列,而且 s1 和 s2 均为小数。
可以使用数组先对 s1 进行统计,之后使用滑动窗口进行扫描,每滑动一次检查窗口内的字符频率和 s1 是否相等 ~
以下代码,可以作为滑动窗口模板使用:
PS. 你会发现以下代码和 643. 子数组最大平均数 I 和 1423. 可获得的最大点数 代码很相似,因为是一套模板。
初始化将滑动窗口压满,取得第一个滑动窗口的目标值
继续滑动窗口,每往前滑动一次,需要删除一个和添加一个元素
class Solution {
public:
bool checkInclusion(string s1, string s2) {
vector<int> cnt1(26),cnt2(26);
int n = s1.size(),m = s2.size();
if(n>m) return false;
for(int i = 0;i<n;i++){//搞两个窗口出来,小的那个不动
++cnt1[s1[i]-'a'];
++cnt2[s2[i]-'a'];
}
if(cnt1==cnt2) return true;
for(int i = n;i<m;i++){//滑大的那个
++cnt2[s2[i]-'a'];
--cnt2[s2[i-n]-'a'];
if(cnt1==cnt2) return true;
}
return false;
}
};
时间复杂度:O(n)O(n)
空间复杂度:O(n)O(n)
但其实这是某道困难题的简化版,本题根据「字符」滑动,而 30. 串联所有单词的子串 则是根据「单词」来。但基本思路都是一样的,强烈建议你来试试 ~
-
最小区间
-
最小窗口子序列