Leetcode【双指针 前缀和 哈希】| 1. 两数之和 & 560. 和为K的子数组 & 974. 和可被 K 整除的子数组

目录

  • Leetcode | 1.两数之和
    • 题目
    • 解题
      • 暴力解法
      • 双指针(优化时间)
      • 哈希表
  • Leetcode | 560. 和为K的子数组
    • 题目
    • 解题
      • 思路(关键:前缀和)
      • 暴力枚举
      • 哈希表
  • Leetcode | 974. 和可被 K 整除的子数组
    • 题目
    • 解题
      • 思路(关键:前缀和&取余)
      • 暴力解法(超时)
      • 哈希表
      • 前缀和取余个数统计

Leetcode | 1.两数之和

题目

给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
示例:
给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/two-sum

解题

暴力解法

 最笨的方法:枚举出所有的情况,找出符合条件的两个数。

class Solution {
    public int[] twoSum(int[] nums, int target) {        
        int n = nums.length;
        int[] res = new int[2];
        for(int i = 0; i < n; i++){
            for(int j = i+1; j < n; j++){
                if(nums[i]+nums[j] == target){
                    res[0] = i;
                    res[1] = j;
                    return res;
                }
            }
        }
        return res;
    }
}

Leetcode【双指针 前缀和 哈希】| 1. 两数之和 & 560. 和为K的子数组 & 974. 和可被 K 整除的子数组_第1张图片

双指针(优化时间)

 先排序,然后用双指针,左指针从左,用指针从最右开始依次寻找。优化了时间复杂度,只需遍历一层,无需双层遍历。因为要用额外的数组存储本来的数组方便存储本来的索引值,所以算是空间换时间。

class Solution {
    public int[] twoSum(int[] nums, int target) {        
        int n = nums.length;
        int[] res = new int[2];
        List<Integer> list = Arrays.stream(nums).boxed().collect(Collectors.toList());
        Arrays.sort(nums);
        int p = 0, q = n-1;
        while(p<q){
            if(nums[p]+nums[q] == target){
                res[0] = list.indexOf(nums[p]);
                res[1] = list.lastIndexOf(nums[q]);
                return res;
            }else if(nums[p]+nums[q] > target){//当前总和大于target,右指针往左挪,使总和变小向target靠近
                q--;
            }else{//当前总和小于target,左指针往右挪找找更大的数使总和变大靠近target
                p++;
            }
        }
        return res;
    }
}

Leetcode【双指针 前缀和 哈希】| 1. 两数之和 & 560. 和为K的子数组 & 974. 和可被 K 整除的子数组_第2张图片

哈希表

 只需一遍遍历,用哈希表存储遍历过的值和下标,这样可以使用map的containsKey()方法方便之后的遍历中与之前遍历过的任意值之间的比较。

class Solution {
    public int[] twoSum(int[] nums, int target) {        
        int n = nums.length;
        int[] res = new int[2];
        Map<Integer,Integer> map = new HashMap<Integer,Integer>();//key存储值,value存储下标
        for(int i = 0; i < n; i++){
            if(map.containsKey(target-nums[i])){
                return new int[]{map.get(target-nums[i]), i};
            }else{
                map.put(nums[i],i);
            }
        }
        return res;
    }
}

Leetcode【双指针 前缀和 哈希】| 1. 两数之和 & 560. 和为K的子数组 & 974. 和可被 K 整除的子数组_第3张图片
进阶:三数之和 & 最接近的三数之和

Leetcode | 560. 和为K的子数组

题目

给定一个整数数组和一个整数 k,你需要找到该数组中和为 k 的连续的子数组的个数。
示例 1 :
输入:nums = [1,1,1], k = 2
输出: 2 , [1,1] 与 [1,1] 为两种不同的情况。
说明 :
数组的长度为 [1, 20,000]。
数组中元素的范围是 [-1000, 1000] ,且整数 k 的范围是 [-1e7, 1e7]。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/subarray-sum-equals-k

解题

思路(关键:前缀和)

 该题的关键在于会算数组不同区间内的值的和。因为要求的是连续的子数组,所以有一个能方便算出任意滑动窗口区间内的连续子数组的和的方法,这个问题就变得简单了。
 可以设置一个sum[]数组,sum[i]存储从nums[0]到nums[i]的值的和。这样区间[i,j]这个连续子数组的和就为sum[j]-sum[i]+nums[i]。这个思想就是前缀和的运用。

