题目来源
题目描述
class NumArray {
public:
NumArray(vector<int>& nums) {
}
void update(int index, int val) {
}
int sumRange(int left, int right) {
}
};
题目解析
本次要解决的问题是:单点修改+区间查询(区间和)
线段树
线段树是一个二叉树,每个节点保存数组 n u m s nums nums在区间 [ s , e ] [s,e] [s,e]的最小值、最大值或者总和等信息。
线段树可以用数组或者树来实现。
对于数组实现,假设根节点下标为0,如果一个节点在数组的下标是 n o d e node node,那么它的左节点为node * 2 + 1,右节点下标为node。
-
建立build函数
- 我们在节点node保存数组nums在区间[s,e]的总和
- s == e时,节点node是叶子节点,它保存的值等于nums[s]
- s < e时,节点node的左子节点保存[s,mid]的总和,右子节点保存区间[mid+1,e]的总和,那么节点node保存的值等于它的两个子节点保存的值之和
- 假设nums的大小为n,我们规定根节点node = 0保存区间[0,n-1]的总和,然后自下而上递归的建树
- 我们在节点node保存数组nums在区间[s,e]的总和
-
单点修改change函数
- 当我们要修改nums[idx]的值时,我们先找到对应区间[idx,idx]的叶子节点,直接修改叶子节点的值为val,然后自下而上递归的更新父节点的值
-
范围求和range函数
- 给定区间[left,right]时,我们将区间[left,right]拆分成多个节点对应的区间
- 如果节点node对应的区间与[left,right]相同,那么直接返回该节点的值,即当前区间和
- 如果节点node对应的区间与[left,right]不同,那么将区间拆分成[left,mid],[mid+1,right]两个区间,分别计算左子结点和右子结点。
- 给定区间[left,right]时,我们将区间[left,right]拆分成多个节点对应的区间
class NumArray{
private:
std::vector<int> segmentTree;
int N;
void build(int node, int left, int right, std::vector<int> &nums){
if(left == right){
segmentTree[node] = nums[left];
return;
}
int mid = (left + right) >> 1;
build(node * 2 + 1, left, mid, nums);
build(node * 2 + 2, mid + 1, right, nums);
segmentTree[node] = segmentTree[node * 2 + 1] + segmentTree[node * 2 + 2];
}
void change(int idx, int val, int node, int left, int right){
if(left == right){
segmentTree[node] = val;
return;
}
int mid = (left + right) >> 1;
if(idx <= mid){
change(idx, val, node, left, mid);
}else{
change(idx, val, node, mid + 1, right);
}
segmentTree[node] = segmentTree[node * 2 + 1] + segmentTree[node * 2 + 2];
}
int range(int l, int r, int node, int left, int right){
if(l == left && r == right){
return segmentTree[node];
}
int mid = (left + right) >> 1;
if(r <= mid){
return range(l, r, node, left, mid);
}else if(mid + 1 <= l){
return range(l, r, node, mid + 1, right);
}else{
return range(l, r, node, left, mid) + range(l, r, node, mid + 1, right);
}
}
public:
NumArray(std::vector<int> & nums) : N(nums.size()), segmentTree(nums.size() * 4){
build(0, 0, N - 1, nums);
}
void update(int idx, int val){
change(idx, val, 0, 0, N - 1);
}
void sumRange(int l, int r){
range(l, r, 0, 0, N - 1);
}
};
懒更新
树状数组
树状数组是一种可以动态维护序列前缀和的数据结构(下标从1开始),它的功能是:
- 单点修改add(idx, val):将序列第idx个数的值更新val
- 区间查询prefixSum(idx):查询前idx个元素的前缀和
因为题目要求实现更新nums在某个位置的值,因此我们保存原始的nums数组
- 构造函数:树状数组初始对应一个零序列,因此我们遍历nums数组,调用add函数来更新树状数组
- update函数:
- 获取 nums 在 idx 的增加值, 调用add 函数更新树状数组,并更nums[idx]=val。
- sumRange 函数
- 区间和[left,right]可以转化为两个前缀和之差
class NumArray{
private:
std::vector<int> tree;
std::vector<int> &nums;
int lowBits(int idx){
return idx &(-idx);
}
void add(int idx, int val){
while (idx < tree.size()){
tree[idx] += val;
idx += lowBits(idx);
}
}
int prefixSum(int idx){
int sum = 0;
while (idx > 0){
sum += tree[idx];
idx -= lowBits(idx);
}
return sum;
}
public:
NumArray(std::vector<int> & nums) : tree(nums.size() + 1), nums(nums){
for (int i = 0; i < nums.size(); ++i) {
add(i + 1, nums[i]);
}
}
void update(int idx, int val){
add(idx + 1, val - nums[idx]);
nums[idx] = val;
}
int sumRange(int l, int r){
return prefixSum(r + 1) - prefixSum(l);
}
};
前置知识:
- 该操作含义:计算x最低位的1在哪个位置。假设其计算结果是val
- 其结构val可用的含义:
- 数x在树状数组中所“覆盖的长度”(包括它自己在内)
- 树x的父节点为x + val
- 数x的前一个覆盖节点为
x - val
比如对于lowBit(4)
来说,4→0100 ,−4→1100 (补码形式), 其 val就为0100→4,所以:
- 覆盖长度为:4
- 其父节点为:4 + 4 = 8
- 其前一个覆盖的节点为:4-4=0