307. 区域和检索 - 数组可修改

文章介绍了如何高效地处理数组的查询和更新操作,包括暴力解法、分块思想优化、树状数组以及线段树的数据结构。这些方法旨在减少计算时间,提高性能。树状数组和线段树分别通过低字位计算和二分树结构实现了单点修改和区间求和的O(logn)时间复杂度。
摘要由CSDN通过智能技术生成

给你一个数组 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]
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
  • 调用 update 和 sumRange 方法次数不大于 3 * 104 

解题思路

 拿到题目,暴力解决:

class NumArray {
public:
    NumArray(vector<int>& nums) {
        arr = vector<int>(nums.size()+1);
        for (int i = 0; i < nums.size(); i++) {
            arr[i] = nums[i];
        }
    }
    
    void update(int index, int val) {
        arr[index] = val;
    }
    
    int sumRange(int left, int right) {
        int sum  = 0;
        for (int i = left; i <= right; i++)
            sum += sum_arr[i];
        return sum;
    }
private:
    vector<int>  arr;

};

超时。。。

分析问题,减少计算时间,分块思想;

1、分块求解能降低时间:

class NumArray {
private:
    vector<int> sum; // sum[i] 表示第 i 个块的元素和
    int size; // 块的大小
    vector<int> &nums;
public:
    NumArray(vector<int>& nums) : nums(nums) {
        int n = nums.size();
        size = sqrt(n);
        sum.resize((n + size - 1) / size); // n/size 向上取整
        for (int i = 0; i < n; i++) {
            sum[i / size] += nums[i];
        }
    }
    void update(int index, int val) {
        sum[index / size] += val - nums[index];
        nums[index] = val;
    }
    int sumRange(int left, int right) {
        int b1 = left / size, i1 = left % size, b2 = right / size, i2 = right % size;
        if (b1 == b2) { // 区间 [left, right] 在同一块中
            return accumulate(nums.begin() + b1 * size + i1, nums.begin() + b1 * size + i2 + 1, 0);
        }
        int sum1 = accumulate(nums.begin() + b1 * size + i1, nums.begin() + b1 * size + size, 0);
        int sum2 = accumulate(nums.begin() + b2 * size, nums.begin() + b2 * size + i2 + 1, 0);
        int sum3 = accumulate(sum.begin() + b1 + 1, sum.begin() + b2, 0);
        return sum1 + sum2 + sum3;
    }
};

2. 树状数组

         一个长度为n的数组,完成单点修改或区间求和:

时间复杂度:

        朴素算法:单点修改:O(1)、区间查询:O(n)

        树状数组:单点修改:O(logn)、区间查询: O(logn)

分区思想:

         所有层的第偶数个数字都是没有用的,即使去掉也不影响计算。去掉所有第偶数个数字,若数组长度为偶数则剩下数据组成的数组长度和原数组长度一样,剩下数据组成的数组为树状数组。

        区间求和:只需要求区间包含的子区间的和即可。

        单点修改:只需要向上修改被包含的区间即。

树状数组:

         t[x]节点的父节点为t[x+lowbit(x)],t[x]节点长度等于lowbit(x)、t[x]父节点长度等于t[x+lowbit(x)]

lowbit计算:

1、消掉最后一个1,再用原值减去该值

int lowbit(x) 
{	
    return x - (x & (x - 1));
}

2、正数原码和补码相同,负数求补码:把原码的符号位保持不变,数值位逐位取反,即可得原码的反码,在反码的基础上加 1 即得该原码的补码。

int lowbit(x) 
{	
    return x & -x;
}

更新后缀和,构建和修改

void update(int x, int val, ArrayInt c, int n)
{
    for ( ; x <= n; c[x] += val, x += lowbit(x));
}

查询前缀和,求和

int sum(int x, ArrayInt c, int n)
{
    int ret = 0;
    for ( ; x > 0; ret += c[x], x -= lowbit(x));
    return ret;
}

   解题:

