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即可.
当我们了解了同余定理之后, 这道题其实就迎刃而解了;
下面的这个解法是如何利用同余定理的呢?
- 首先同余定理正反理论是等价的, 即 (a-b) / k , a - b 能够整除 k, 那么a和b对k 取模的余数是相同的, 反之也是成立的,
(a和b分别对k取模的余数相同, 那么a-b就能够整除k)
- 下面的解法实际上是利用了同余定理的反向理论, 我们首先求出数组的前缀和, 但是吧
数组的子数组和是两个前缀和的减法得到的
, 如果我们执意去求子数组的和, 岂不就成了暴力解法了?- 所以下面的解法是反其道而行之, 我不求子数组和是够能够整除k, 而是遍历前缀和数组求出每个前缀和的余数, 我比较不同前缀和余数,
如果两个前缀和a, b 对 k的余数相同
,他不就说明前缀和的减法(正是子数组和)能够整除k了吗北鼻
;- 所以, 我们求出前缀和数组, 然后遍历前缀和数组, 求出每个前缀和的余数, 并把余数和这个前缀和的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;
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律