前缀和 是一个数组的某项下标之前(包括此项元素)的所有数组元素的和。上述sum[i]就是前缀和。

 已知任意连续子数组的和的求法,该题就转换为上面那道Leetcode | 1. 两数之和这么简单了。
 两数之和求的是nums[i]+nums[j] = target时的 i 和 j 值。
 该题求的时sum[j]-sum[i] = k时的{i,j}有几种组合情况,就有几个和为k的子数组。

暴力枚举

 用一个sum[n]数组存储前缀和,枚举出所有的[i,j]区间计算区间和是否等于target。注意i可以等于j。

public class Solution {
    public int subarraySum(int[] nums, int k) {
        int count = 0;
        //求前缀和
        int n = nums.length;
        int[] sum = new int[n];
        sum[0] = nums[0];
        for(int i = 1; i < n; i++){
            sum[i] = sum[i-1]+nums[i];
        }
        //枚举所有情况
        for(int i = 0; i < n; i++){
            for(int j = i; j < n; j++){
                if(sum[j]-sum[i]+nums[i] == k){
                    count++;
                }
            }
        }
        return count;
    }
}

Leetcode【双指针 前缀和 哈希】| 1. 两数之和 & 560. 和为K的子数组 & 974. 和可被 K 整除的子数组_第4张图片

哈希表

 用哈希表存储前缀和。
 两数之和题目中求的是nums[i]+nums[j] = target时的 i 和 j 值,哈希表存储的使num[i],遍历过程中寻找target-nums[i]。
 这一题求的是sum[j]-sum[i] = k的组合情况数,哈希表存储的是当前遍历到的前缀和(sum[j],i),遍历过程中寻找sum[j]-k = sum[i]的i的情况。

public class Solution {
    public int subarraySum(int[] nums, int k) {
        int count = 0;
        //求前缀和,用哈希表存储,用一个变量计算
        int n = nums.length;
        Map<Integer,Integer> summap = new HashMap<Integer,Integer>();//key为i之前的前缀和,value为等于此前缀和的前缀和个数,即有几个i
        summap.put(0,1);//即从第一个数开始的子数组,区间和用差值计算的话sum[j]-sum[-1],sum[-1]=0边界条件,前缀和为0的i=0开始本来就是一种情况了。
        int sum = 0;
        for(int i = 0; i < n; i++){
            //当前遍历到的前缀和
            sum += nums[i];//即sum[j]
            //判断之前遍历过的j之前是否存在一个i满足sum[j]-k,即在j前是否有区间[i,j]满足条件,若有,有几个?即对应的value
            if(summap.containsKey(sum-k)){
                count += summap.get(sum-k);
            }
            //将当前sum[j]存储哈希表中方便之后遍历的查找
            summap.put(sum,summap.getOrDefault(sum,0)+1);
        }

        return count;
    }
}

Leetcode【双指针 前缀和 哈希】| 1. 两数之和 & 560. 和为K的子数组 & 974. 和可被 K 整除的子数组_第5张图片

Leetcode | 974. 和可被 K 整除的子数组

题目

给定一个整数数组 A,返回其中元素之和可被 K 整除的(连续、非空)子数组的数目。
示例:
 输入:A = [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]
提示:
 1 <= A.length <= 30000
 -10000 <= A[i] <= 10000
 2 <= K <= 10000
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/subarray-sums-divisible-by-k

解题

思路(关键:前缀和&取余)

 这题与上题的关键除了利用前缀和之外,因为要被K整除,而不是和为K。
 判断整除的方式,除了最简单的求各个组合前缀和的差值取余K看是否整除外(暴力解法),还可以不用枚举出所有组合的前缀和之差,而是分别直接对前缀和进行取余K,余数相等的两个前缀和之间的差肯定能被K整除。
 这里需要注意一点:负数取余仍是负数,需要对其余数加上K变成正数来与其他能被K整除的比较。

暴力解法(超时)

 求出前缀和,枚举出所有组合的前缀和之差取余K判断余数是否为0即是否能被K整除。这个解法会超时。
 时间复杂度:O(n2)
 空间复杂度:O(n)

