定义与概念
线段树是一种二叉树数据结构,用于高效地处理区间查询和区间修改操作。它将一个区间划分成多个子区间,每个节点都对应一个区间。
例如,对于区间 [1,10] , 线段树的根节点可能表示整个区间 [1,10] ,根节点的左子结点 可能表示 [1,5], 右子节点表示区间 [6,10],这种划分会持续下去,直到区间长度为 1(叶子结点)。叶子节点表示原始区间中的单个元素。
朴素思想
假设我们现在有一个数组,我们想对其一个区间查询其区间和,那么对这个数组的查询操作,及找到该区间内所有元素的和的时间复杂度为O(n)。如果我们想要更新其数组内的一个数的值,这个更新操作的时间复杂度便为O(1),可以直接根据下标进行修改。
前缀和
提到和,尤其是对于查询区间和,我们容易想到的一个点就是使用前缀和,这样我们就可以将查询的操作提升到O(1), 但是使用前缀和会有一个问题,当我们的更新次数过多时,尤其是需要更新的元素比较靠前时,每一次更新的代价都会为O(n),从而没有达到优化的效果。但是对于元素不变动的数组前缀和还是有很不错的优势!
线段树
线段树将上述问题的查询以及更新的时间复杂度都变成了O(logn)。当进行多次查询与更新时,线段树一定比上述两种方法更具优势。
首先我们先来看一下线段树是什么结构。线段树是一棵二叉树,每个非叶子节点都有左右子节点。用数组来表示树的结构,对于根树的根节点,它会在index=1的位置上(其实此处0也行,不过大家普遍用1,区别就是计算子节点的方式不同),然后对于其节点的左右子节点的下标分别为 2*index 与 2*index+1。
- 每个节点除了存储表示区间的左右端点和外,还存储与区间相关的信息。
- 例如,在求区间和的线段树中,节点存储该区间内所有元素的和。如果是求区间最大值的线段树,节点存储该区间内元素的最大值。
查询: 我们会根据区间从根节点
向树的两边递归查寻。假设我们现在要查找此树的[2,4]
的区间和,及[50,50,1]
的和, 那么这个过程是什么样的呢?
代码实现
建树
public static void buildTree(int[] arr, int[] tree, int start, int end, int nodeIndex) {
// 找到叶子节点,不再递归
if (start == end) {
tree[nodeIndex] = arr[start];
} else {
// 找到树的左子节点(2*nodeIndex)
// 找到树的右子节点(2*nodeIndex +1)
int leftNode = 2 * nodeIndex;
int rightNode = 2 * nodeIndex + 1;
int mid = (start + end) / 2;
// 将树进行分割,左右递归建树
buildTree(arr, tree, start, mid, leftNode);
buildTree(arr, tree, mid + 1, end, rightNode);
tree[nodeIndex] = tree[leftNode] + tree[rightNode];
}
}
检索
需要分情况进行讨论:
- 情况一
- 情况二则是当前区间被我们的检索区间包围,及蓝色区间在绿色区间里面时,因此不必继续往下递归,可以直接返回当前节点值。这里比较容易想,读者可参考之前的线段树查询。思考一下,每一个节点表示的都是一个区间内所有元素的和,那么当整个当前区间都被我们的检索区间包围了,证明我们需要所有的元素,因此不必继续往下递归查询,可以返回其节点值。
public static int query(int[] arr, int[] tree, int start, int end, int l, int r, int nodeIndex) {
if (l > end || r < start) {
return 0;
}
if (l <= start && end <= r) {
return tree[nodeIndex];
} else {
// 递归查询
int leftNode = 2 * nodeIndex;
int rightNode = 2 * nodeIndex + 1;
int mid = (start + end) / 2;
int leftSum = query(arr, tree, start, mid, l, r, leftNode);
int rightSum = query(arr, tree, mid + 1, end, l, r, rightNode);
return leftSum + rightSum;
}
}
更新操作和建树操作很像,可以进一步思考。
应用场景
- 区间求和问题:高效地计算给定区间内元素的和。例如,给定一个数组,频繁查询数组中某个区间内元素的和。
- 区间最大值 / 最小值问题:快速获取给定区间内元素的最大值或最小值。比如,在一个时间序列数据中,查询某段时间内的最高温度或最低温度。
- 动态区间更新与查询:在一些动态的数据环境中,需要同时支持区间修改(如增加一个值、修改元素)和区间查询操作,线段树能够很好地应对这种情况。