leetcode-区域和检索 - 数组可修改

563 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情

题目描述

给你一个数组 nums ,请你完成两类查询。

  1. 其中一类查询要求 更新 数组 nums 下标对应的值
  2. 另一类查询要求返回数组 nums 中索引 left 和索引 right 之间( 包含 )的nums元素的 和 ,其中 left <= right

实现 NumArray 类:

  • NumArray(int[] nums) 用整数数组 nums 初始化对象
  • void update(int index, int val) 将 nums[index] 的值 更新 为 val
  • int sumRange(int left, int right) 返回数组 nums 中索引 left 和索引 right 之间( 包含 )的nums元素的 和 (即,nums[left] + nums[left + 1], ..., nums[right])

示例 1:
输入:
["NumArray", "sumRange", "update", "sumRange"]
[[[1, 3, 5]], [0, 2], [1, 2], [0, 2]]
输出:
[null, 9, null, 8]
解释:
NumArray numArray = new NumArray([1, 3, 5]);
numArray.sumRange(0, 2); // 返回 1 + 3 + 5 = 9
numArray.update(1, 2); // nums = [1,2,5]

思路

这题是数据结构相关的题目。我们还是从最直接的思路出发,想想如果我们只使用array,会存在什么问题。

  • 初始化,时间复杂度是O(n)
  • 更新操作,时间复杂度是O(1)
  • sumRange,时间复杂度是O(n) 初始化没什么可说的,更新也已经是O(1)了,存在优化空间的就是sumRange。三叶姐的题解介绍了树状数组和线段树的方法,需要良好数据结构基础。我们这里介绍一种朴素的分段的方法。
    我们把原始数组分成n个段,每个段的大小都固定为boxSize,当然,最后一个段可能有零头的存在,所以大小在0~s之间。这样分好之后,我们先进行一次预处理,把每个段的和都求出来,保存在sums数组中。然后我们分别来看更新和sumRange操作的过程。
更新操作

更新的num[i]的时候,同时也要更新num[i]所在的段的和sums[i/boxSize]。这里更新sums[i/boxSize]的时候,不需要对这个段重新求和,只要在原始段和的基础上,加上i位置新值和旧值的差就好,所以我们这里调换一下顺序,先更新sums[i/boxSize],这时候num[i]还是旧值,可以把更新操作的时间复杂度维持在O(1)

sumRange

首先判断left和right所在的段:

  • 同一段 没什么可说的,直接从left直接累加到right即可,注意包含right,是一个前闭后闭的区间。这种情况的时间复杂度是O(boxSize)
  • 不同段 这时候可以把和分成3个部分:
  • sumLeft:left所在区间从left到区间末的和
  • sumMid:left和right中间所有的区间和
  • sumRight:right所在区间从开始到right的和 例如下图就是当boxSize=4时,求2-8和的拆分 求和.png
    这种情况的时间复杂度是O(boxSize + len/boxSize + boxSize),即O(boxSize + len/boxSize)
怎么确定boxSize的值

根据上面最大的时间复杂度O(boxSize + len/boxSize),即求出使得这个表达式最小的boxSize的值。
令 a=sqrt(boxSize),b=sqrt(len/boxSize)
boxSize + len/boxSize = a^2 + b^2 >= aab = 2*len 当切仅当a=b时等号等成立,所以 boxSize = sqrt(len)时,表达式取得最小值

Java版本代码

class NumArray {

    private int[] nums;

    private int[] sums;

    private int boxSize;

    public NumArray(int[] nums) {
        this.nums = nums;
        int len = nums.length;
        boxSize = (int)Math.sqrt(len);
        sums = new int[len/boxSize+1];
        for (int i = 0; i < len; i++) {
            sums[i/boxSize] += nums[i];
        }
    }

    public void update(int index, int val) {
        sums[index/boxSize] += val - nums[index];
        nums[index] = val;
    }

    public int sumRange(int left, int right) {
        int b1 = left / boxSize;
        int b2 = right / boxSize;
        if (b1 == b2) {
            int sum = 0;
            for (int i = left; i <= right; i++) {
                sum += nums[i];
            }
            return sum;
        }
        int sumLeft = 0;
        for (int i = left; i < (b1+1)*boxSize; i++) {
            sumLeft += nums[i];
        }
        int sumMid = 0;
        for (int i = b1+1; i < b2; i++) {
            sumMid += sums[i];
        }
        int sumRight = 0;
        for (int i = b2*boxSize; i <= right; i++) {
            sumRight += nums[i];
        }
        return sumLeft + sumMid + sumRight;
    }
}

/**
 * Your NumArray object will be instantiated and called as such:
 * NumArray obj = new NumArray(nums);
 * obj.update(index,val);
 * int param_2 = obj.sumRange(left,right);
 */