class NumArray {
private:
    vector<int> tree; // 树状树组
    vector<int> nums;
    int lowbit(int x) 
    {
        return (x & -x);
    }       
    void add(int index, int val) {
        for(;index <=nums.size(); index += lowbit(index)) 
            tree[index] += val;
    }  
public: 
    void update(int index, int val) {
        int idx = index + 1;
        for(;idx <=nums.size(); idx += lowbit(idx)) {
            tree[idx] += (val - nums[index]);
        } 
        nums[index] = val;   
    }
    NumArray(vector<int>& nums) : nums(nums) {
        tree.resize(nums.size()+1); 
        for (int i = 1; i <= nums.size(); i++) {
            add(i,nums[i-1]);
        }
    }
    int sumRange(int left, int right) {
        int sum1 = 0,sum2 = 0;
        for(;left > 0 ; left -= lowbit(left))
             sum1 += tree[left];
        int r = right + 1;
        for(; r > 0; r -= lowbit(r))
             sum2 += tree[r];
        return (sum2 - sum1);
           
    }
};

3、线段树

        线段树也是通过分区来计算来减少计算;

        时间复杂度:

                朴素算法:单点修改:O(1)、区间查询:O(n)

                线段树:单点修改:O(logn)、区间查询: O(logn)

数组:  

 线段树构建过程

线段树数组表示: 

        可以发现:节点k左孩子序号为: 2k+1; 右孩子序号为:2k+2;节点k为左孩子和右孩子的并集;            

线段树构建:

    void build_tree(int k, int start, int end) 
    { 
        if (start == end) {
            tree[k] = nums[start];
        } else {
            int mid = (start + end) / 2;
            build_tree(2*k +1, start, mid);
            build_tree(2*k +2, mid+1, end);
            tree[k] = tree[2*k+1] + tree[2*k +2];
        }
    }

线段树更新:

    void update_tree(int k, int start, int end, int index, int val) {
        if (end == start) { 
            nums[index] = val;
            tree[k] = val;
        } else{
            int mid = (start + end) / 2;
            if (index <= mid && index >= start) {
                update_tree(2*k+1, start,mid,index,val);
            } else {
                update_tree(2*k+2, mid+1, end, index, val);
            }
            tree[k] = tree[2*k +1] + tree[2*k +2];
        }
    }

线段树查询:

    int query_tree(int k, int start, int end, int L, int R) {
        if (L > end || R < start) 
            return 0;
        else if(start == end) {
            return tree[k];
        } else if (L <= start && R>= end ) {
            return tree[k];
        } else {
            int mid = (start + end) / 2;
            int sum_left = query_tree(2*k+1,start,mid,L,R);
            int sum_right = query_tree(2*k+2,mid+1,end,L,R);
            return (sum_left + sum_right);
        }
    }

解题:

class NumArray {
private:
    vector<int> &nums;
    vector<int> tree;
    void build_tree(int k, int start, int end) 
    { 
        if (start == end) {
            tree[k] = nums[start];
        } else {
            int mid = (start + end) / 2;
            build_tree(2*k +1, start, mid);
            build_tree(2*k +2, mid+1, end);
            tree[k] = tree[2*k+1] + tree[2*k +2];
        }
    }
    void update_tree(int k, int start, int end, int index, int val) {
        if (end == start) { 
            nums[index] = val;
            tree[k] = val;
        } else{
            int mid = (start + end) / 2;
            if (index <= mid && index >= start) {
                update_tree(2*k+1, start,mid,index,val);
            } else {
                update_tree(2*k+2, mid+1, end, index, val);
            }
            tree[k] = tree[2*k +1] + tree[2*k +2];
        }
    }
    int query_tree(int k, int start, int end, int L, int R) {
        if (L > end || R < start) 
            return 0;
        else if(start == end) {
            return tree[k];
        } else if (L <= start && R>= end ) {
            return tree[k];
        } else {
            int mid = (start + end) / 2;
            int sum_left = query_tree(2*k+1,start,mid,L,R);
            int sum_right = query_tree(2*k+2,mid+1,end,L,R);
            return (sum_left + sum_right);
        }
    }

public:
    NumArray(vector<int>& nums):nums(nums) {
        tree.resize(nums.size()*4);
        build_tree(0,0,nums.size()-1);
    }
    void update(int index, int val) {
        update_tree(0,0,nums.size()-1,index,val);
    }
    int sumRange(int left, int right) {
        return query_tree(0,0,nums.size()-1,left,right);
    }
};

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值