第1课_线段树的基本操作
热度🔥:12 免费课程
授课语音
线段树的基本操作
1. 线段树的简介
线段树(Segment Tree)是一种树形数据结构,广泛应用于处理区间查询和区间更新问题。它可以在 O(log n) 的时间复杂度内解决很多涉及区间的操作,比如区间求和、区间最小值、区间最大值等问题。线段树可以看作是对数组的一种分治方式,将问题分解为较小的子问题,以提高查询和更新的效率。
1.1 线段树的应用场景
- 区间求和问题:给定一个数组,要求在区间 [l, r] 内求和。
- 区间最小值/最大值问题:要求在区间 [l, r] 内求最小值或最大值。
- 区间更新问题:对数组的某个区间进行修改,比如加值、减值、设置某个数等。
1.2 线段树的结构
线段树是一棵完全二叉树,每个节点存储的是某个区间的某个属性值(如和、最小值、最大值等)。树的叶子节点表示的是数组的元素,而非叶子节点表示的是其子区间的合并结果。
例如,如果我们要构建一个求和的线段树,树的根节点表示整个数组的和,根的左子节点表示数组前一半的和,右子节点表示后一半的和。
2. 线段树的基本操作
线段树主要有以下几个基本操作:
- 构建线段树:从数组构建出线段树的结构。
- 区间查询:查询一个区间的结果(如求和、求最小值、最大值等)。
- 区间更新:更新区间内的元素,比如修改某个区间的值。
2.1 构建线段树
线段树的构建需要一个数组和一个递归过程,构建过程通常是递归分治的方式。每个节点都包含一个区间的相关信息。
2.2 区间查询
查询操作是通过递归的方式合并子节点的结果来完成的。例如,查询一个区间的和,可以通过递归地将区间分成若干子区间,然后将这些子区间的和合并得到最终结果。
2.3 区间更新
更新操作有两种常见的方式:
- 单点更新:只修改某个位置的值。
- 区间更新:对某个区间的值进行修改。这个操作可能需要涉及到懒标记(Lazy Propagation)来优化更新过程。
3. 线段树的Java实现
接下来,我们将通过一个简单的例子来演示线段树的基本操作,首先实现线段树的构建、区间查询以及单点更新。
3.1 线段树的实现代码
public class SegmentTree {
private int[] tree; // 线段树数组
private int n; // 数组的大小
// 构造函数,构建线段树
public SegmentTree(int[] arr) {
this.n = arr.length;
this.tree = new int[4 * n]; // 线段树的大小是4倍于原数组大小
build(arr, 0, 0, n - 1); // 从根节点开始构建线段树
}
// 构建线段树
private void build(int[] arr, int node, int start, int end) {
if (start == end) {
tree[node] = arr[start]; // 叶子节点保存数组中的值
} else {
int mid = (start + end) / 2;
int leftChild = 2 * node + 1;
int rightChild = 2 * node + 2;
build(arr, leftChild, start, mid); // 构建左子树
build(arr, rightChild, mid + 1, end); // 构建右子树
tree[node] = tree[leftChild] + tree[rightChild]; // 合并左右子树的结果
}
}
// 查询区间 [l, r] 的和
public int query(int l, int r) {
return query(0, 0, n - 1, l, r);
}
// 区间查询操作
private int query(int node, int start, int end, int l, int r) {
if (r < start || end < l) {
return 0; // 当前节点与查询区间不重合,返回 0
}
if (l <= start && end <= r) {
return tree[node]; // 当前节点完全在查询区间内,返回节点值
}
// 当前节点部分在查询区间内,递归查询左右子树
int mid = (start + end) / 2;
int leftChild = 2 * node + 1;
int rightChild = 2 * node + 2;
int leftQuery = query(leftChild, start, mid, l, r);
int rightQuery = query(rightChild, mid + 1, end, l, r);
return leftQuery + rightQuery; // 合并结果
}
// 更新数组中某个位置的值
public void update(int idx, int value) {
update(0, 0, n - 1, idx, value);
}
// 更新操作
private void update(int node, int start, int end, int idx, int value) {
if (start == end) {
tree[node] = value; // 更新叶子节点的值
} else {
int mid = (start + end) / 2;
int leftChild = 2 * node + 1;
int rightChild = 2 * node + 2;
if (start <= idx && idx <= mid) {
update(leftChild, start, mid, idx, value); // 更新左子树
} else {
update(rightChild, mid + 1, end, idx, value); // 更新右子树
}
tree[node] = tree[leftChild] + tree[rightChild]; // 合并更新后的结果
}
}
}
3.2 代码解释
构造函数:我们首先创建一个长度为
4 * n
的数组tree
来存储线段树。线段树的大小一般是原数组大小的 4 倍,因为在最坏的情况下,叶子节点的数量是n
,而根节点到叶子节点之间最多需要 2 次分割,每次都需要 2 个子节点。因此,线段树的节点数是4 * n
。build
函数:这个函数用于递归地构建线段树。叶子节点对应数组中的元素,非叶子节点是左右子节点的合并结果。query
函数:这是区间查询的核心函数,用于查询区间[l, r]
的和。它会递归地判断当前节点是否与查询区间有交集,并通过递归调用合并子节点的查询结果。update
函数:这是单点更新操作。当数组中的某个元素值发生变化时,调用update
函数对相应节点的值进行更新,并向上更新其父节点的值。
4. 线段树的时间复杂度
- 构建线段树:构建线段树的时间复杂度是 O(n),因为我们需要对数组进行一次递归处理,树的深度是 O(log n),每个节点的合并操作是 O(1) 的。
- 区间查询:查询操作的时间复杂度是 O(log n),因为线段树的深度是 O(log n),在查询过程中最多访问到 O(log n) 个节点。
- 单点更新:更新操作的时间复杂度也是 O(log n),因为在更新过程中,最多需要修改 O(log n) 个节点的值。
5. 总结
线段树是一种高效的数据结构,适用于处理动态区间查询和区间更新问题。它通过将一个区间问题分解成多个子区间,从而实现了高效的查询和更新操作。通过本讲解,我们了解了线段树的基本构建、查询和更新操作,并实现了一个简单的线段树示例。在实际应用中,线段树可以有效地提升许多区间操作问题的效率,尤其是当数据量很大时,传统的暴力方法往往无法满足性能要求。
希望今天的课程能帮助你更好地理解和使用线段树,解决实际问题。