0303-区域和检索 - 数组不可变

Raphael Liu Lv10

给定一个整数数组 nums,处理以下类型的多个查询:

  1. 计算索引 leftright (包含 leftright)之间的 nums 元素的 ,其中 left <= right

实现 NumArray 类:

  • NumArray(int[] nums) 使用数组 nums 初始化对象
  • int sumRange(int i, int j) 返回数组 nums 中索引 leftright 之间的元素的 总和 ,包含 leftright 两点(也就是 nums[left] + nums[left + 1] + ... + nums[right] )

示例 1:

**输入:**
["NumArray", "sumRange", "sumRange", "sumRange"]
[[[-2, 0, 3, -5, 2, -1]], [0, 2], [2, 5], [0, 5]]
**输出:** [null, 1, -1, -3]

**解释:**
NumArray numArray = new NumArray([-2, 0, 3, -5, 2, -1]);
numArray.sumRange(0, 2); // return 1 ((-2) + 0 + 3)
numArray.sumRange(2, 5); // return -1 (3 + (-5) + 2 + (-1)) 
numArray.sumRange(0, 5); // return -3 ((-2) + 0 + 3 + (-5) + 2 + (-1))

提示:

  • 1 <= nums.length <= 104
  • -105 <= nums[i] <= 105
  • 0 <= i <= j < nums.length
  • 最多调用 104sumRange **** 方法

方法一:前缀和

最朴素的想法是存储数组 nums 的值,每次调用 sumRange 时,通过循环的方法计算数组 nums 从下标 $i$ 到下标 $j$ 范围内的元素和,需要计算 $j-i+1$ 个元素的和。由于每次检索的时间和检索的下标范围有关,因此检索的时间复杂度较高,如果检索次数较多,则会超出时间限制。

由于会进行多次检索,即多次调用 sumRange,因此为了降低检索的总时间,应该降低 sumRange 的时间复杂度,最理想的情况是时间复杂度 $O(1)$。为了将检索的时间复杂度降到 $O(1)$,需要在初始化的时候进行预处理。

注意到当 $i \le j$ 时,sumRange}(i,j)$ 可以写成如下形式:

$$
\begin{aligned}
&\quad \ \text{sumRange}(i,j) \
&=\sum\limits_{k=i}^j \textit{nums}[k] \
&= \sum\limits_{k=0}^j \textit{nums}[k] - \sum\limits_{k=0}^{i-1} \textit{nums}[k]
\end{aligned}
$$

由此可知,要计算 sumRange}(i,j)$,则需要计算数组 nums 在下标 $j$ 和下标 $i-1$ 的前缀和,然后计算两个前缀和的差。

如果可以在初始化的时候计算出数组 nums 在每个下标处的前缀和,即可满足每次调用 sumRange 的时间复杂度都是 $O(1)$。

具体实现方面,假设数组 nums 的长度为 $n$,创建长度为 $n+1$ 的前缀和数组 sums,对于 $0 \le i<n$ 都有 sums}[i+1]=\textit{sums}[i]+\textit{nums}[i]$,则当 $0<i \le n$ 时,sums}[i]$ 表示数组 nums 从下标 $0$ 到下标 $i-1$ 的前缀和。

将前缀和数组 sums 的长度设为 $n+1$ 的目的是为了方便计算 sumRange}(i,j)$,不需要对 $i=0$ 的情况特殊处理。此时有:

$sumRange}(i,j)=\textit{sums}[j+1]-\textit{sums}[i]$$

[sol1-Java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class NumArray {
int[] sums;

public NumArray(int[] nums) {
int n = nums.length;
sums = new int[n + 1];
for (int i = 0; i < n; i++) {
sums[i + 1] = sums[i] + nums[i];
}
}

public int sumRange(int i, int j) {
return sums[j + 1] - sums[i];
}
}
[sol1-JavaScript]
1
2
3
4
5
6
7
8
9
10
11
var NumArray = function(nums) {
const n = nums.length;
this.sums = new Array(n + 1).fill(0);
for (let i = 0; i < n; i++) {
this.sums[i + 1] = this.sums[i] + nums[i];
}
};

NumArray.prototype.sumRange = function(i, j) {
return this.sums[j + 1] - this.sums[i];
};
[sol1-Golang]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type NumArray struct {
sums []int
}

func Constructor(nums []int) NumArray {
sums := make([]int, len(nums)+1)
for i, v := range nums {
sums[i+1] = sums[i] + v
}
return NumArray{sums}
}

func (na *NumArray) SumRange(i, j int) int {
return na.sums[j+1] - na.sums[i]
}
[sol1-Python3]
1
2
3
4
5
6
7
8
9
10
11
12
class NumArray:

def __init__(self, nums: List[int]):
self.sums = [0]
_sums = self.sums

for num in nums:
_sums.append(_sums[-1] + num)

def sumRange(self, i: int, j: int) -> int:
_sums = self.sums
return _sums[j + 1] - _sums[i]
[sol1-C++]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class NumArray {
public:
vector<int> sums;

NumArray(vector<int>& nums) {
int n = nums.size();
sums.resize(n + 1);
for (int i = 0; i < n; i++) {
sums[i + 1] = sums[i] + nums[i];
}
}

int sumRange(int i, int j) {
return sums[j + 1] - sums[i];
}
};
[sol1-C]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct {
int* sums;
} NumArray;

NumArray* numArrayCreate(int* nums, int numsSize) {
NumArray* ret = malloc(sizeof(NumArray));
ret->sums = malloc(sizeof(int) * (numsSize + 1));
ret->sums[0] = 0;
for (int i = 0; i < numsSize; i++) {
ret->sums[i + 1] = ret->sums[i] + nums[i];
}
return ret;
}

int numArraySumRange(NumArray* obj, int i, int j) {
return obj->sums[j + 1] - obj->sums[i];
}

void numArrayFree(NumArray* obj) {
free(obj->sums);
}

复杂度分析

  • 时间复杂度:初始化 $O(n)$,每次检索 $O(1)$,其中 $n$ 是数组 nums 的长度。
    初始化需要遍历数组 nums 计算前缀和,时间复杂度是 $O(n)$。
    每次检索只需要得到两个下标处的前缀和,然后计算差值,时间复杂度是 $O(1)$。

  • 空间复杂度:$O(n)$,其中 $n$ 是数组 nums 的长度。需要创建一个长度为 $n+1$ 的前缀和数组。

 Comments
On this page
0303-区域和检索 - 数组不可变