[303]. 区域和检索 - 数组不可变
Posted Debroon
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[303]. 区域和检索 - 数组不可变相关的知识,希望对你有一定的参考价值。
题目
题目:https://leetcode-cn.com/problems/range-sum-query-immutable/
函数原型
class NumArray
public:
NumArray(vector<int>& nums)
int sumRange(int left, int right)
;
线段树(区间树)
线段树,专门用于处理区间数据,因此又名区间树。
为什么需要线段树,其实数组就可以各种区间操作,数组本身就是一个区间是吧。
因为线段树、动态线段树、数状数组、SQRT分解这些数据结构,都是专门用于解决区间操作的,如果是数组就需要遍历一次,整个区间的查询、整个区间的更新都是 O ( n ) O(n) O(n),而线段树都是 O ( l o g n ) O(logn) O(logn)。
线段树解决的问题类型:
- 区间更新:更新区间中一个元素或一个区间的值
- 区间查询:查询区间中最大值、最小值、区间数字和
线段树,是如何用 O ( l o g n ) O(logn) O(logn) 的时间达到的呢?
线段树,是用空间换时间、升维实现的。
每个节点存储的是一个区间内的信息:
相对于把时间预处理了,比如求区间 [5, 9] 的和,直接调用即可。
那如何求区间 [2, 5] 的和呢,貌似没有这个区间?
把几个区间拼起来即可,如下图:
那怎么创建线段树?
比如上面线段树的根节点有 10 个元素,左孩子是前 5 个元素,右孩子是后 5 个元素。
- 前 5 个元素的和 + 后 5 个元素的和 = 10 个元素的和
- 左孩子 + 右孩子 = 父节点
对于左、右孩子,以此划分,直到叶节点(只有一个元素)。
整个创建过程,是递归的。
线段树的创建,创建好左右子树之后,也就能决定自身节点的值了;
其实,从树的角度看,他们本质都是“后序遍历”,即处理完自己的子树之后,再处理自己。
线段树的物理存储,是数组。
如上图,线段树是一个平衡二叉树(不会退化为链表,整体元素分布平均),堆也是平衡二叉树。
但是一些叶节点是不存在的,为了放入数组中,我们可以假设那些不存在的叶节点为空,形成一个完全二叉树。
那就有一个问题了,如果区间有 n 个元素,用数组表示需要多少个节点?
对于一个满二叉树,树的层数和节点之间的关系:
- 第 0 层:1 个
- 第 1 层:2 个
- 第 2 层:4 个
- 第 3 层:8 个
- 第 h-1 层: 2 h − 1 2^h-1 2h−1 个
最后一层 h-1 层,有 2 h − 1 2^h-1 2h−1 个节点,说明最后一层的节点数大致等于前面所有层节点之和。
- 最好:如果 n = 2 的幂,那么只需要 2n 的空间就可以存储
- 最坏:如果 n = 2 的幂 + 1,那么需要多一层空间存储这个额外的元素,而最后一层的节点数大致等于前面所有层节点之和,也就是需要 4n 的空间存储,最坏的情况下,有一半的空间是浪费的(如果是链式存储,可以避免浪费)
所以,数组表示需要开 4n
空间。
class NumArray
public:
NumArray(vector<int>& arr)
if (arr.size() > 0)
for (auto v : arr)
data.push_back(v);
tree.resize(4 * data.size());
buildTree(0, 0, data.size() - 1); // 创建一个线段树
int sumRange(int i, int j)
if (data.size() == 0) return 0;
return query(0, 0, data.size() - 1, i, j); // 从根节点开始查询区间[i, j]和
private:
vector<int> data; // 数组,用于保存数据副本
vector<int> tree; // 线段树数组
// 返回完全儿叉树的数组表示中,一个索引所表示的元素的左孩子节点的索引(知道根节点,就知道左孩子)
int leftchild(int index)
return index * 2 + 1;
// 返回完全儿叉树的数组表示中,一个索引所表示的元素的右孩子节点的索引(知道根节点,就知道右孩子)
int rightchild(int index)
return index * 2 + 2;
// 在 treeIndex 位置创建表示区间 [l, r] 的线段树
void buildTree(int treeIndex, int l, int r)
if (l == r) // 结束条件:只有 1 个元素
tree[treeIndex] = data[l]; // 所存储的信息等于元素本身
return;
// 表示一个区间
int leftTreeIndex = leftchild(treeIndex); // 必定有左孩子
int rightTreeIndex = rightchild(treeIndex); // 相应的右孩子
// 创建左右子树相应的区间范围
int mid = l + (r - l) / 2; // 以 (l+r)/2 为中心
buildTree(leftTreeIndex, l, mid); // 区间 [l, mid] 创建线段树
buildTree(rightTreeIndex, mid+1, r); // 区间 [mid+1, r] 创建线段树
tree[treeIndex] = tree[leftTreeIndex] + tree[rightTreeIndex];
// 把左、右子树的值加在一起
/* 查询区间
- treeIndex 表示当前处理的线段树的节点,在 tree 这个数组的什么位置
- 在以 treeIndex 为根的线段树中 [l, r] 区间内,寻找 [queryL, queryR] 的值
*/
int query(int treeIndex, int l, int r, int queryL, int queryR)
if (l == queryL && r == queryR)
// 结束条件:左边界和想查询的 queryL 重合 且 右边界和想查询的 queryR 重合
return tree[treeIndex]; // 当前节点值是目标值
// 如果这个节点不是要找的区间,就需要到当前节点的孩子节点去找
int mid = l + (r - l) / 2;
int leftTreeIndex = leftchild(treeIndex); // 计算左孩子索引
int rightTreeIndex = rightchild(treeIndex); // 计算右孩子索引
if (queryL >= mid + 1) // 如果目标区间和左孩子无关
return query(rightTreeIndex, mid + 1, r, queryL, queryR); // 去右孩子找
else if (queryR <= mid) // 如果目标区间和右孩子无关
return query(leftTreeIndex, l, mid, queryL, queryR); // 去左孩子找
// 如果俩种情况都不是,目标区间没有完全落在孩子的区间中,那就需要进行拼接,俩边都需要找
// 从查找 [queryL, queryR] 变成查找[queryL, mid]、[mid + 1, queryR]
int leftresult = query(leftTreeIndex, l, mid, queryL, mid);
int rightresult = query(rightTreeIndex, mid + 1, r, mid + 1, queryR);
return leftresult + rightresult; // 融合
;
线段树好的地方,在于支持动态维护,适合多次查询、多次更新、边更新边查询。
SRQT 分解
线段树用来解决区间问题是 O ( l o g n ) O(log n) O(logn),SRQT 分解解决区间问题是 O ( n ) O(\\sqrt n) O(n),在大规模数据上比不上线段树,好处在于,编程简单。
SRQT 分解:把一个含有 N 个元素的数组分成 n \\sqrt n n 份。
还有一种特殊情况,元素数量不等:
分好后,对每一份都计算一下元素和:
- 第 0 组:130
- 第 1 组:145
- 第 2 组:151
- 第 3 组:223
- 第 4 组:107
比如,查询区间 [6, 9]:
- 6 / 4 = 1(下取整),在第 1 组
- 9 / 4 = 2(下取整),在第 2 组
从 6 到 9 的元素遍历一次就好了,但这种情况貌似就没发挥出 SQRT 的作用。
SQRT 适合查询那种跨度大的区间,如区间[3, 16]:
本来我们应该从 3 到 16 遍历一次,但其实我们不用遍历第 1、2、3 组了。
- 头组:从第 0 组遍历到 55
- 中间:只需要把第 1、2、3 组的值加起来即可
- 尾组:从第 4 组遍历到 91
- 区间[3, 16] = 头组 + 中间 + 尾组
class NumArray
vector<int> data;
vector<int> blocks; // 存储各组的和
int N; // 元素总数
int B; // 每组元素个数
int Bn; // 组数
public:
NumArray(vector<int>& nums)
N = nums.size();
if (N == 0)
return;
B = (int)sqrt(N);
Bn = N / B + (N % B > 0 ? 1 : 0); // 不能整除,组数+1
for(auto v : nums)
data.push_back(v);
blocks.assign(N, 0);
for(int i=0; i<N; i++)
blocks[i / B] += nums[i]; // 求组和
int sumRange(int left, int right)
int bstart = left / B; // 起始组号
int bend = right / B; // 终止组号
int res = 0;
if( bstart == bend ) // 所求的区间和属于同一组
for(int i=left; i<=right; i++) // 遍历得到结果
res += data[i];
return res;
// 所求的区间和不属于同一组,需要分成 3 组
int x = (bstart + 1) * B;
for(int i=left; i<x; i++) // 头组
res += data[i];
for(int i=bstart+1; i<bend; i++) // 中间
res += blocks[i];
for(int i=bend * B; i<=right; i ++) // 尾组
res += data[i];
return res;
;
以上是关于[303]. 区域和检索 - 数组不可变的主要内容,如果未能解决你的问题,请参考以下文章