17.<tag-数组和前缀和, 同余定理, 哈希表>-lt.523-连续的子数组和 + lt.525-连续数组 1.2

解答下面两题的话, 那必须得知道前缀和是什么, 怎么用呀, 点我

lt.523-连续的子数组和

[案例需求]
在这里插入图片描述

[思路分析一, 使用标准的前缀和求解]

  • 思路跟上面提到的笔者的旧文基本一致, 先用一个前缀和方法, 对数组遍历, 并把每个nums[i] + preSum[i - 1] 也就是每一个index 的前缀和进行求和, 返回前缀和数组 preSum;
  • 对前缀和数据进行双重for循环遍历, 要求外层i内层j的差值在2以上, 因为题目要求每个子数组长度大于2嘛, 如果遇到某个i到j之间的子数组的值 % k == 0, 那么直接返回true即可.
  • 时间复杂度分析: 构建前缀和数组经历一次for循环, m; 双重for循环 n^2, 所以总的时间复杂度为 O(n^2 + m), 提交后超时了, 害, 就卡我最后一个用例是吧, 小力抠

在这里插入图片描述

1.前缀和初级解法

class Solution {
    public boolean checkSubarraySum(int[] nums, int k) {
        //
        int[] preSum = prefixSum(nums);
        int len = preSum.length;

        for(int i = 0; i < len - 2; i++){
            for(int j = i + 2; j < len; j++){
                if((preSum[j] - preSum[i]) % k == 0)return true;
            }
        }

        return false;
    }

    //前缀和
    public int[] prefixSum(int[] nums){
        int[] preSum = new int[nums.length + 1];
        
        preSum[0] = 0;

        for(int i = 1; i < preSum.length; i++){
            preSum[i] = preSum[i - 1] + nums[i - 1];
        }

        return preSum;
    }
}

第二种写法

class Solution {
    public boolean checkSubarraySum(int[] nums, int k) {
        int len = nums.length;
        if(len <= 1)return false;

        int[] preSum = new int[len + 1];
        preSum[0] = 0;

        for(int i = 1; i < len + 1; i++){
            preSum[i] = preSum[i - 1] + nums[i - 1];
        }

        //暴力法
        for(int i = 0; i < len + 1; i++){
            for(int j = 0; j < len + 1; j++){
                if(Math.abs(i - j) >= 2 && (preSum[i] - preSum[j]) % k == 0){
                    System.out.println(i + "-" + j);
                    System.out.println(preSum[i] + "-" + preSum[j]);
                    return true;
                }
            }
        }
        return false;
    }
}

[思路分析二, 前缀和 + 哈希表]

本思路的解题关键就在于对同余定理 的应用

什么是同余定理? ==> 给定一个正整数m,如果两个整数a和b满足a-b能够被m整除,即(a-b)/m得到一个整数,那么就称整数a与b对模m同余,记作a≡b(mod m)。对模m同余是整数的一个等价关系。
即, (a -b) / m 能够整除的话, 那么 a和b对m取余, 他俩的余数一定是相等的!


了解同余定理之后, 我们再来思考这道题, 题目要求 (a- b) / k 能够整除, a 和 b 都是数组中多个元素的和(子数组和), 当整除后, 要求a和b的长度差距在2以上;

所以我们可以遍历数组, 把数组中的每个子数组的和 % k的余数存储到哈希表中(<余数, 下标>), 然后一边比较集合中是否存在该余数, 存在的话再去比较下标的差值, 差距在2上则返回true即可.
当我们了解了同余定理之后, 这道题其实就迎刃而解了;

下面的这个解法是如何利用同余定理的呢?

  1. 首先同余定理正反理论是等价的, 即 (a-b) / k , a - b 能够整除 k, 那么a和b对k 取模的余数是相同的, 反之也是成立的, (a和b分别对k取模的余数相同, 那么a-b就能够整除k)
  2. 下面的解法实际上是利用了同余定理的反向理论, 我们首先求出数组的前缀和, 但是吧数组的子数组和是两个前缀和的减法得到的, 如果我们执意去求子数组的和, 岂不就成了暴力解法了?
  3. 所以下面的解法是反其道而行之, 我不求子数组和是够能够整除k, 而是遍历前缀和数组求出每个前缀和的余数, 我比较不同前缀和余数, 如果两个前缀和a, b 对 k的余数相同, 他不就说明前缀和的减法(正是子数组和)能够整除k了吗北鼻;
  4. 所以, 我们求出前缀和数组, 然后遍历前缀和数组, 求出每个前缀和的余数, 并把余数和这个前缀和的index存入HasMap, 找到相同余数的两个前缀和, 并比较index差值 >= 2 即可;
