线段树是什么?
线段树是一种二叉树 结构,用于存储和处理区间相关的信息。它的每个节点都代表一个区间:
- 叶子节点:存储原始数组的单个元素。
- 非叶子节点:存储其子节点所代表区间的某种聚合信息(例如,和、最大值、最小值等)。
通过这种结构,线段树可以将复杂的区间操作分解为对树的递归操作,从而提高效率。
例如,假设我们有一个数组 arr = [1, 3, 5, 7, 9, 11],我们可以用线段树来快速计算任意区间的和,比如 [1, 3] 的和(即 3+5+7=15)。
线段树的基本结构
以数组 [1, 3, 5, 7, 9, 11] 为例,线段树会将整个数组划分为多个区间,并通过树形结构存储这些区间的聚合信息(这里以"和"为例):
- 根节点:代表整个区间 [0, 5],值为 1+3+5+7+9+11=36。
- 根的左子节点:代表左半部分 [0, 2],值为 1+3+5=9。
- 根的右子节点:代表右半部分 [3, 5],值为 7+9+11=27。
- 依此类推,直到叶子节点,每个叶子节点存储单个元素。
线段树的操作
线段树支持以下三种主要操作:
(1) 构建线段树
- 从原始数组开始,递归地将区间一分为二,直到每个叶子节点代表一个元素。
- 每个非叶子节点的值是其左右子节点值的聚合(例如求和)。
(2) 区间查询
- 查询某个区间 [left, right] 的聚合值(比如和)。
- 根据当前节点的区间与查询区间的关系,决定是直接返回节点值、跳过,还是递归查询子节点。
(3) 单点更新
- 更新数组中某个位置的值,然后同步更新线段树中所有受影响的节点。
这些操作的时间复杂度均为 O(log n),因为线段树的高度是对数组长度 n 取对数。
Java 实现
下面是一个完整的 Java 代码示例,展示如何实现线段树,包括构建、查询和更新操作。我们以计算区间和为例。
线段树的构建
java
class SegmentTree {
private int[] tree; // 线段树数组
private int n; // 原始数组长度
// 构造函数:传入原始数组,初始化线段树
public SegmentTree(int[] arr) {
n = arr.length;
tree = new int[4 * n]; // 线段树的空间通常需要 4 倍原始数组大小
buildTree(arr, 1, 0, n - 1); // 从节点 1 开始构建
}
// 递归构建线段树
private void buildTree(int[] arr, int node, int start, int end) {
if (start == end) {
// 叶子节点,直接存储数组元素
tree[node] = arr[start];
} else {
int mid = (start + end) / 2;
// 递归构建左子树和右子树
buildTree(arr, 2 * node, start, mid);
buildTree(arr, 2 * node + 1, mid + 1, end);
// 父节点值为左右子节点之和
tree[node] = tree[2 * node] + tree[2 * node + 1];
}
}
}
查询操作
java
// 公开方法:查询区间 [left, right] 的和
public int query(int left, int right) {
return query(1, 0, n - 1, left, right);
}
// 递归查询区间和
private int query(int node, int start, int end, int left, int right) {
if (right < start || left > end) {
return 0; // 无交集,返回 0
}
if (left <= start && end <= right) {
return tree[node]; // 完全包含,直接返回节点值
}
// 部分重叠,递归查询左右子树
int mid = (start + end) / 2;
int leftSum = query(2 * node, start, mid, left, right);
int rightSum = query(2 * node + 1, mid + 1, end, left, right);
return leftSum + rightSum;
}
更新操作
java
// 公开方法:更新位置 index 的值为 val
public void update(int index, int val) {
update(1, 0, n - 1, index, val);
}
// 递归更新
private void update(int node, int start, int end, int index, int val) {
if (start == end) {
// 到达叶子节点,更新值
tree[node] = val;
} else {
int mid = (start + end) / 2;
if (index <= mid) {
update(2 * node, start, mid, index, val); // 更新左子树
} else {
update(2 * node + 1, mid + 1, end, index, val); // 更新右子树
}
// 更新父节点
tree[node] = tree[2 * node] + tree[2 * node + 1];
}
}
代码说明
- tree 数组:用一维数组表示线段树,节点编号从 1 开始,左子节点为 2 * node,右子节点为 2 * node + 1。
- 空间需求:线段树通常需要 4n 的空间,以确保能容纳所有节点。
- 构建:从根节点递归划分区间,计算每个节点的和。
- 查询:根据区间关系递归计算结果。
- 更新:从叶子节点向上更新所有受影响的节点。
图解(ASCII 表示)
以下是以数组 [1, 3, 5, 7, 9, 11] 为例的线段树结构(表示区间和):
java
[0,5]:36
/ \
[0,2]:9 [3,5]:27
/ \ / \
[0,1]:4 [2,2]:5 [3,4]:16 [5,5]:11
/ \ / \ /\
[0]:1 [1]:3 [3]:7 [4]:9 [5]:11
解释
-
0,5\]:36 是根节点,表示整个区间的和。
-
0\]:1、\[1\]:3 等是叶子节点,表示单个元素。