标签5连续子数组的和(前缀和)
首先需要理清楚两个概念子序列和子数组
子序列:数组中不连续的某一段数字,但是要保持原先的先后顺序(n>...>k>j>0)
A[i],A[i+j],A[i+k],...,A[i+n]
子数组:数组中连续的某一段数字,例如
A[i],A[i+1],A[i+2],...,A[i+n]A[i],A[i+1],A[i+2]
这道题是对子数组求和,因此我们可以考虑用前缀和(preSum)来表示A[i],...,A[i+n]这段子数组的和
前缀和概念:
preSum[i]:表示的是A[0]+A[1]+A[2]+...+A[i]的和(这里定义是左闭右闭)
523. 连续的子数组和
解法:对于子数组 nums[i + 1:j] (不包含下标 j ),其区间和为 sum[j]−sum[i](其中 sum 为预处理得到的前缀和数组),
我们要判断的是 (sum[j]−sum[i])%k是否等于 0。
根据 mod 运算的性质,我们知道 (sum[j]−sum[i])%k=sum[j]%k−sum[i]%k
故若想 (sum[j]−sum[i])%k=0,则必有 sum[j]%k=sum[i]%ksum[j]\%k = sum[i]\%k。
算法:每当我们计算出一个前缀和 sum[j] 时,我们判断哈希表中是否存在键值为 sum[j]%k,若存在则有 sum[j]%k=sum[i]%k,我们返回 True。
map<int, int>
保存 <前缀和对k的取余, 第一次出现 sum%k 的位置>
public boolean checkSubarraySum(int[] nums, int k) {
if(nums.length == 0) {
return false;
}else {
HashMap<Integer, Integer> map = new HashMap<>();
//map的key是前缀和对k取余,value是前缀和的截止下标
map.put(k == 0 ? nums[0] : nums[0] % k, 0);
int[] array = new int[nums.length];
array[0] = nums[0];
for(int i = 1;i < nums.length;i ++){
array[i] = array[i - 1] + nums[i];
int key = k == 0 ? array[i] : array[i] % k;
//1.如果array[i] - array[j] == 0并且i - j大于等于2符合题意,子数组是下标j+1到i
//2.如果key == k符合题意,子数组是从下标0到i
//3.key == 0,符合题意,子数组是从下标0到i
if((map.containsKey(key) && i - map.get(key) >= 2) || key == k || key == 0){
return true;
}else if(! map.containsKey(key)){
//贪心思想,如果有key就不put,也就是存储i最小的key
map.put(key, i);
}
}
return false;
}
}
525. 连续数组
解法:思路和上面的一致,把0看作-1,题目转化为连续子数组和为0的问题。
class Solution {
//规定nums数组只能是0或者1
public int findMaxLength(int[] nums) {
if(nums.length <= 1) {
return 0;
}else {
HashMap<Integer, Integer> map = new HashMap<>();
int[] sum = new int[nums.length];
sum[0] = nums[0] == 0 ? -1 : 1;
map.put(sum[0], 0);
int length = 0;
for(int i = 1;i < nums.length;i ++){
sum[i] = sum[i - 1] + (nums[i] == 0 ? -1 : nums[i]);
if(sum[i] == 0){
//[0,i]区间和为0
length = i + 1 > length ? i + 1 : length;
}
if(map.containsKey(sum[i])){
//[j+1,i]区间和为0
length = (i - map.get(sum[i])) > length ? i - map.get(sum[i]) : length;
}else if(! map.containsKey(sum[i])){
//贪心思想,如果有key就不put,也就是存储i最小的key
map.put(sum[i], i);
}
}
return length;
}
}
}
560. 和为K的子数组
解法如下:
public int subarraySum(int[] nums, int k) {
if(nums.length == 0) {
return 0;
}else {
int[] sum = new int[nums.length];
HashMap<Integer, LinkedList<Integer>> map = new HashMap<>();
sum[0] = nums[0];
int counter = 0;
LinkedList<Integer> list = new LinkedList<>();
list.add(0);
map.put(sum[0], list);
if(sum[0] == k){
counter ++;
}
for(int i = 1;i < nums.length;i ++) {
sum[i] = sum[i - 1] + nums[i];
if(map.containsKey(sum[i] - k)) {
counter += map.get(sum[i] - k).size();
}
//不是elseif,因为两种情况都可能发生
if (sum[i] == k) {
counter += 1;
}
//将sum[i]作为关键字加入map的list中
if(map.containsKey(sum[i])) {
map.get(sum[i]).add(i);
}else {
//LinkedList<Integer> list2 = new LinkedList<>();
map.put(sum[i], new LinkedList<Integer>());
map.get(sum[i]).add(i);
}
}
return counter;
}
}
1248. 统计「优美子数组」
解法:前缀和对应的map的key是前缀和的值,value是该值出现的次数或者出现的下标都可以
public int numberOfSubarrays(int[] nums, int k) {
if(nums.length == 0) {
return 0;
}else {
int[] sum = new int[nums.length];
//这里的map的key是前缀和,value存放的不是之前的i也就是前缀和对应的下标,而是前缀和出现的次数
HashMap<Integer, Integer> map = new HashMap<>();
sum[0] = nums[0] % 2 == 0 ? 0 : 1;
map.put(sum[0], 1);
int counter = 0;
if(sum[0] == k){
counter ++;
}
for(int i = 1;i < nums.length;i ++) {
sum[i] = sum[i - 1] + (nums[i] % 2 == 0 ? 0 : 1);
if(map.containsKey(sum[i] - k)) {
counter += map.get(sum[i] - k);
}
//这里是ifif的关系而不是elseif
if(sum[i] == k) {
//只能counter ++,而不是加上sum[i]出现的次数,因为重复
counter ++;
}
if(map.containsKey(sum[i])) {
//put的key是sum[i]
map.put(sum[i], map.get(sum[i]) + 1);
}else {
map.put(sum[i], 1);
}
}
return counter;
}
}
public int numberOfSubarrays1(int[] nums, int k) {
if(nums.length == 0) {
return 0;
}else {
int[] sum = new int[nums.length];
HashMap<Integer, LinkedList<Integer>> map = new HashMap<>();
sum[0] = nums[0] % 2 == 0 ? 0 : 1;
LinkedList<Integer> tem = new LinkedList<>();
tem.add(0);
map.put(sum[0], tem);
int counter = 0;
if(sum[0] == k){
counter ++;
}
for(int i = 1;i < nums.length;i ++) {
sum[i] = sum[i - 1] + (nums[i] % 2 == 0 ? 0 : 1);
if(map.containsKey(sum[i] - k)) {
counter += map.get(sum[i] - k).size();
}
//这里是ifif的关系而不是elseif
if(sum[i] == k) {
counter ++;
}
if(map.containsKey(sum[i])) {
map.get(sum[i]).add(i);
}else {
LinkedList<Integer> list = new LinkedList<>();
list.add(i);
map.put(sum[i], list);
}
}
return counter;
}
}
53. 最大子序和
时间复杂度的要求是O(n)
解法1:动态规划的解法,空间复杂度不需要O(n),只需一个sum变量记录当前连续子数组的值即可。
class Solution {
//动态规划大的解法
public int maxSubArray(int[] nums) {
if(nums.length == 0) {
return 0;
}else if(nums.length == 1){
return nums[0];
}else {
//注意题目中的描述是连续子数组的和
int answer = Integer.MIN_VALUE;
//sum标识包含nums[i]的连续子数组的和的最大值
int sum = 0;
for(int i = 0;i < nums.length;i ++) {
if(sum < 0) {
sum = nums[i];
}else {
sum += nums[i];
}
answer = Math.max(answer,sum);
}
return answer;
}
}
}
class Solution {
public int maxSubArray(int[] nums) {
if(nums.length == 0) {
return 0;
}else if(nums.length == 1) {
return nums[0];
}else {
return divideandrule(nums,0,nums.length - 1);
}
}
public int divideandrule(int[] nums,int left,int right) {
if(left == right) {
return nums[left];
}else {
int mid = left + (right - left) / 2;
//[left,mid]的连续子数组的最大值
int leftnum = divideandrule(nums,left,mid);
//[mid + 1,right]的连续子数组的最大值
int rightnum = divideandrule(nums,mid + 1,right);
//[left,right]区间,包括nums[mid],nums[mid + 1]在内的连续子数组的最大值
int midnum = merge(nums,left,right,mid);
return Math.max(midnum,Math.max(leftnum,rightnum));
}
}
//nums的连续子数组必须要包含nums[mid]和nums[mid + 1]
public int merge(int[] nums,int left,int right,int mid) {
/*
int leftnum = 0;
int rightnum = 0;
*/
int leftnum = Integer.MIN_VALUE;
int rightnum = Integer.MIN_VALUE;
int sum = 0;
//
for(int i = mid;i >= left;i --) {
sum += nums[i];
if(leftnum < sum) {
leftnum = sum;
}
}
sum = 0;
for(int i = mid + 1;i <= right;i ++) {
sum += nums[i];
if(rightnum < sum) {
rightnum = sum;
}
}
return leftnum + rightnum;
}
}
1524. 和为奇数的子数组数目
class Solution {
public int numOfSubarrays(int[] arr) {
if(arr.length == 0) {
return 0;
}else {
//o代表当前前缀和奇数的数目之和
int o = 0;
//0看作一个偶数
//e代表当前前缀和偶数的数目之和
int e = 1;
int sum = 0;
int answer = 0;
//下面两个是一样的,Math.pow()得强制类型转换
//int number = 1000000007;
int number = (int)Math.pow(10,9) + 7;
//奇数减去偶数等于奇数,偶数减去奇数等于奇数
for(int i = 0;i < arr.length;i ++) {
sum += arr[i];
if(sum % 2 == 0) {
answer = (o + answer) % number;
e ++;
}else {
answer = (e + answer) % number;
o ++;
}
}
return answer;
}
}
}
区间问题
986. 区间列表的交集
解法:双指针合并区间
class Solution {
public int[][] intervalIntersection(int[][] A, int[][] B) {
List<int[]> list = new ArrayList<>();
if(B.length == 0 && A.length == 0) {
return list.toArray(new int[list.size()][2]);
}else {
int i = 0;
int j = 0;
int left;
int right;
//很明显循环进行的条件是i,j都合法
while(i < A.length && j < B.length) {
left = Math.max(A[i][0],B[j][0]);
right = Math.min(A[i][1],B[j][1]);
//能形成区间
if(left <= right) {
list.add(new int[]{left,right});
}
//A[i]B[j]合并之后,A[i]B[j + 1]或者A[i + 1]B[j]都可能在进行合并
//求完一个交集区间后,较早结束的子区间不可能再和别的子区间重叠了,它的指针要移动。
if(A[i][1] < B[j][1]) {
i ++;
}else {
j ++;
}
}
return list.toArray(new int[list.size()][2]);
}
}
}