class Solution {
    public boolean checkSubarraySum(int[] nums, int k) {
       //同余定理, a % k == b % k, (a - b) % k = 整数
       //对于本题, 目的是求前缀个 % k == 整数
       //1. 求出前缀和
       //2. 每个前缀和都 % k, 把得到的余数存入到HashMap, <余数, 下标> 
       Map<Integer, Integer> map = new HashMap<>();
       
       int len = nums.length;
       int[] preSum = new int[len + 1];
      
       for(int i = 1; i < len + 1; i++){
           preSum[i] = nums[i - 1] + preSum[i - 1];     
       }

     
       //遍历前缀和数组, 求出每个前缀和的余数, 遇到相同的, 检查下标差距 >= 2否
        for(int i = 0; i < len + 1; i++){
            if(map.containsKey(preSum[i] % k)){
               int index = map.get(preSum[i] % k);
               if(Math.abs(index - i) >= 2)return true;
           }else{
               map.put(preSum[i] % k , i);
           }  
        }
        return false;   
    }
}
/*
前缀数组sum,sum[i]表示前i个元素的和。
子数组nums[i..j]的和 subNum = sum[j+1]-sum[i];
※同余定理: subNum % k == 0,等价于 sum[j+1] % k == sum[i] % k !!!(j>i)
 */
class Solution {
    public boolean checkSubarraySum(int[] nums, int k) {
        int[] sum = new int[nums.length+1];
        for (int i=1; i<sum.length; ++i){
            sum[i] = sum[i-1]+nums[i-1];
        }

        HashMap<Integer, Integer> mod = new HashMap();  // 保存余数对应的下标
        for (int i=0; i<sum.length; ++i){
            int sumMod = sum[i]%k;
            if (mod.containsKey(sumMod) && i>mod.get(sumMod)+1) return true;
            else if (!mod.containsKey(sumMod)) mod.put(sumMod, i);   // 只在不存在key时更新,保证子数组长度尽可能大。
        }
        return false;
    }
}

简洁但稍难理解的解法

class Solution {
    public boolean checkSubarraySum(int[] nums, int k) {
        int m = nums.length;
        if (m < 2) {
            return false;
        }
        Map<Integer, Integer> map = new HashMap<Integer, Integer>();
        map.put(0, -1);
        int remainder = 0;
        for (int i = 0; i < m; i++) {
            remainder = (remainder + nums[i]) % k;
            if (map.containsKey(remainder)) {
                int prevIndex = map.get(remainder);
                if (i - prevIndex >= 2) {
                    return true;
                }
            } else {
                map.put(remainder, i);
            }
        }
        return false;
    }
}

[案例需求]

lt.525-连续数组

[案例需求]
在这里插入图片描述

[思路分析]
在这里插入图片描述

newNumbers 就是 -1和1的组成
counter 存储newNums的前缀和
preIndex第一次出现的下标

HashMap 存储的是 <前缀和, 索引>
当counter(记录的是前缀和) 和 HashMap的前缀和相同时, 说明子数组和为0了, 把索引长度求出来更新即可;
[代码实现]

class Solution {
    public int findMaxLength(int[] nums) {
        //数组中含有0, 1, 目的是求相同数量的0和1的子数组,  求出这些子数组中最长的一个长度
        // 我们可以把0, 转化为 -1, 这样数组中只有-1和1, 这样的话子数组0和1数量相同时, 他的和必为0;
        //综上我们可以把这道题转为求数组中和为0的连续数组的最大长度, 
        // 由于长度不一定, 我们可以把这道题化为求前缀和的问题, 并在每个前缀和记录下对应的index
        //这就需要使用到HashMap (前缀和, index)
        //对于这个数组, 我们不需要把他转换为-1和1的数组, 只需要维护一个counter, 遇到1就+1, 遇到0就减少1;
        // 当然了这个counter在集合中被初始化为 (0, -1)
        int counter = 0; //记录前缀和
        int index = -1; //记录前缀和的index
        Map<Integer, Integer> map = new HashMap<>();

        map.put(counter, index);

        int res = 0;

        for(int i = 0; i < nums.length; i++){
            //如果map中有前缀和0(key), 立马找出他的index跟前一次为0的index进行相减, 得出这一次前缀和为0的长度
            //记录遇到的长度, 每次记录下最大的长度
            if(nums[i] == 0){
                --counter;
            }else{
                ++counter;
            }

            //注意, i是当前前缀和为0的位置, map中counter的value是前缀和为0的位置(在i的前一次)
            if(map.containsKey(counter)){
                res = Math.max(res, i - map.get(counter));
            }else{
                map.put(counter, i);
            }
        }

        return res;
    }
}
posted @   青松城  阅读(29)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示