class Solution {
    public int subarraysDivByK(int[] A, int K) {
        int count = 0;
        //求前缀和
        int n = A.length;
        int[] sums = new int[n];
        sums[0] = A[0];
        for(int i = 1; i < n; i++){
            sums[i] = sums[i-1]+A[i];
        }
        //枚举所有情况 
        for(int i = 0; i < n; i++){
            for(int j = i; j < n; j++){
                if((sums[j]-sums[i]+A[i])%K==0){
                    count++;
                }
            }
        }
        return count;
    }
}

哈希表

 哈希表存储前缀和除以K的余数(key),遍历过程中对哈希表中余数相同的个数进行统计,value存储余数为key值时的前缀和的个数。
 时间复杂度:O(n)
 空间复杂度:O(min(n,k))

class Solution {
    public int subarraysDivByK(int[] A, int K) {
        int count = 0;
        //求前缀和 哈希表key存储前缀和取余K的余数,余数相同的两个前缀和之间的子数组是可被K整除的
        int n = A.length;
        int sum = 0;
        Map<Integer,Integer> mp = new HashMap<>();
        mp.put(0,1);//key存储余数,value存储这个余数出现了几次
        for(int i = 0; i < n; i++){
            sum += A[i];
            int mod = sum%K<0?sum%K+K:sum%K;//因为负数取模仍是负数,所以需要加K变成正数
            if(mp.containsKey(mod)){
                count+=mp.get(mod);
            }
            mp.put(mod,mp.getOrDefault(mod,0)+1);
        }
        
        return count;
    }
}

Leetcode【双指针 前缀和 哈希】| 1. 两数之和 & 560. 和为K的子数组 & 974. 和可被 K 整除的子数组_第6张图片
优化代码:

class Solution {
    public int subarraysDivByK(int[] A, int K) {
        int count = 0;
        //求前缀和 哈希表key存储前缀和取余K的余数,余数相同的两个前缀和之间的子数组是可被K整除的
        int sum = 0;
        Map<Integer,Integer> mp = new HashMap<>();
        mp.put(0,1);//key存储余数,value存储这个余数出现了几次
        for(int num:A){
            sum += num;
            int mod = (sum%K+K)%K;//因为负数取模仍是负数,所以需要加K变成正数,而如果负数正好能整除余数为0加K后还是得再整除一哈
            int tmp = mp.getOrDefault(mod,0);
            count += tmp;
            mp.put(mod,tmp+1);
        }
        
        return count;
    }
}

Leetcode【双指针 前缀和 哈希】| 1. 两数之和 & 560. 和为K的子数组 & 974. 和可被 K 整除的子数组_第7张图片

前缀和取余个数统计

 用数组nummod[K]来记录各个前缀和除以K的余数的个数情况。除以K的余数情况一共有K个,所以数组长度为K,nums[i]存储余数为i的前缀和的个数。
 任意两个相同的余数的前缀和之间就可组成一个能被K整除的子数组。所以若有n个余数同为i的前缀和,那么这种情况下可组成能被K整除的子数组的组合为n*(n-1)/2。
 所以我们可以用数组来记录出重复余数的前缀和个数,然后依次记录能组成符合条件的前缀和组合个数即可。
 时间复杂度:O(n)
 空间复杂度:O(k)

class Solution {
    public int subarraysDivByK(int[] A, int K) {
        int count = 0;
        int sum = 0;
        int[] nummod = new int[K];
        nummod[0] = 1;//初始边界条件 第0个元素能整除,余数为0的数有一个,这样方便计算从第一个元素到第n个元素组成的子数组(其和为第n个元素的前缀和与第0个元素的前缀和之差)
        for(int num: A){
            sum += num;
            int mod = (sum%K+K)%K;
            nummod[mod]++;
        }
        for(int i = 0; i < K; i++){
            if(nummod[i]!=0){
                count += nummod[i]*(nummod[i]-1)/2;
            }     
        }
        return count;
    }
}

Leetcode【双指针 前缀和 哈希】| 1. 两数之和 & 560. 和为K的子数组 & 974. 和可被 K 整除的子数组_第8张图片
参考:官方题解

你可能感兴趣的:(Leetcode)