题目来源于leetcode,解法和思路仅代表个人观点。传送门。
难度: 中等
时间:-
早就想学线段树了,一直没时间学,这次抽空学一下。看之后有无时间再学一下树状数组。
给你一个数组 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=2∗node+1,其右节点的 i d = 2 ∗ n o d e + 2 id=2*node+2 id=2∗node+2。
在叶子节点存储数组 n u m s nums nums的 n n n个值。(一共有 n n n个叶子节点, n − 1 n-1 n−1个非叶子节点)。
由于要防止越界访问,实际数组需要开 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 2∗node+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 2∗node+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+(e−s)/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的大小。
2080. 区间内查询数字的频率【中等】
请你设计一个数据结构,它能求出给定子数组内一个给定值的 频率 。
子数组中一个值的 频率 指的是这个子数组中这个值的出现次数。
请你实现 RangeFreqQuery
类:
RangeFreqQuery(int[] arr)
用下标从 0 开始的整数数组 arr
构造一个类的实例。int query(int left, int right, int value)
返回子数组arr[left...right]
中value
的 频率 。arr[left...right]
指的是 nums
中包含下标left
和 right
在内 的中间一段连续元素。示例 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(4∗n)。
树状数组…待更新