【LeetCode每日一题2022/04/04】307. 区域和检索 - 数组可修改【中等】线段树

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

  • 题目1
  • 线段树
    • 代码
    • 算法复杂度
  • 题目2
    • 代码——线段树
    • 算法复杂度

题目来源于leetcode,解法和思路仅代表个人观点。传送门。
难度: 中等
时间:-
早就想学线段树了,一直没时间学,这次抽空学一下。看之后有无时间再学一下树状数组

题目1

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

线段树

方法二:线段树

线段树是什么?
线段树 s e g m e n t T r e e segmentTree segmentTree是一个二叉树。
每个节点 n o d e node node保存数组 n u m s nums nums在区间 [ s , e ] [s,e] [s,e]的 [最小值/最大值/总和] 等信息。

使用数组/树实现。 这里使用数组。
设根节点的 i d id id为0,则对于某个节点的 i d = n o d e id = node id=node,其左节点的 i d = 2 ∗ n o d e + 1 id=2*node+1 id=2node+1,其右节点的 i d = 2 ∗ n o d e + 2 id=2*node+2 id=2node+2

在叶子节点存储数组 n u m s nums nums n n n个值。(一共有 n n n个叶子节点, n − 1 n-1 n1个非叶子节点)。
由于要防止越界访问,实际数组需要开 4 n 4n 4n的空间。


在本题中,
对于区间 [ s , e ] [s,e] [s,e],节点 n o d e node node保存 [ s , e ] [s,e] [s,e]的总和 s u m s , e sum_{s,e} sums,e
节点 n o d e node node左节点 2 ∗ n o d e + 1 2*node+1 2node+1保存 [ s , m ] [s,m] [s,m]的总和 s u m s , m sum_{s,m} sums,m
节点 n o d e node node右节点 2 ∗ n o d e + 2 2*node+2 2node+2保存 [ m + 1 , e ] [m+1,e] [m+1,e]的总和 s u m m + 1 , e sum_{m+1,e} summ+1,e

其中,中间点 m = s + ( e − s ) / 2 m = s + (e-s)/2 m=s+(es)/2


线段的“简单”实现版。

代码

class NumArray {
public:
    vector<int> segmentTree;
    int n;

    void build(int node, int s, int e, vector<int>& nums){
    	// s==e,即达到叶子节点保存数组的值。
        if(s == e){
            segmentTree[node] = nums[s];
            return;
        }
        // 递归建树,先构建孩子节点,再更新当前的节点
        int m = (unsigned) (s + e) >> 1;
        build(node*2+1, s, m, nums);
        build(node*2+2, m+1, e, nums);
        // merge and update
        segmentTree[node] = segmentTree[node*2+1] + segmentTree[node*2+2];
    }
    void change(int index, int val, int node, int s, int e){
        // s == e == index,单点修改
        if(s == e){
            segmentTree[node] = val;
            return;
        }
        // 类似二分查找,index指明元素val在nums中的位置。
        int m = s + (e - s) /2;
        int lnode = node*2 + 1;
        int rnode = node*2 + 2;
        if(index <= m){
            change(index, val, lnode, s, m);
        }else{
            change(index, val, rnode, m+1, e);
        }
        // merge and update
        segmentTree[node] = segmentTree[node*2+1] + segmentTree[node*2+2];
    }
    int range(int left, int right, int node, int s, int e){
    	// [ [s...e]...left ] || [ right...[s...e] ]
        if(left > e || right < s){
            return 0;
        }
        // [left...[s...e]...right]
        if(left <= s && right >= e){
            return segmentTree[node];
        }
        // [s...left...e...right] => [s...m], [m+1...e]
        int m = s + (e - s) /2;
        int lnode = node*2 + 1;
        int rnode = node*2 + 2;
        return range(left, right, lnode, s, m) + range(left, right, rnode, m+1, e);
    }
    NumArray(vector<int>& nums) {
        this->n = nums.size();
        segmentTree.resize(n*4);
        build(0, 0, n-1, nums);
    }
    
    void update(int index, int val) {
        change(index, val, 0, 0, n-1);
    }
    
    int sumRange(int left, int right) {
        return range(left, right, 0, 0, n-1);
    }
};

/**
 * Your NumArray object will be instantiated and called as such:
 * NumArray* obj = new NumArray(nums);
 * obj->update(index,val);
 * int param_2 = obj->sumRange(left,right);
 */

算法复杂度

