Java&C++题解与拓展——leetcode307.区域和检索-数组可修改【树状数组、线段树学习与使用】

每日一题做题记录,参考官方和三叶的题解

题目要求

在这里插入图片描述

思路一:树状数组

需实现单点修改区间求和两个功能,所以想到使用树状数组。

树状数组(二叉索引树,Binary Indexed Tree)

  • 学习参考链接
  • 初衷是用于解决数据压缩里的累计频率的计算问题,Cumulative Frequency;
  • 现用于高效计算前缀和or区间和,且支持在 O ( log ⁡ n ) O(\log n) O(logn)时间内修改单点值;
  • 利用二进制划分区间,每个节点的值都是其所有子节点值的总和,也即将所划分的区间上界索引处的值更新为区间所有值之和;
  • 涉及两个基本计算,单点增加前缀和查询,复杂度均为 O ( log ⁡ n ) O(\log n) O(logn)
方法功能
lowbit(key)低位运算,计算非负整数 k e y key key在二进制表示下最低位的 1 1 1与其后所有的 0 0 0所构成的数值
add(idx, val)单点增加,将 i d x idx idx处的值增加 v a l val val(并更新其他节点)
query(key)查询前缀和,查询序列前 k e y key key项和,即 log ⁡ n \log n logn个区间的总和

Java

class NumArray {
	//树状数组定义
    int[] tr;
    int lowbit(int x) {
        return x & -x;
    }
    void add(int x, int u) {
        for(int i = x; i <= n; i += lowbit(i))
            tr[i] += u;
    }
    int query(int x) {
        int res = 0;
        for(int i = x; i > 0; i -= lowbit(i))
            res += tr[i];
        return res;
    }

    int[] num;
    int n;
    public NumArray(int[] nums) {
        num = nums;
        n = num.length;
        tr = new int[n + 10];
        for(int i = 0; i < n; i++)
            add(i + 1, num[i]);
    }
    
    public void update(int index, int val) {
        add(index + 1, val - num[index]); //更新各父节点
        num[index] = val; //修改目标
    }
    
    public int sumRange(int left, int right) {
        return query(right + 1) - query(left); //前缀和相减
    }
}
  • 时间复杂度:插入和查询复杂度均为 O ( log ⁡ n ) O(\log n) O(logn)
  • 空间复杂度: O ( n ) O(n) O(n)

C++

【基本和Java一样,除了一小点vector的处理】

class NumArray {
public:
    //树状数组定义
    vector<int> tr;
    int lowbit(int x) {
        return x & -x;
    }
    void add(int x, int u) {
        for(int i = x; i <= n; i += lowbit(i))
            tr[i] += u;
    }
    int query(int x) {
        int res = 0;
        for(int i = x; i > 0; i -= lowbit(i))
            res += tr[i];
        return res;
    }

    vector<int> num;
    int n;
    NumArray(vector<int>& nums) {
        num = nums;
        n = num.size();
        tr.resize(n + 10);
        for(int i = 0; i < n; i++)
            add(i + 1, num[i]);
    }
    
    void update(int index, int val) {
        add(index + 1, val - num[index]); //更新各父节点
        num[index] = val; //修改目标
    }
    
    int sumRange(int left, int right) {
        return query(right + 1) - query(left); //前缀和相减
    }
};

  • 时间复杂度:插入和查询复杂度均为 O ( log ⁡ n ) O(\log n) O(logn)
  • 空间复杂度: O ( n ) O(n) O(n)

思路二:线段树

一个比树状数组更🐂更通用的方法。

线段树

  • 学习参考链接
  • 一种维护区间信息的数据结构,可在 O ( log ⁡ n ) O(\log n) O(logn)时间内实现单点修改、区间修改、区间查询(包括求和、最值)等操作;
  • 基本思想就是划分区间最终将线段划分为一个树形结构,通过合并相应区间来处理区间信息;
  • 懒惰标记:(一个本题不需要的思想),通过延迟对节点信息的修改来减少不必要的操作次数,仅标记其被更改但不更新,下一次再访问到的时候再进行修改,用在pushdown()方法里,即从父辈节点开始向下更新值。
方法功能
pushup(sta)更新 s t a sta sta的所有父辈节点(由子向上)
build(sta, l, r)从编号 s t a sta sta的节点开始构造范围为 [ l , r ] [l,r] [l,r]的树节点
update(sta, idx, val)从编号 s t a sta sta的节点开始,在 i d x idx idx处的值增加 v a l val val
query(sta, l, r)从编号 s t a sta sta的节点开始,查询 [ l , r ] [l,r] [l,r]的区间和

