128. 最长连续序列
给定一个未排序的整数数组 nums
,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
请你设计并实现时间复杂度为 O(n)
的算法解决此问题
示例 1:
输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。
示例 2:
输入:nums = [0,3,7,2,5,8,4,6,0,1]
输出:9
题解
哈希表(unordered_set)
两个优化点
- 处理跳过,也就是有比自己当前数值小1的就直接过,优化性能
- 输入数据存在大量重复的情况,遍历的时候去遍历set而不是原数组,来达到用set去重的效果
我们考虑枚举数组中的每个数 x x x,考虑以其为起点,不断尝试匹配 x + 1 , x + 2 , ⋯ x+1,x+2,⋯ x+1,x+2,⋯ 是否存在,假设最长匹配到了 x + y x+y x+y,那么以 x x x 为起点的最长连续序列即为 x , x + 1 , x + 2 , ⋯ , x + y x,x+1,x+2,⋯ ,x+y x,x+1,x+2,⋯ ,x+y,其长度为 y + 1 y+1 y+1,我们不断枚举并更新答案即可。
对于匹配的过程,暴力的方法是 O ( n ) O(n) O(n) 遍历数组去看是否存在这个数,但其实更高效的方法是用一个哈希表存储数组中的数,这样查看一个数是否存在即能优化至 O ( 1 ) O(1) O(1) 的时间复杂度。
哈希表 —> 将 查看一个数是否存在 优化至 O ( 1 ) O(1) O(1) 。
仅仅是这样我们的算法时间复杂度最坏情况下还是会达到 O ( n 2 ) O(n^2) O(n2)(即外层需要枚举 O ( n ) O(n) O(n)个数,内层需要暴力匹配 O ( n ) O(n) O(n) 次(即使有哈希表使得匹配过程为 O ( 1 ) O(1) O(1))),无法满足题目的要求。但仔细分析这个过程,我们会发现其中执行了很多不必要的枚举,如果已知有一个 x , x + 1 , x + 2 , ⋯ , x + y x,x+1,x+2,⋯ ,x+y x,x+1,x+2,⋯ ,x+y 的连续序列,而我们却重新从 x + 1 x+1 x+1, x + 2 x+2 x+2 或者是 x + y x+y x+y 处开始尝试匹配,那么得到的结果肯定不会优于枚举 x x x 为起点的答案,因此我们在外层循环的时候碰到这种情况跳过即可。
那么怎么判断是否跳过呢?由于我们要枚举的数 x x x 一定是在数组中不存在前驱数 x − 1 x−1 x−1 的,不然按照上面的分析我们会从 x − 1 x−1 x−1 开始尝试匹配,因此我们每次在哈希表中检查是否存在 x − 1 x−1 x−1 即能判断是否需要跳过了。
增加了判断跳过的逻辑之后,时间复杂度是多少呢?外层循环需要 O ( n ) O(n) O(n) 的时间复杂度,只有当一个数是连续序列的第一个数的情况下才会进入内层循环,然后在内层循环中匹配连续序列中的数,因此数组中的每个数只会进入内层循环一次(只有连续序列的第一个数进入内层循环,连续序列中其他的数在「这个」内存循环中被消耗掉了,外层循环轮到他们的时候直接跳过)。根据上述分析可知,总时间复杂度为 O ( n ) O(n) O(n),符合题目要求。
注意:哈希表(map/set)中的count函数只返回「有没有该元素」,与该元素下标对应的数值无关
count:找到该元素返回1,找不到返回0
比如
unordered_map<string, int> dict; // 声明unordered_map对象 cout<<dict.count("apple");//输出0,因为找不到"apple"索引的元素 dict.insert(pair<string,int>("apple",0)); //dict.emplace("apple",0); //dict.insert(make_pair("apple",0)); //dict["apple"] = 0; cout<<dict.count("apple");//输出1,因为找得到"apple"索引的元素 dict.erase("apple"); cout<<dict.count("apple");//输出0,因为找不到"apple"索引的元素
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
unordered_set<int> numset;
int ret = 0;
for(int i:nums) numset.insert(i);//这一步利用set对原数组去重
for(auto i:numset){//使用基于范围的循环访问set
if(!numset.count(i-1)){
int startnum = i,locallen = 1;//当前连续序列的起始数字和长度
while(numset.count(startnum+1)){//判断序列是否连续
++startnum
++locallen;
}
ret = max(ret,locallen);
}
}
return ret;
}
};
724. 寻找数组的中心下标
给你一个整数数组 nums
,请计算数组的 中心下标 。
数组 中心下标 是数组的一个下标,其左侧所有元素相加的和等于右侧所有元素相加的和。
如果中心下标位于数组最左端,那么左侧数之和视为 0
,因为在下标的左侧不存在元素。这一点对于中心下标位于数组最右端同样适用。
如果数组有多个中心下标,应该返回 最靠近左边 的那一个。如果数组不存在中心下标,返回 -1
。
前缀和的应用
简单题,一开始的思路是先正序遍历一遍求前缀和,再倒序遍历一遍,用一个变量滚动记录后缀和
实际上后缀和即为区间和,用已经求出的前缀和数组相减即得
class Solution {
public int pivotIndex(int[] nums) {
int n = nums.length;
int[] sum = new int[n + 1];
for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + nums[i - 1];
for (int i = 1; i <= n; i++) {
int left = sum[i - 1], right = sum[n] - sum[i];//任意元素右侧值(后缀和)直接用前缀和相减就得到了
if (left == right) return i - 1;
}
return -1;
}
}
560. 和为K的子数组
给你一个整数数组 nums
和一个整数 k
,请你统计并返回 该数组中和为 k
的子数组的个数 。
子数组是数组中元素的连续非空序列。
示例 1:
输入:nums = [1,1,1], k = 2
输出:2
示例 2:
输入:nums = [1,2,3], k = 3
输出:2
前缀和&哈希
前缀和定义
- 构建前缀和数组,以快速计算区间和(前后两段前缀和之差);
- 注意在计算区间和的时候,下标有偏移(下标为0的前缀和为0,下标为1前缀和为nums[0],也即前缀和不包含当前下标元素),因此双层循环中,取后面的(更大)前缀和的j遍历范围为[i+1,nums.size()+1](下标为nums.size()+1的前缀和即为nums数组之和)
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
int ans = 0;
vector<int> prefix(nums.size()+1);//注意前缀和比元素个数多1,下标对应偏移
prefix[0] = 0;//下标0的前缀和为0
for(int i=0;i<nums.size();i++){//遍历一遍构建前缀和数组
prefix[i+1] = prefix[i]+nums[i];
}
for(int i=0;i<nums.size();i++){//双重遍历,将暴力中的遍历元素求和,转换成前缀和遍历相减找k
for(int j=i+1;j<nums.size()+1;j++){
if(prefix[j]-prefix[i]==k) ans++;
}
}
return ans;
}
};
//前缀和相减获得中间子串之和(区间和),O(N^2)
加入两数之和哈希思想
-
前后前缀和相减为区间和,一遍遍历所有元素求前缀和的同时计算区间和=k的数量
-
由于只关心前缀和之差为k的出现次数,不关心具体是哪两项的前缀和之差等于k,可以使用哈希表加速运算;
-
由于哈希表保存了之前相同前缀和的个数,计算区间总数的时候不是一个一个地加,而是直接在哈希表中获取,空间换时间,相当于内层的搜索判断循环
for(int j=i+1;j<nums.size()+1;j++){ if(prefix[j]-prefix[i]==k) ans++; }
直接变成
if(prefixhash.find(presum-k)!=prefixhash.end()){ ans+=prefixhash[presum-k]; }
哈希表使得搜索对应前缀和时间复杂度O(N) -> O(1)
算法如下:
从左到右一次遍历,使用哈希表保存已经计算过的前缀和出现次数
计算完包括了当前数的前缀和preSum以后,我们在哈希表中查一查在当前数之前,有多少个前缀和等于 preSum - k (意味着当前数的前缀和能与前面多少个历史前缀和形成的区间和为k)
将这个数值加入到ans中,并在哈希表中更新当前的preSum
考虑边界条件初始化,下标 0 之前没有元素,可以认为前缀和为 0,个数为 1 个,因此 prefixhash[0] = 1,这一点是必要且合理的。
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
unordered_map<int,int> prefixhash;//key为前缀和大小,value为其出现次数
int presum = 0;
int ans = 0;
prefixhash[0] = 1;//注意哈希前缀和的初始化,前缀和为0的个数为1,也即前缀和数组初始化prefix[0] = 1
for(int n:nums){
presum+=n;//计算包括了当前数的前缀和preSum
if(prefixhash.find(presum-k)!=prefixhash.end()){//内层循环哈希化为O(1)
ans+=prefixhash[presum-k];
}
prefixhash[presum]++;
}
return ans;
}
};
//前缀和相减获得中间子串之和,
//与滑动窗口不同点(这道题为什么不能用滑动窗口)在于滑动窗口需满足增减元素时,窗口内定义值的单调性
//而这道题求和,窗口内值并非单调的,因为有负数
1248. 统计「优美子数组」
给你一个整数数组 nums
和一个整数 k
。如果某个连续子数组中恰好有 k
个奇数数字,我们就认为这个子数组是「优美子数组」。
请返回这个数组中 「优美子数组」 的数目。
示例 1:
输入:nums = [1,1,2,1,1], k = 3
输出:2
解释:包含 3 个奇数的子数组是 [1,1,2,1] 和 [1,2,1,1] 。
示例 2:
输入:nums = [2,4,6], k = 1
输出:0
解释:数列中不包含任何奇数,所以不存在优美子数组。
示例 3:
输入:nums = [2,2,2,1,2,2,1,2,2,2], k = 2
输出:16
前缀和&哈希
因为这里的前缀和定义为奇数的个数,按照定义最多有nums.size()个,因此直接用数组代替map作哈希表
- 数组下标是「前缀和」,值是「前缀和的个数」
- 也就是这里的前缀和取值范围为[0,nums.size()],nums.size()表示整个nums数组都是奇数元素,正好是了vector (nums.size()+1)的下标范围
上一题中前缀和可以取任意和值,因此要用严格的哈希表map,不能用数组下标表示前缀和
class Solution {
public:
int numberOfSubarrays(vector<int>& nums, int k) {
vector<int> preodd(nums.size()+1);//下标代表前缀和,定义域为[0,nums.size()],值为其次数
preodd[0] = 1;
int odds = 0, ans = 0;
for(int num:nums){
odds+=num%2;
if(odds>=k) ans+=preodd[odds-k];
preodd[odds]++;
}
return ans;
}
};
滑动窗口
class Solution {
vector<int> cnt;
public:
int numberOfSubarrays(vector<int>& nums, int k) {
int odd = 0, ans = 0;
int left =0,right = 0;
while(right<nums.size()){
// 右指针先走,每遇到一个奇数则 oddCnt++。
odd+=nums[right++]%2;
// 若当前滑动窗口 [left, right) 中有k个奇数了,进入此分支统计当前窗口中优美数组个数。
if(odd == k){
// 将滑动窗口的左边界向右拓展,直到遇到下一个奇数(或出界)
// leftEvenCnt 即为第 1 个奇数左边的偶数的个数
int leftnum = 0;
while(nums[left]%2==0){
leftnum++;
left++;
}
//循环结束时left指向那个奇数,也算在优美数组中,因此再+1指向下一轮的元素
left++,leftnum++;
// 将滑动窗口的右边界向右拓展,直到遇到下一个奇数(或出界)
// rightEvenCnt 即为第 k 个奇数右边的偶数的个数
int rightnum = 1;//因为上一个right指向的奇数也算在优美数组中,因此开局就有1
while(right<nums.size()&&nums[right]%2==0){
rightnum++;
right++;
}
ans += leftnum*rightnum;
// 一定注意这一轮已经统计完了,左指针指向当前优美子数组区间「第一个奇数的下一个数」
// 也就是已经将第一个奇数消耗了(第一个奇数不在当前优美子数组区间了)
// 右指针指向当前优美子数组区间之外的下一个奇数,该奇数下一轮才会计算进来,不用管
// 由于当前优美区间第一个奇数已经不在窗口内了,因此odd--,一定别忘了
odd--;
}
}
return ans;
}
};
974. 和可被 K 整除的子数组
给定一个整数数组 nums
和一个整数 k
,返回其中元素之和可被 k
整除的(连续、非空) 子数组 的数目。
子数组 是数组的 连续 部分。
示例 1:
输入:nums = [4,5,0,-2,-3,1], k = 5
输出:7
解释:
有 7 个子数组满足其元素之和可被 k = 5 整除:
[4, 5, 0, -2, -3, 1], [5], [5, 0], [5, 0, -2, -3], [0], [0, -2, -3], [-2, -3]
示例 2:
输入: nums = [5], k = 9
输出: 0
前缀和&哈希&同余定理&负余数转正余数
判断子数组的和能否被 k 整除就等价于判断 ( P [ j ] − P [ i − 1 ] ) m o d k = = 0 (P[j]−P[i−1]) mod k==0 (P[j]−P[i−1]) mod k==0,根据 同余定理,只要 P [ j ] m o d k = = P [ i − 1 ] m o d k P[j] mod k==P[i−1] mod k P[j] mod k==P[i−1] mod k,就可以保证上面的等式成立。
因此使用哈希表存前缀和的modk余数而非其本身作为下标
因为cpp的%存在负余数,我们需要将所有负余数归到[0,k-1]
-
比如k=4,cpp中-7%4 = -3而非1(-7 = 4 * (-2)+1 || -7 = 4 * (-1) + (-3)),nums为[-7,8],则取余后的prefixmap为{{0,1},{-3,1},{1,1}}
-
这样ans = 0,实际上余数为-3和余数为1是等价的,ans = 1(8%4==0)
-
因此将负余数转成对应正余数
int modulus = (sum % k + k) % k;
-
这样余数的取值范围为[0,k-1],可以用数组代替哈希表
class Solution {
public:
int subarraysDivByK(vector<int>& nums, int k) {
vector<int> mp(k);
int ans = 0;
int sum = 0;
mp[0] += 1;//前缀和为0的数量为1
for(int num:nums){
sum+=num;
int modulus = (sum % k + k) % k;//负余数转正余数
ans+=mp[modulus];
mp[modulus]++;//由于同余定理,哈希表不存原前缀和,直接存其对k余数
}
return ans;
}
};
304. 二维区域和检索 - 矩阵不可变
给定一个二维矩阵 matrix
,以下类型的多个请求:
- 计算其子矩形范围内元素的总和,该子矩阵的 左上角 为
(row1, col1)
,右下角 为(row2, col2)
。
实现 NumMatrix
类:
NumMatrix(int[][] matrix)
给定整数矩阵matrix
进行初始化int sumRegion(int row1, int col1, int row2, int col2)
返回 左上角(row1, col1)
、右下角(row2, col2)
所描述的子矩阵的元素 总和 。
二维前缀和模板
类比一维前缀和,sum[0]指的是 「前0个数」的前缀和,边界条件为0
class NumMatrix {
vector<vector<int>> sum;
public:
NumMatrix(vector<vector<int>>& matrix) {
sum.resize(matrix.size()+1,vector<int>(matrix[0].size()+1));//i=0和j=0均为边界条件
for(int i =1;i<sum.size();i++){
for(int j = 1;j<sum[0].size();j++){
sum[i][j] = sum[i-1][j]+sum[i][j-1]+matrix[i-1][j-1]-sum[i-1][j-1];
}
}
}
int sumRegion(int row1, int col1, int row2, int col2) {
return sum[row2+1][col2+1]-sum[row2+1][col1]-sum[row1][col2+1]+sum[row1][col1];
}
};
2765. 最长交替子数组
给你一个下标从 0 开始的整数数组 nums
。如果 nums
中长度为 m
的子数组 s
满足以下条件,我们称它是一个 交替子数组 :
m
大于1
。s1 = s0 + 1
。- 下标从 0 开始的子数组
s
与数组[s0, s1, s0, s1,...,s(m-1) % 2]
一样。也就是说,s1 - s0 = 1
,s2 - s1 = -1
,s3 - s2 = 1
,s4 - s3 = -1
,以此类推,直到s[m - 1] - s[m - 2] = (-1)m
。
请你返回 nums
中所有 交替 子数组中,最长的长度,如果不存在交替子数组,请你返回 -1
。
子数组是一个数组中一段连续 非空 的元素序列。
示例 1:
输入:nums = [2,3,4,3,4]
输出:4
解释:交替子数组有 [3,4] ,[3,4,3] 和 [3,4,3,4] 。最长的子数组为 [3,4,3,4] ,长度为4 。
示例 2:
输入:nums = [4,5,6]
输出:2
解释:[4,5] 和 [5,6] 是仅有的两个交替子数组。它们长度都为 2
本题关键思路:当前最长交替子数组结束后,下一个遍历的点不用从当前起点下一个位置开始,否则成了 O ( n 2 ) O(n^2) O(n2)的双层循环暴力法,可以从当前终点的上一个点或者当前终点开始,这两个点的选取取决于当前终点与其前一个点是否满足差1而不是-1关系
我们已经知道子数组 n u m s [ f i r s t I n d e x , … , i − 1 ] nums[firstIndex,…,i−1] nums[firstIndex,…,i−1] 是满足交替子数组的条件的。分析以下情况:
如果这个子数组的长度大于等于 3 3 3,那么我们不需要从 f i r s t I n d e x + 1 firstIndex+1 firstIndex+1 开始外层循环,因为子数组 n u m s [ f i r s t I n d e x + 1 , f i r s t I n d e x + 2 ] nums[firstIndex+1,firstIndex+2] nums[firstIndex+1,firstIndex+2] 必不满足交替子数组**(前后差-1)**。
如果这个子数组的长度大于等于 4 4 4,那么我们不需要从 f i r s t I n d e x + 2 firstIndex+2 firstIndex+2 开始外层循环,因为子数组 n u m s [ f i r s t I n d e x + 2 , … , i − 1 ] nums[firstIndex+2,…,i−1] nums[firstIndex+2,…,i−1] 虽然满足交替子数组,但是这个交替数组会在 i i i 被破环,并且长度必小于 n u m s [ f i r s t I n d e x , … , i − 1 ] nums[firstIndex,…,i−1] nums[firstIndex,…,i−1]。(也即已访问数组的子数组,没有更新全局长度的意义)
通过这样分析,我们可以得出结论,外层的循环可以从 i − 1 i−1 i−1 继续。而这样的话,我们可以丢弃外层循环,在内层循环多做一个 f i r s t I n d e x firstIndex firstIndex 是否可以为 i − 1 i−1 i−1 的判断,而只保留内层循环。如果可以,则将 f i r s t I n d e x firstIndex firstIndex 更新为 i − 1 i−1 i−1,并更新最长长度。如果不可以,则将 f i r s t I n d e x firstIndex firstIndex 更新为 i i i
分组循环
适用场景:按照题目要求,数组会被分割成若干组,且每一组的判断/处理逻辑是一样的。
核心思想:
外层循环负责遍历组之前的准备工作(记录开始位置),和遍历组之后的统计工作(更新答案最大值)。
内层循环负责遍历组,找出这一组最远在哪结束。
这个写法的好处是,各个逻辑块分工明确,也不需要特判最后一组。以我的经验,这个写法是所有写法中最不容易出 bug 的,推荐大家记住。
对于本题来说,在内层循环时,假设这一组的第一个数是
3
3
3,那么这一组的数字必须形如
3
,
4
,
3
,
4
,
⋯
3,4,3,4,⋯
3,4,3,4,⋯也就是
n
u
m
s
[
i
+
1
]
−
n
u
m
s
[
i
]
=
1
nums[i+1] - nums[i] = 1
nums[i+1]−nums[i]=1
并且
n
u
m
s
[
i
]
=
n
u
m
s
[
i
−
2
]
nums[i] = nums[i-2]
nums[i]=nums[i−2]
另外,对于
[
3
,
4
,
3
,
4
,
5
,
4
,
5
]
[3,4,3,4,5,4,5]
[3,4,3,4,5,4,5] 这样的数组,第一组交替子数组为
[
3
,
4
,
3
,
4
]
[3,4,3,4]
[3,4,3,4],第二组交替子数组为
[
4
,
5
,
4
,
5
]
[4,5,4,5]
[4,5,4,5],这两组有一个数是重叠的,所以下面代码在外层循环末尾要把
i
i
i 减一。
class Solution {
public:
int alternatingSubarray(vector<int>& nums) {
int ans = 0;
int left =0;
int n = nums.size();
while(left<n-1){
if(nums[left+1]-nums[left]!=1){
left++;// 直接跳过
continue;
}
int i = left+2;// 记录这一组的开始位置,上面判断left 和 left+1 已经满足要求,从 left+2 开始判断
while(i<n&&nums[i]==nums[i-2]){
i++;
}
//从 left 到 i-1 是满足题目要求的(并且无法再延长的)子数组,所以长度为(i-1)-left+1 = i-left
ans = max(i-left,ans);
left = i-1;
}
return ans?ans:-1;
}
};
一般来说,分组循环的模板如下(可根据题目调整):
n = len(nums)
i = 0
while i < n:
start = i
while i < n and ...:
i += 1
# 从 start 到 i-1 是一组
# 下一组从 i 开始,无需 i += 1
滑动窗口
滑动窗口 = 暴力法双层循环 退化为 单层循环
单层循环体主动移动右指针 + 被动移动左指针
被动:窗口内元素不满足条件了,移动左指针到窗口内元素满足条件为止
class Solution {
public:
int alternatingSubarray(vector<int>& nums) {
int ans = -1,flip = 1;//flip表示当前应该的前后差值
int left =0,right = 1;
int n = nums.size();
while(right<n){
if(nums[right]-nums[right-1]!=flip){//不满足条件了,就判断新的左边界是否可以成为i-1
if(flip == 1){
left = right;
right++;
}else{
left = right-1;
flip = 1;
}
}
else{
flip*=-1;
ans = max(ans,right-left+1);
right++;//主动移动右边界
}
}
return ans;
}
};
713. 乘积小于 K 的子数组
给你一个整数数组 nums
和一个整数 k
,请你返回子数组内所有元素的乘积严格小于 k
的连续子数组的数目。
示例 1:
输入:nums = [10,5,2,6], k = 100
输出:8
解释:8 个乘积小于 100 的子数组分别为:[10]、[5]、[2],、[6]、[10,5]、[5,2]、[2,6]、[5,2,6]。
需要注意的是 [10,5,2] 并不是乘积小于 100 的子数组。
示例 2:
输入:nums = [1,2,3], k = 0
输出:0
滑动窗口
这题主要注意如何根据快慢指针的索引,计算出区间内的子数组种类数。这里首先要明确以下几点:
- 子数组要求连续
- 如果满足条件,子数组的所有子数组都会满足条件。
- 每次满足条件时,为了不发生重复计算,应该要求以快指针结尾的子数组进行个数计算,这样不会发生重复。 综上,根据快慢指针的索引计算的满足条件子数组个数就slow到fast的长度:fast - slow + 1
因此每次移动右指针,找到区间后,计算的是当前窗口中以右指针「结尾」的子数组数量
比如移动右指针到了窗口为 [ l , r ] [l,r] [l,r],那么以 r r r 结尾的子数组为 [ l , r ] , [ l + 1 , r ] , . . . [ r , r ] [l,r],[l+1,r],...[r,r] [l,r],[l+1,r],...[r,r],也就是这个窗口长度的数值: r − l + 1 r-l+1 r−l+1
以右指针结尾的子数组数量计算正确性:依赖于滑动窗口的单调性,当前窗口中不以右指针结尾的子数组乘积必然小于 [ l , r ] [l,r] [l,r]窗口的乘积,而更新全局子数组数的操作是与每轮移动右指针同步的,因此以当前右指针前面的元素结尾的子数组数量,已经在前面的轮次计算过了,没有被漏计算
比如 [ 3 , 1 , 4 , 2 ] [3,1,4,2] [3,1,4,2],最大乘积为就为 [ 3 , 1 , 4 , 2 ] [3,1,4,2] [3,1,4,2]对应(以2结尾)的乘积,以 3 , 1 , 4 3,1,4 3,1,4结尾的子数组乘积小于该值(单调性),而以 3 , 1 , 4 3,1,4 3,1,4结尾的子数组数量已经在前面的循环中被计算了(每次移动右指针,都计算子数组数量, 3 , 1 , 4 3,1,4 3,1,4都被右指针访问过),不用担心漏,这轮只需要计算以右指针 2 2 2结尾的子数组数即可
class Solution {
public:
int numSubarrayProductLessThanK(vector<int>& nums, int k) {
int ans = 0;
int left = 0,right = 0;
int cal = 1;
while(right<nums.size()){
cal*=nums[right];
while(left<=right&&cal>=k) cal/=nums[left++];
ans+=right-left+1;//以右指针「结尾」的子数组数量
right++;
}
return ans;
}
};
567. 字符串的排列
示例 1:
输入:s1 = "ab" s2 = "eidbaooo"
输出:true
解释:s2 包含 s1 的排列之一 ("ba").
示例 2:
输入:s1= "ab" s2 = "eidboaoo"
输出:false
滑动窗口(c++map删除元素语法)
map的for循环中如何删除元素?第一次写成如下
//报错
for(auto e:mp){
if(e.second==0) mp.erase(e.first);
}
不能在range-for的循环体中改变遍历的容器的大小,即不允许遍历的同时添加或删除元素!
因为对于某些容器,向容器中添加或删除元素会导致迭代器失效,因此后续遍历操作都是未定义的。
而range-based for是基于迭代器的语法糖,因此不能在遍历过程中直接增减元素
range-for底层实现时预存了容器的end()值,而一旦遍历的时候向该容器添加或删除元素,就会使该预存的end()失效,由上述迭代器失效的问题,就不难明白:range-for的循环体中不允许对该容器添加或删除元素!
https://blog.csdn.net/hechao3225/article/details/54982530
https://blog.csdn.net/kksc1099054857/article/details/114978169
上面的e被erase后失效,for循环中对e操作结果都是不可预料的,正确写法如下
for(auto it = mp.begin();it!=mp.end();){
if(it->second==0) it = mp.erase(it);//不可以写作mp.erase(it->first);返回值为int
else it++;
}
//或者
for(auto it = mp.begin();it!=mp.end();){
if(it->second==0) mp.erase(it++);
else it++;
}
将erase的返回值(下一个迭代器)赋值给新的it,注意erase中必须传入迭代器参数而非值,否则返回值为int而非下一个迭代器
或者
新写法的迭代器的自增从for头部中取出,放在循环体中。it++返回了自增前的迭代器的一个临时拷贝。
然后这个临时迭代器指向的内容被删除了,但是it本身已经自增到下一个位置了,不受影响。
同时注意迭代器遍历自增写法
class Solution {
public:
bool checkInclusion(string s1, string s2) {
if(s1.length()>s2.length()) return false;
unordered_map<char,int> mp;
int right = 0;
for(;right<s1.length();right++){
mp[s1[right]]++;
mp[s2[right]]--;
}
right--;
for(auto it = mp.begin();it!=mp.end();){
if(it->second==0) it = mp.erase(it);//不可以写作mp.erase(it->first);返回值为int
else it++;
}
int l = s1.length();
while(right<s2.length()){
if(mp.size() == 0) return true;
int left = right-l+1;
right++;
if(++mp[s2[left]]==0) mp.erase(s2[left]);
if(--mp[s2[right]]==0) mp.erase(s2[right]);
}
return false;
}
};
30. 串联所有单词的子串
给定一个字符串 s
和一个字符串数组 words
。 words
中所有字符串 长度相同。
s
中的 串联子串 是指一个包含 words
中所有字符串以任意顺序排列连接起来的子串。
- 例如,如果
words = ["ab","cd","ef"]
, 那么"abcdef"
,"abefcd"
,"cdabef"
,"cdefab"
,"efabcd"
, 和"efcdab"
都是串联子串。"acdbef"
不是串联子串,因为他不是任何words
排列的连接。
返回所有串联子串在 s
中的开始索引。你可以以 任意顺序 返回答案。
示例 1:
输入:s = "barfoothefoobarman", words = ["foo","bar"]
输出:[0,9]
解释:因为 words.length == 2 同时 words[i].length == 3,连接的子字符串的长度必须为 6。
子串 "barfoo" 开始位置是 0。它是 words 中以 ["bar","foo"] 顺序排列的连接。
子串 "foobar" 开始位置是 9。它是 words 中以 ["foo","bar"] 顺序排列的连接。
输出顺序无关紧要。返回 [9,0] 也是可以的。
多起点滑动窗口
将滑动窗口找字母异位词的字母换成了字符串,也就是找严格的字符串异位子串,
字符串等长,但是待切分的原串s并没有从下标0起始按照等长单词划分
比如 s = “abacdabd”,words = [“ab”,“cd”]
如果窗口从0开始滑动,过程判断了“abac”, “acda”, “dabd”,忽略了下标为4的"cdab",窗口从1开始滑动就覆盖了这种情况
因此多加一步:从下标[0,wordlen-1]这个区间的下标都起始滑动一遍,步长为wordlen,这样子每一遍的滑动都退化为求字母异位词(用哈希表将字符串映射为int和将字母映射为int的过程是一样的),如果从wordlen开始,也就相当于从0开始滑了一轮步长,没有意义,因此外层循环选窗口滑动起点的枚举范围是[0,wordlen-1]
类比取余
class Solution {
public:
vector<int> findSubstring(string s, vector<string>& words) {
int unitlen = words[0].length();
int targetlen = words.size()*unitlen;
if(s.length()<unitlen) return {};
unordered_map<string,int> mp;
vector<int> ans;
for(auto str:words) mp[str]++;
int differ = mp.size();
int left,right;
for(int i=0;i<unitlen;i++){//因原字符串无序特性,多起点滑动窗口,类比取余,外层多加循环
left = right = i;
while(right<s.length()+1){
if(differ == 0) ans.push_back(left);
if(right-left == targetlen) {//左右指针距离到达窗口长度,移动左指针维护单词哈希
if(++mp[s.substr(left,unitlen)]==0) differ--;
else if(mp[s.substr(left,unitlen)]==1) differ++;
left+=unitlen;
}
if(!mp.count(s.substr(right,unitlen))){//遇到没有在words出现过的单词,直接剪枝
while(left<right){
mp[s.substr(left,unitlen)]++;
left+=unitlen;
}
left = right = right+unitlen;
differ = mp.size();
}
else{//否则每轮都按照步长移动右指针,同时维护单词哈希
if(--mp[s.substr(right,unitlen)]==0) differ--;
else if(mp[s.substr(right,unitlen)]==-1) differ++;
right+=unitlen;
}
}
}
return ans;
}
};
424. 替换后的最长重复字符
给你一个字符串 s
和一个整数 k
。你可以选择字符串中的任一字符,并将其更改为任何其他大写英文字符。该操作最多可执行 k
次。
在执行上述操作后,返回 包含相同字母的最长子字符串的长度。
示例 1:
输入:s = "ABAB", k = 2
输出:4
解释:用两个'A'替换为两个'B',反之亦然。
示例 2:
输入:s = "AABABBA", k = 1
输出:4
解释:
将中间的一个'A'替换为'B',字符串变为 "AABBBBA"。
子串 "BBBB" 有最长重复字母, 答案为 4。
可能存在其他的方法来得到同样的结果。
滑动窗口
本题滑动窗口平衡式为:right - left(窗口长度) - max(出现次数最多的字符的出现次数)= other(其他字符的出现次数) <= k
。
本题进阶在于:滑动窗口有两种: 一种是right每次往右走,只要不满足条件,left就一直收敛。 另一种是,right每次往右走,如果不满足条件,left最多收敛一次(进阶)。
- 只收敛一次是因为比当前winSize小的情况没必要考虑。
- 要
right
的值加进去不满足条件,left
和right
就一起右滑,因为长度小于right - left
的区间就没必要考虑了,所以right - left
一直保持为当前的最大值 - 使得窗口长度 right-left 是只增不减的
- 第二种情况在求最长区间的时候可以用到
为什么不满足平衡式的时候,左指针右移一次不用维护maxn,maxn只在右指针移动时维护?
- maxn并不是当前窗口内出现次数最多字母的次数,而是历史窗口中出现次数最多字母的次数。
- 因为找到一个满足条件的窗口之后,接下来只需要去找一个更大的窗口, 之前找到的那个窗口的大小 = right - left = maxn+ k,所以在k固定不变的前提下,要找更大的那个窗口,也就等价于找能够使得maxn增大的新的窗口。
- 也就是说在改变左窗口端点之后,不用管原来的maxn是否变化,因为无论怎么变小,窗口最大值都不会变大,判断新加入的字母的个数是否能超过maxn(因为只有新加入来的字母才有可能使得最终的maxn增大)。
总结:合法窗口长度 = maxn+k,左指针需要右移的时候,k必然是顶格的(所有的k都用上了),不能再通过使用k来增长,此时获得更长窗口的唯一途径是增加maxn,只能通过右指针右移获得,
因此左指针右移只需维护frequency正确性即可,这样间接维护了maxn正确性
至于左指针移动一次而非移动到窗口严格满足条件(if而非while)的原因:right移动前窗口是满足条件的,移动后不满足了,因此left右移一格后,还是right移动前的满足条件窗口长度,只需考虑后面有没有比该值大的长度即可,不用考虑更小的窗口,因此left不用移到窗口严格满足条件
class Solution {
public:
int characterReplacement(string s, int k) {
int right = 0,left = 0;
int maxever = 0, ans = 0;
int frequency[26];
memset(frequency,0,sizeof(frequency));
while(right<s.length()){
maxever = max(maxever,++frequency[s[right++]-'A']);//更新历史的maxn
if(right-left-maxever>k){//如果窗口不平衡了
frequency[s[left++]-'A']--;//左指针移动,维护frequency即间接维护maxn(下一轮r)
}
//ans = max(ans,right-left);
}
return right-left;//ans,长度是只增不减的,因此不用每次更新ans了
}
};
34. 在排序数组中查找元素的第一个和最后一个位置
给你一个按照非递减顺序排列的整数数组 nums
,和一个目标值 target
。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target
,返回 [-1, -1]
。
你必须设计并实现时间复杂度为 O(log n)
的算法解决此问题。
示例 1:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
示例 2:
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
示例 3:
输入:nums = [], target = 0
输出:[-1,-1]
二分查找(红蓝染色法)
https://www.bilibili.com/video/BV1AP41137w7/?spm_id_from=333.788&vd_source=9127d0ec8fa2d871e0c484a298e38bde
背下来找到有序数组nums中第一个大于等于给定target的下标写法后,这道题的start也就直接传入函数解决了,因为start是该元素第一次出现的位置
end为最后一次出现的位置,转换成其下一个元素(不用管是否存在,必然会返回其下一个位置的下标)第一次出现下标-1
就如上图,
找第一个大于target的下标 -> 找第一个大于等于target+1的下标
找第一个小于target的下标 -> 找第一个大于等于target的下标-1
找第一个小于等于target的下标 -> 找第一个大于等于target+1的下标-1
关键点:
-
由于left,right的维护满足**循环不变量(切片)**特性:left指针(不包含)左区间恒小于target,right(不包含)右区间恒大于等于target,[left,right]区间(包含)为未知大小关系,
使得无论target是否在nums中出现,都一定能返回 第一个 >= target的元素下标:left/right+1(左闭右闭)
- 比如为什么返回left?因为**循环不变量(切片)**的维护保证了left左侧(不包含)必为 < target的元素,那么结束时left所指元素必为 >= target的元素
- 同理right右侧(不包含)必为 >= target的元素,那么选取第一个就是 right+1
- 如果变成左闭右开区间,right指针处的数值并不在未知区间内,定义为已知量(>= target),实际上就是把“right**(不包含)右区间恒大于等于target”这一描述改为“right(包含)右区间恒大于等于target”,[left,right)区间是不确定大小关系的**,因此right初始值也变成取不到的nums.size(),每次转移时并非 right = mid - 1 而是 right = mid
-
循环结束条件实质上为搜索区间为空,表现出来就是left,right越界,如闭区间中的left>right或者左开右闭中的left >= right
-
由于循环不变量的维护,nums所有元素 < target时候,left结束为nums.size(),target不存在或者所有元素 > target的时候,返回的left下标对应值不是target,因此对于这道题,只需判断这两个条件即可得知严格等于target的元素
if(start == nums.size()||nums[start]!=target) return {-1,-1};//注意这里的判断
求中点下标防止溢出的写法:
(right - left) / 2 +left
class Solution {
public:
// lower_bound 返回「最小的」满足 nums[i] >= target 的 i
// 如果数组为空,或者所有数都 < target,则返回 nums.size()
// 如果所有数都 > target,返回nums[0],该数不等于target
// 要求 nums 是非递减的,即 nums[i] <= nums[i + 1]
// 闭区间写法
int lower_bound(vector<int>& nums, int target){
int left = 0, right = nums.size()-1;
while(left<=right){// 区间不为空
// 循环不变量:无论怎么更新,left指针左区间恒小于target,right右区间恒大于等于target
// 中间区间未知与target大小关系
// nums[left-1] < target
// nums[right+1] >= target
int mid = (right - left)/2 + left;// 注意防止溢出的写法
if(nums[mid] < target) left = mid + 1;// 范围缩小到 [mid+1, right]
else right = mid - 1;// 范围缩小到 [left, mid-1]
}
return left;//return right+1;这里left即为right+1
}
int lower_bound_openrightbound(vector<int>& nums, int target){
int left = 0, right = nums.size();
while(left<right){
// 循环不变量:
// nums[left-1] < target
// nums[right] >= target 由于开区间,right指针处的数值并不在未知区间内,定义为已知量
int mid = (right - left)/2 + left;
if(nums[mid] < target) left = mid + 1;
else right = mid;
}
return left;//return right;// 返回 left 还是 right 都行,因为循环结束后 left == right
}
int lower_bound_openbothbounds(vector<int>& nums, int target){
int left = -1, right = nums.size();//左右开区间
while(left+1<right){//注意这里的循环终止条件:(l,r)什么时候为空区间?
// 循环不变量:
// nums[left] < target
// nums[right] >= target 由于开区间,right指针处的数值并不在未知区间内,定义为已知量
int mid = (right - left)/2 + left;
if(nums[mid] < target) left = mid ;
else right = mid;
}
return left+1;//return right;
}
vector<int> searchRange(vector<int>& nums, int target) {
int start = lower_bound(nums,target);
if(start == nums.size()||nums[start]!=target) return {-1,-1};//注意这里的判断
int end = lower_bound_openrightbound(nums,target+1)-1;
return {start,end};
}
};