时间复杂度:
build函数: O ( n ) O(n) O(n)。其中, n n n是数组 n u m s nums nums的大小。最大不超过 O ( 4 n ) O(4n) O(4n)
change函数: O ( log ⁡ n ) O(\log n) O(logn)。每次都二分一个节点选择更新,回溯的方式更新当前节点。
range函数: O ( log ⁡ n ) O(\log n) O(logn)
空间复杂度: O ( 4 n ) O(4n) O(4n)。其中, n n n是数组 n u m s nums nums的大小。


题目2

2080. 区间内查询数字的频率【中等】

请你设计一个数据结构,它能求出给定子数组内一个给定值的 频率

子数组中一个值的 频率 指的是这个子数组中这个值的出现次数。

请你实现 RangeFreqQuery 类:

  • RangeFreqQuery(int[] arr) 用下标从 0 开始的整数数组 arr 构造一个类的实例。
  • int query(int left, int right, int value) 返回子数组arr[left...right]value频率
    一个 子数组 指的是数组中一段连续的元素。arr[left...right] 指的是 nums 中包含下标leftright 在内 的中间一段连续元素。

示例 1:

输入:
["RangeFreqQuery", "query", "query"]
[[[12, 33, 4, 56, 22, 2, 34, 33, 22, 12, 34, 56]], [1, 2, 4], [0, 11, 33]]
输出:
[null, 1, 2]

解释:
RangeFreqQuery rangeFreqQuery = new RangeFreqQuery([12, 33, 4, 56, 22, 2, 34, 33, 22, 12, 34, 56]);
rangeFreqQuery.query(1, 2, 4); // 返回 1 。4 在子数组 [33, 4] 中出现 1 次。
rangeFreqQuery.query(0, 11, 33); // 返回 2 。33 在整个子数组中出现 2 次。

提示:

1 <= arr.length <= 105
1 <= arr[i], value <= 104
0 <= left <= right 调用 query 不超过 105 次。


注:本题不一定要使用线段树,还有更优的解法。
方法一:哈希表 + 二分查找

代码——线段树

class RangeFreqQuery {
public:
    vector<unordered_map<int,int>> nums;
    int n;
    void build(int node,int s, int e, vector<int>& arr){
        if(s == e){
            nums[node][arr[s]]++;
            return;
        }
        int m = s + (e - s)/2;
        int lnode = node*2 + 1;
        int rnode = node*2 + 2;
        build(lnode, s, m, arr);
        build(rnode, m+1, e, arr);
        //merge
        for(auto& [key,val]:nums[lnode]){
            nums[node][key] += val;
        }
        for(auto& [key,val]:nums[rnode]){
            nums[node][key] += val;
        }
    }
    int range(int left, int right, int val, int node, int s, int e){
        // [ [s...e]...left ] || [ right...[s...e] ]
        if(left > e || right < s){
            return 0;
        }
        // [left...[s...e]...right]
        if(left<= s && right >= e){
            return nums[node][val];
        }
        // [s...left...e...right] => [s...m], [m+1...e]
        int m = s + (e - s) /2;
        int lnode = node*2 + 1;
        int rnode = node*2 + 2;
        // merge
        return range(left, right, val, lnode, s, m) + range(left, right, val, rnode, m+1, e);
    }
    // 二者效率差不多
    int range2(int left, int right, int val, int node, int s, int e){
        if(left == s && right == e){
            return nums[node][val];
        }
        int m = s + (e - s) /2;
        int lnode = node*2 + 1;
        int rnode = node*2 + 2;
        if (right <= m) {
            return range(left, right, val, lnode, s, m);
        } else if (left > m) {
            return range(left, right, val, rnode, m + 1, e);
        } else {
            return range(left, m, val, lnode, s, m) + range(m + 1, right, val, rnode, m + 1, e);
        }
    }
    RangeFreqQuery(vector<int>& arr) {
        this->n = arr.size();
        nums.resize(4*n);
        build(0,0,n-1, arr);
    }
    
    int query(int left, int right, int value) {
        return range(left,right,value,0,0,n-1);
    }
};

/**
 * Your RangeFreqQuery object will be instantiated and called as such:
 * RangeFreqQuery* obj = new RangeFreqQuery(arr);
 * int param_1 = obj->query(left,right,value);
 */

算法复杂度

时间复杂度:
bulid函数: O ( n log ⁡ n ) O(n \log n) O(nlogn)。其中 n n n为数组 a r r arr arr的大小。线段树 s e g m e n t T r e e segmentTree segmentTree log ⁡ n \log n logn层,每层总共需要合并 n n n次。
range函数: O ( log ⁡ n ) O(\log n) O(logn)
空间复杂度: O ( 4 ∗ n ) O(4*n) O(4n)


树状数组…待更新

你可能感兴趣的:(算法,leetcode)