leetcode刷题记录-307. 区域和检索 - 数组可修改

795 阅读3分钟

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

前言

今天的题目为中等,第一次触碰到对于数组求和的时间复杂度优化,没有理解大佬们的题解中的线段树是什么意思,只能退而求其次选择了官方解答中的分段处理的思想。

每日一题

今天的题目是 307. 区域和检索 - 数组可修改,难度为中等

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

  • 其中一类查询要求 更新 数组 nums 下标对应的值

  • 另一类查询要求返回数组 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]
numArray.sumRange(0, 2); // 返回 1 + 2 + 5 = 8

 

提示:

  • 1 <= nums.length <= 3 * 104
  • -100 <= nums[i] <= 100
  • 0 <= index < nums.length
  • -100 <= val <= 100
  • 0 <= left <= right < nums.length
  • 调用 pdate 和 sumRange 方法次数不大于 3 * 104 

题解

暴力解法

本来一看到题目,简单题,不就是在一个类里面新建两个修改数组的方法,然后也真的去做了,结果就是:

class NumArray {
  constructor(arr) {
    this.arr = arr;
  }
  update(index, val) {
    this.arr[index] = val;
  }
  sumRange(left, right) {
    let sum = 0;
    for (let i = left; i <= right; i++) {
      sum += this.arr[i];
    }
    return sum;
  }
}

image.png

开始意识到,题目没有那么简单

分块处理

分块处理,参考了leetcode上官方解答的第一个方法,分块思考:

区域和检索 - 数组可修改

上面暴力解法会超时的原因主要在于,求和的次数过多的话,每次都去遍历全部的 left 到 right 浪费了太多的时间,所以就引出了一个想法,我们能否将整个的大数组,分为很多的小数组,每个小数组就用它们的和来表示,那么在最后一个求和的时候,如果碰到了 left 和 right 的范围很大,那么也不用去循环遍历,只要将之前分好的小数组的和加起来就好,这样在运行了多次求和函数特别是范围大的求和函数的情况下,能节省很多的浪费在循环上的时间复杂度

原本如果从 left 一直遍历到 right 那么时间复杂度为 O(n),在分块处理的方法中,我们把原数组的长度开根号,比如说原数组长度为9,那么我们就把它分为三个部分,每个部分用三个数的和表示,那么在最完美的情况下,能够将时间复杂度节省到 O(√n)

  1. 那么明确了方法,就要在一开始保存数组的时候开始进行处理,我们将题目给的原数组开根号,看看长度需不需要进行分段,需要的话就用一个新的数组保存下来。下面拿一个10个数的数组做个例子。

image.png

  1. 那么在我们对数组做了分块,那么修改方法需要修改的地方就变成了两个,一个是原数组,一个是新的分块数组中对应那一块的值。

  2. 重点就是修改方法,那么分块是怎么做到节省时间复杂度的呢,我们将 left 和right 做一个判断,在一个分块后的数组当中,最坏的情况就是会分为三个部分,一个是左边的一个块被拆分了:

image.png

右边也是同理,那么我们就需要分三块来处理这个的逻辑,一个是左边的那一块的值,一个是中间的所有快,一个是右边的被拆分的值,对应上面的,就是左边的部分是 索引为 2 的这一个元素,中间就是 [2,4,5] 组成的 11,右边就是 6 和 2 的值。

那么试想一下,在 left 和 right 的跨度够大的时候,中间的分块是不是就不用像用索引相加那样子求和 O(n) 次,而只需要 O(√n) 次,那么这就大大地节省了时间复杂度。

class NumArray {
  constructor(nums) {
    this.nums = nums;
    const n = nums.length;
    this.size = Math.floor(Math.sqrt(n));
    this.newNums = new Array(Math.floor((n + this.size - 1) / this.size)).fill(0);
    for (let i = 0; i < n; i++) {
      this.newNums[Math.floor(i / this.size)] += nums[i];
    }
  }
  update(index, val) {
    this.newNums[Math.floor(index / this.size)] += val - this.nums[index];
    this.nums[index] = val;
  }
  sumRange(left, right) {
    const leftSize = Math.floor(left / this.size),
      leftVal = left % this.size,
      rightSize = Math.floor(right / this.size),
      rightVal = right % this.size;
    if (leftSize === rightSize) {
      let newNums = 0;
      for (let j = leftVal; j <= rightVal; j++) {
        newNums += this.nums[leftSize * this.size + j];
      }
      return newNums;
    }
    let sum1 = 0;
    for (let j = leftVal; j < this.size; j++) {
      sum1 += this.nums[leftSize * this.size + j];
    }
    let sum2 = 0;
    for (let j = 0; j <= rightVal; j++) {
      sum2 += this.nums[rightSize * this.size + j];
    }
    let sum3 = 0;
    for (let j = leftSize + 1; j < rightSize; j++) {
      sum3 += this.newNums[j];
    }
    return sum1 + sum2 + sum3;
  }
}

image.png