题解 | 「力扣」第 523 题:连续数组(中等)

191 阅读1分钟

摘要:求区间和经常需要想到前缀和,前缀和需要预处理。把预处理的信息记录到哈希表,这是常见的做法。在写代码的时候需要比较小心,弄清楚「循环不变量」。

image.png


题解 | 「力扣」第 523 题:连续数组(中等)

给定一个包含 非负数 的数组和一个目标 整数 k ,编写一个函数来判断该数组是否含有连续的子数组,其 大小至少为 22,且总和为 k 的倍数,即总和为 n * k ,其中 n 也是一个整数

示例 1:

输入:[23, 2, 4, 6, 7], k = 6
输出:True
解释:[2, 4] 是一个大小为 2 的子数组,并且和为 6。

示例 2:

输入:[23, 2, 6, 4, 7], k = 6
输出:True
解释:[23, 2, 6, 4, 7]是大小为 5 的子数组,并且和为 42。

说明:

  • 1nums.length1051 \le nums.length \le 10^5
  • 0nums[i]1090 \le nums[i] \le 10^9
  • 0sum(nums[i])23110 \le sum(nums[i]) \le 2^{31} - 1
  • 1k23111 \le k \le 2^{31} - 1

思路分析

  • 先考虑暴力解法,优化的思路是「空间换时间」;
  • 连续 子区间的问题很多时候可以考虑使用「前缀和」的思想;
  • 一边遍历、一边执行相关操作,因此可以考虑使用哈希表(这是一条经验)。

方法一:暴力解法(超时)

枚举所有长度 大于等于 22 的连续子数组,对它们分别求和,并判断和是否是给定整数 kk 的倍数。

参考代码 1

public class Solution {

    public boolean checkSubarraySum(int[] nums, int k) {
        int len = nums.length;
        for (int left = 0; left < len - 1; left++) {
            // 大小至少为 2
            for (int right = left + 1; right < len; right++) {
                int sum = 0;
                for (int i = left; i <= right; i++) {
                    sum += nums[i];
                }
                if (sum == k || (k != 0 && sum % k == 0)) {
                    return true;
                }
            }
        }
        return false;
    }
}

时间复杂度O(N3)O(N^3) ,这里 NN 是输入数组的长度,使用了三重嵌套的 for 循环遍历数组,因此时间复杂度是 O(N3)O(N^3)

只用了常数个额外变量,因此空间复杂度是 O(1)O(1)

方法二:通过前缀和计算区间和(超时)

「前缀和」的基本思想是 空间换时间

  • 注意到题目中我们求连续子数组的区间和;
  • 求区间和的一个基本的技巧是:根据前缀和的差,求出区间和。

参考代码 2

public class Solution {

    public boolean checkSubarraySum(int[] nums, int k) {
        int len = nums.length;

        // preSum[i] 表示:区间 nums[0..i) 的前缀和
        int[] preSum = new int[len + 1];
        preSum[0] = 0;
        for (int i = 0; i < len; i++) {
            preSum[i + 1] = preSum[i] + nums[i];
        }


        for (int left = 0; left < len - 1; left++) {
            // 大小至少为 2
            for (int right = left + 1; right < len; right++) {
                // 区间和
              	int sum = preSum[right + 1] - preSum[left];
                if (sum == k || (k != 0 && sum % k == 0)) {
                    return true;
                }
            }
        }
        return false;
    }
}

时间复杂度O(N2)O(N^2) ,这里 NN 是输入数组的长度,构建前缀和数组 O(N)O(N),枚举所有长度大于等于 22 的连续子数组 O(N2)O(N^2)

重点

根据求解 「力扣」第 1 题(两数之和)的经验,我们可以在遍历的过程当中记录已经出现的信息,这样就可以通过一次遍历完成计算。记录已经遍历过的信息使用 哈希表

前缀和对 kk 的余数相同,说明区间和是 kk 的倍数

方法三:前缀和与哈希表

在遍历的时候哈希表中,向哈希表插入记录:

key:sum % k
value:i

参考代码 3

import java.util.HashMap;
import java.util.Map;

public class Solution {

    public boolean checkSubarraySum(int[] nums, int k) {
        int sum = 0;

        // key:区间 [0..i] 里所有元素的和 % k
        // value:下标 i
        Map<Integer, Integer> map = new HashMap<>();
        // 理解初始化的意义,-1 是下标,可以认为是哨兵
        map.put(0, -1);
        int len = nums.length;
        for (int i = 0; i < len; i++) {
            sum = (sum + nums[i]) % k;
            if (map.containsKey(sum)) {
                // 长度大于 2
                if (i - map.get(sum) > 1) {
                    return true;
                }
            } else {
                map.put(sum, i);
            }
        }
        return false;
    }
}

时间复杂度O(N)O(N),仅需要遍历输入数组一遍。

思考

这道问题的循环不变量是什么?

image.png