Java

class NumArray {
    Node[] tr;
    class Node {
        int l, r, v;
        Node(int _l, int _r) {
            l = _l;
            r = _r;
        }
    }
    void build(int sta, int l, int r) {
        tr[sta] = new Node(l, r);
        if(l == r)
            return;
        //递归构造
        int m = l + r >> 1;
        build(sta << 1, l, m);
        build(sta << 1 | 1, m + 1, r);
    }
    void pushup(int sta) {
        tr[sta].v = tr[sta << 1].v + tr[sta << 1 | 1].v;
    }
    void update(int sta, int idx, int val) {
        if(tr[sta].l == idx && tr[sta].r == idx) {
            tr[sta].v += val;
            return;
        }
        int m = tr[sta].l + tr[sta].r >> 1;
        if(idx <= m)
            update(sta << 1, idx, val);
        else
            update(sta << 1 | 1, idx, val);
        pushup(sta);
    }
    int query(int sta, int l, int r) {
        if(l <= tr[sta].l && tr[sta].r <= r)
            return tr[sta].v;
        int m = tr[sta].l + tr[sta].r >> 1;
        int res = 0;
        if(l <= m)
            res += query(sta << 1, l, r);
        if(r > m)
            res += query(sta << 1 | 1, l, r);
        return res;
    }

    int[] num;
    public NumArray(int[] nums) {
        num = nums;
        int n = num.length;
        tr = new Node[n * 4];
        build(1, 1, n);
        for(int i = 0; i < n; i++)
            update(1, i + 1, num[i]);
    }
    
    public void update(int index, int val) {
        update(1, index + 1, val - num[index]);//更新各父节点
        num[index] = val; //修改目标
    }
    
    public int sumRange(int left, int right) {
        return query(1, left + 1, right + 1);
    }
}
  • 时间复杂度:插入和查询复杂度均为 O ( log ⁡ n ) O(\log n) O(logn)
  • 空间复杂度: O ( n ) O(n) O(n)

C++

这个地方是有点复杂的,没有定义新的类型,靠多维护几个参数解决。

class NumArray {
private:
    vector<int> tr;
    int n;
    void build(int sta, int l, int r, vector<int> &nums) {
        if(l == r) {
            return;
        }
        int m = (l + r) >> 1;
        build(sta << 1, l, m, nums);
        build(sta << 1 | 1, m + 1, r, nums);
    }
    void pushup(int sta) {
        tr[sta] = tr[sta << 1] + tr[sta << 1 | 1];
    }
    void update(int sta, int l, int r, int idx, int val) {
        if(l == idx && r == idx){
            tr[sta] += val;
            return;
        }
        int m = (l + r) >> 1;
        if(idx <= m)
            update(sta << 1, l, m, idx, val);
        else
            update(sta << 1 | 1, m + 1, r, idx, val);
        pushup(sta);
    }
    int query(int sta, int l, int r, int left, int right) {
        if(left <= l && right >= r)
            return tr[sta];
        int m = (l + r) >> 1;
        int res = 0;
        if(left <= m)
            res += query(sta << 1, l, m, left, right);
        if(right > m)
            res += query(sta << 1 | 1, m + 1, r, left, right);
        return res;
    }

public:
    vector<int> num;
    NumArray(vector<int> &nums) :tr(nums.size() * 4), n(nums.size()) {
        num = nums;
        build(1, 1, n, num);
        for(int i = 0; i < n; i++)
            update(1, 1, n, i + 1, num[i]);
    }
    
    void update(int index, int val) {
        update(1, 1, n, index + 1, val - num[index]); //更新各父节点
        num[index] = val; //修改目标
    }
    
    int sumRange(int left, int right) {
        return query(1, 1, n, left + 1, right + 1);
    }
};

  • 时间复杂度:插入和查询复杂度均为 O ( log ⁡ n ) O(\log n) O(logn)
  • 空间复杂度: O ( n ) O(n) O(n)

总结

看起来很简单的题目,结果完全触及知识盲区,get了两个处理区间的数据结构,然后三叶姐姐总结对于区间类问题考虑问题的方向:

1.简单求区间和,用「前缀和」
2.多次将某个区间变成同一个数,用「线段树」
3.其他情况,用「树状数组」

另:

题目要求前缀和树状数组差分线段树备注
数组不变,求区间和
多次修改某个数(单点),求区间和本题符合
多次修改某个区间,输出最终结果
多次修改某个区间, 求区间和根据修改区间范围大小选择
多次将某个区间变成同一个数, 求区间和根据修改区间范围大小选择

欢迎指正与讨论!
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值