LeetCode 每日一题327:区间和的个数

327.区间和的个数

给定一个整数数组 nums,返回区间和在 [lower, upper] 之间的个数,包含 lower 和 upper。
区间和 S(i, j) 表示在 nums 中,位置从 i 到 j 的元素之和,包含 i 和 j (i ≤ j)。

说明:
最直观的算法复杂度是 O(N²) ,请在此基础上优化你的算法。

示例:

输入: nums = [-2,5,-1], lower = -2, upper = 2,
输出: 3 
解释: 3个区间分别是: [0,0], [2,2], [0,2],它们表示的和分别为: -2, -1, 2

一句话:
给一个数组和一个区间,求数组中有多少个【连续的元素之和】在这个区间内。

解题思路:

第一时间想到暴力法,但是题目明确说明不能使用暴力法,暴力法的时间复杂度为O(N²)。暴力法代码在文章最后。刷题最重要是先想到解决方案,然后考虑优化~

此解法参考官方解答:前缀和 + 归并排序

  • 根据数组算出前缀和数组 preSums,问题等价求 preSum[j] - preSum[i] 在区间内的个数
  • 将 preSums 平分为2段 lefts 和 rights,求得 rights[j] - lefts[i] 在区间内的个数。然后按照此方法继续分解lefts 和 rights,直到分解不动。累加每一次的结果就是最后的结果
  • 假设 lefts 和 rights 都是有序的,问题变得简单很多。定义3个指针 i ,l,r。i 指向 lefts[0],l 和 r 指向 rights[0]。移动 l 找出第一个 rights[l] - lefts[i] 在区间内的点,再移动 r 找出最后一个 rights[r] - lefts[i] 在区间内的点,「r - l + 1 」即为本次的个数。然后移动 i 指向lefts[i+1],再次移动 l 和 r 找出满足区间的最小点和最大点,因为数组是递增的,l 和 r只能向右移动,使得问题简单很多。
  • 利用归并排序合并时特点,每次合并的2个数组都是有序的

假设下图为归并排序某一次合并的状态。
在合并前我们利用两2个数组的有序性先求得满足区间的个数。
在这里插入图片描述

归并排序:https://www.runoob.com/w3cnote/merge-sort.html

方法一:前缀和 + 归并排序

先写一个归并排序,在每次合并前先求满足区间的个数
将代码中【统计代码】去掉,即为归并排序

public int countRangeSum(int[] nums, int lower, int upper) {
    // 前缀和数组
    long[] preSums = new long[nums.length + 1];
    for (int i = 0; i < nums.length; i++) {
        preSums[i + 1] = preSums[i] + nums[i];
    }

    return mergeSort(preSums, 0, preSums.length - 1, lower, upper);
}

/**
 * 归并排序
 */
public int mergeSort(long[] preSums, int left, int right, int lower, int upper) {
    if (left == right) {
        return 0;
    }
    // 分解
    int mid = (left + right) / 2;
    int retL = mergeSort(preSums, left, mid, lower, upper);
    int retR = mergeSort(preSums, mid + 1, right, lower, upper);

    int ret = retL + retR;

    // 在合并数组前统计2个升序数组满足的角标对
    // 【统计】开始
    int i = left;
    int l = mid + 1;
    int r = mid + 1;
    while (i <= mid) {
        // 移动左指针找出满足区间的最小角标
        while (l <= right && preSums[l] - preSums[i] < lower) {
            l++;
        }
        // 移动右指针找出满足区间的最大角标 + 1
        while (r <= right && preSums[r] - preSums[i] <= upper) {
            r++;
        }
        ret += (r - l);
        i++;
    }
    // 【统计】结束

    // 合并
    merge(preSums, left, mid, right);

    return ret;
}

public void merge(long[] arr, int left, int mid, int right) {
    long[] temp = new long[right - left + 1];

    int i = 0;

    int p1 = left;
    int p2 = mid + 1;

    // 比较两个数组,依次将小的元素放入temp,直到遍历完其中一个数组
    while (p1 <= mid && p2 <= right) {
        temp[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
    }

    // 将未遍历完的数组放入temp
    while (p1 <= mid) {
        temp[i++] = arr[p1++];
    }
    while (p2 <= right) {
        temp[i++] = arr[p2++];
    }

    // 改变原数组
    for (int j = 0; j < temp.length; j++) {
        arr[left + j] = temp[j];
    }
}

执行结果:

在这里插入图片描述
时间复杂度: 和归并排序一致 O(NlogN)
空间复杂度: 和归并排序一致 O(N)

附暴力法:

public int countRangeSum1(int[] nums, int lower, int upper) {
    long[] temp = new long[nums.length];
    for (int i = 0; i < nums.length; i++) {
        temp[i] =  nums[i] + 0L;
    }

    int ans = 0;
    for (int i = 0; i < temp.length; i++) {
        long c = 0;
        for (int j = i; j < temp.length; j++) {
            c += temp[j];
            if (c >= lower && c <= upper) {
                ans++;
            }
        }
    }
    return ans;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值