[路飞]区间和的个数

191 阅读3分钟

记录 1 道算法题

区间和的个数

327. 区间和的个数 - 力扣(LeetCode) (leetcode-cn.com)

好久没有更了,因为去做了些其他事情。这道题真的很想吐槽,官方的题解测试都通不过,超过输出限制(60/67的时候)。

所以就只讲一下思路,用最基础的测试用例 [-2, 5, 1], lower: -2, upper: 2 输出:3。

要求:区间和就是说数组可以分成多个长度的子数组,计算这些子数组内的和,然后这个和要在 lower 和 upper 之内。

前缀和

我们可以使用归并排序进行解决,为什么呢。首先要介绍前缀和。

前缀和就是另外生成一个空数组,然后数组存的是原数组每一项的和他前面的所有项的和,比如 [1,2,3] 的前缀和数组就是 [1,3,6]。通常为了方便计算会在开头放一个 0,因为第一个数不能减undefined。0不会影响结果。[0,1,3,6]。

然后通过前缀和数组可以求出任意区间的和。比如上面的数组,求出区间[2,3]的和,其实就是前缀和数组中 sum[3] - sum[1] 的值。所有我们可以通过遍历前缀和数组得到全部的区间和。优点是减少了双重循环求区间和的时间复杂度。

解题

接下来是归并排序是如何解决这个问题的。

首先归并排序是用在两个升序的数组上的,这一点可以通过递归等方式,将数组进行拆分到一个个长度为 2 的子数组,然后一层层返回时两个数组都会是升序的。

其次前缀和数组进行排序不会影响区间和。这里的排序不是普遍意义的排序,而是通过递归等深度遍历对相邻的两组子数组进行归并排序,最开始是 2 个进行比较,这时候位置还是原位置,所以可以通过相减得到区间和符合范围的个数。然后重点是这里,经过比较的数会被合并为一个数组,而归并排序是从左右两边数组各取一个值,不会在同一个数组里面取两个值,所以这时候排序的是已经处理过的数,不影响后面继续比较。

归并排序永远把数组分为了两个独立的升序子数组 [未和另一个数组进行比较,但已经与这个数组进比较的数][未和另一个数组进行比较,但已经与这个数组进比较的数]

而对已经比较过的数进行排序的好处是我们只需要遍历一次前缀和数组就可以得出符合条件的个数,虽然里面也有循环。另一个好处就是根据升序的特性排除掉后面的数。

实现

    function countRangeSum(nums, lower, upper) {
        // 先计算前缀和
        let c = 0
        const sum = [0]
        for(let i = 0; i < nums.length; i++) {
            sum.push(c += nums[i])
        }
        
        // 进行归并排序同时进行比较,返回个数
        return compute(sum, 0, sum.length - 1, lower, upper)
    }
    
    function compute(sum, left, right, lower, upper) {
        // 当只有一个数的时候返回 0
        if (left === right) return 0
        
        // 递归
        const mid = Math.floor((left + right) / 2)
        const leftCount = compute(sum, left, mid, lower, upper)
        const rightCount = compute(sum, mid + 1, right, lower, upper)
        // 个数是由每一组比较后一层层返回累加
        let count = leftCount + rightCount
        
        // 比较符合的区间和
        // 比较常见的做法是双重遍历,从左右两个数组拿值,比较区间和
        let i = left
        while(i <= mid) {
            let j = mid + 1
            // 由于还要大于 lower,所以右边的数组要比左边的数组大 lower
            while(j <= right) {
                if (sum[j] - sum[i] >= lower && sum[j] - sum[i] <= upper) {
                    count++
                }
                j++
            }
            i++
        }
        
        // 上面比较区间和有个简单的思路就是使用差值。比如
        // cccccc
        // aaaaaaaaaa
        // 两者相减,就得出了右边的 4 个 a
        // 这里的思路是计算小于 lower 的个数,然后计算小于 upper 的个数,相减就得到了 lower 到 upper 区间内的个数。
        // let i = left
        // let l = mid + 1
        // let r = mid + 1
        // while(i <= mid) {
            // while(l <= right && l < lower) l++
            // while(r <= right && r <= upper) r++
            // count += (r - l)
            // i++
        // }
        
        // 进行归并排序
        const arr = []
        l = left
        r = mid + 1
        // 左右两边对比
        while(l <= mid || r <= right) {
            // 当有一方先遍历完的时候
            if (l > mid) {
                arr.push(r++)
            } else if (r > right) {
                arr.push(l++)
            }
            // 两边都有的时候
            else if (sum[l] < sum[r]) {
                arr.push(l++)
            } else {
                arr.push[r++]
            }
        }
        
        // 排序完成之后要保存到前缀和数组中,参与递归
        for(let i = 0; i < arr.length; i++) {
            // 从 left 开始进行替换
            sum[left + i] = arr[i]
        }
        
        return count
    }