数据结构与算法总结2(个人原创,带详细注释代码)

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. 处理跳过,也就是有比自己当前数值小1的就直接过,优化性能
  2. 输入数据存在大量重复的情况,遍历的时候去遍历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 x1,不然按照上面的分析我们会从 x − 1 x−1 x1 开始尝试匹配,因此我们每次在哈希表中检查是否存在 x − 1 x−1 x1 即能判断是否需要跳过了。

增加了判断跳过的逻辑之后,时间复杂度是多少呢?外层循环需要 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[i1])modk==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]modk==P[i1]modk,就可以保证上面的等式成立。

因此使用哈希表存前缀和的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 = 1s2 - s1 = -1s3 - s2 = 1s4 - 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,,i1] 是满足交替子数组的条件的。分析以下情况:

如果这个子数组的长度大于等于 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,,i1] 虽然满足交替子数组,但是这个交替数组会在 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,,i1](也即已访问数组的子数组,没有更新全局长度的意义)

通过这样分析,我们可以得出结论,外层的循环可以从 i − 1 i−1 i1 继续。而这样的话,我们可以丢弃外层循环,在内层循环多做一个 f i r s t I n d e x firstIndex firstIndex 是否可以 i − 1 i−1 i1 的判断,而只保留内层循环。如果可以,则将 f i r s t I n d e x firstIndex firstIndex 更新为 i − 1 i−1 i1,并更新最长长度。如果不可以,则将 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[i2]
另外,对于 [ 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
滑动窗口

这题主要注意如何根据快慢指针的索引,计算出区间内的子数组种类数。这里首先要明确以下几点:

  1. 子数组要求连续
  2. 如果满足条件,子数组的所有子数组都会满足条件。
  3. 每次满足条件时,为了不发生重复计算,应该要求以快指针结尾的子数组进行个数计算,这样不会发生重复。 综上,根据快慢指针的索引计算的满足条件子数组个数就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 rl+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 的值加进去不满足条件,leftright就一起右滑,因为长度小于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

关键点:

  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
  2. 循环结束条件实质上为搜索区间为空,表现出来就是left,right越界,如闭区间中的left>right或者左开右闭中的left >= right

  3. 由于循环不变量的维护,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};
    }
};
  • 14
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值