数据结构-线段树
概述
线段树 是一种用以维护区间信息 的数据结构,它可在 $O(log^N)$ 时间内实现单点修改、区间修改、区间查询(区间求和,求区间最大值,求区间最小值)等操作。
其结构图大致如下:
关于线段树,以下几点需要注意:
- 线段树与区间树没有任何关系。
- 线段树不支持插入、删除操作。
- 叶节点保存数组中指定位置元素,非叶节点保存所在区间的值 (例如:区间最大值、区间最小值)。
结构
观察图一,发现其结构基本等同于一棵完全二叉树,故而我们使用数组加以实现。
1 | class SegmentTree<E> { |
实现
辅助方法
构建线段树
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17private void build(int treeIndex, int l, int r, E[] arr) {
// 到达叶节点,直接赋值、返回即可。
if (l >= r) {
this.elementData[treeIndex] = arr[l];
return ;
}
// 获取中间位置、左节点位置、右节点位置。
int mid = (l + r) >> 1;
int leftTreeIndex = treeIndex * 2 + 1;
int rightTreeIndex = treeIndex * 2 + 2;
// 递归构建左右子树,并更新当前节点所在区间值。
build(leftTreeIndex, l, mid, arr);
build(rightTreeIndex, mid + 1, r, arr);
this.elementData[treeIndex] = merger.merge((E) this.elementData[leftTreeIndex], (E) this.elementData[rightTreeIndex]);
}当前子树查询指定区间值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22private E queryTree(int treeIndex, int l, int r, int left, int right) {
// 满足查询区间,直接返回即可。
if (l == left && r == right) {
return (E) this.elementData[treeIndex];
}
// 获取中间位置、左节点位置、右节点位置。
int mid = (l + r) >> 1;
int leftTreeIndex = treeIndex * 2 + 1;
int rightTreeIndex = treeIndex * 2 + 2;
// 查询区间在左子树所在区间之中,则在左子树中进行查询。
if (mid >= right) {
return queryTree(leftTreeIndex, l, mid, left, right);
} else if (mid < left) {
// 查询区间在右子树所在区间之中,则在右子树中进行查询。
return queryTree(rightTreeIndex, mid + 1, r, left, right);
} else {
// 查询区间横贯左右子树所在区间,则在左右子树中进行相应查找,然后合并查询结果即可。
return merger.merge(queryTree(leftTreeIndex, l, mid, left, mid), queryTree(rightTreeIndex, mid + 1, r, mid + 1, right));
}
}当前子树更新指定位置元素内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22private void updateTree(int treeIndex, int l, int r, int index, E value) {
// 找到指定位置元素,直接修改即可。
if (l >= r) {
this.elementData[treeIndex] = value;
return ;
}
// 获取中间位置、左节点位置、右节点位置。
int mid = (l + r) >> 1;
int leftTreeIndex = treeIndex * 2 + 1;
int rightTreeIndex = treeIndex * 2 + 2;
// 如果指定位置在左子树所在区间之中,则在左子树中进行修改,反之则在右子树中进行修改。
if (mid >= index) {
updateTree(leftTreeIndex, l, mid, index, value);
} else {
updateTree(rightTreeIndex, mid + 1, r, index, value);
}
// 更新当前节点所在区间值。
this.elementData[treeIndex] = merger.merge((E) this.elementData[leftTreeIndex], (E) this.elementData[rightTreeIndex]);
}初始化
1 | public SegmentTree(E[] arr, Merge<E> merger) { |
查询
查询指定区间的值
1
2
3
4
5
6
7public E query(int left,int right) {
if (left < 0 || right >= this.size || left < right) {
// 输入非法,直接返回。
return null;
}
return queryTree(0, 0, this.size - 1, left, right);
}操纵
更新指定位置元素内容
1
2
3
4
5
6
7public void update(int index, E value) {
if (index < 0 || index >= this.size) {
// 输入非法,直接返回。
return ;
}
updateTree(0, 0, this.size - 1, index, value);
}更新指定区间内容
更新指定区间内容涉及更新具体操作,我们在此规定情景,并指明更新区间内容的方法。
我们规定情景如下:
- 非叶节点保存所在区间元素的最大值。
- 更新操作为:为指定区间所有元素均加上某一常数。
搜索找到区间内所有元素,并为其加上某一常数,这样即可达到更新目的。显而易见,这种方式的时间复杂度为 $O(N)$。为使得更新操作复杂度降为 $O(log^N)$,线段树引入懒更新。
首先在节点结构中引入
mark
字段 (其意义:为左右子树各元素需要加上的常数)。进行更新操作时,我们找到需要更新区间对应的节点,然后仅更新这些节点的所在区间值和mark
字段。后续查询到某个节点时,如果该节点的mark
字段非零,表明左右子树需要更新,则更新左右子树根节点的信息 (如果左右子树根节点被访问,则进一步更新对应节点的左右子树根节点,直至到达叶节点)。
相关文章