Java 中的线段树

2025年5月12日 | 阅读 8 分钟

线段树也是一种二叉树,但它用于以更好的时间复杂度解决特定问题。与堆类似,Java 中的线段树也通过数组来表示。

使用线段树的场景

让我们来了解一下线段树在哪些场景下非常有用。

假设我们有一个数组 a[] = {0, 1, …, n - 1};并且需要在此数组上执行两项任务。这些任务是:

  1. 计算从索引 x 到 y 的元素之和,其中 0 <= x <= y <= n - 1
  2. 将数组 a[] 的一个元素的值更改为新值 p,即,需要执行 a[j] = p,其中 0 <= j <= n - 1。

让我们讨论解决上述任务的各种方法。

朴素方法

完成这些任务很容易。我们可以从索引 x 到 y 运行一个 for 循环,并计算位于索引 x 到 y 之间的元素之和。如果我们分析求和的时间复杂度,我们将得到 O(n) 时间。

另外,要更新第 j 个索引的值,可以执行 a[j] = p,此操作需要 O(1) 时间。因此,完成这两项任务的总时间复杂度为 O(n) + O(1) = O(n)。

另一种方法

另一种方法是提前创建一个前缀数组,然后完成上述任务。创建前缀数组后,从索引 x 到 y 的元素之和需要 O(1) 时间。但是,第 j 个索引的更新将需要 O(n) 时间。这是因为任何索引的更新都会影响前缀数组,而更新需要 O(1) 时间。因此,总时间复杂度为 O(1) + O(n) = O(n),与朴素方法相同。

由于这两种方法的总体时间复杂度保持不变,因此我们需要找到一种方法来降低其时间复杂度,而这种方法就是使用线段树。线段树需要 O(log(n)) 时间来计算从索引 x 到 y 的和。对于第 j 个索引处值的更新,线段树需要 O(log(n)) 时间。因此,总体时间复杂度变为 O(log(n)) + O(log(n)) = O(2 *(log(n))),这小于 O(n)。

线段树的表示

  • 叶子节点代表数组中的元素。
  • 树的内部节点由合并其子节点生成。由于我们使用数组来表示线段树,位于索引 j 的节点在 2 * j + 1 处有左子节点,在 2 * j + 2 处有右子节点,而索引 j 处节点的父节点是 (j - 1) / 2⌋。

因此,对于数组 a[] = {2, 4, 7, 10, 12, 13},线段树在数组中的表示将是

segArr[] = {48, 13, 35, 6, 7, 22, 13, 2, 4, k, k, 10, 12, k, k},其中 k 是虚拟值。虚拟值没有用。事实上,由于数组表示,这会浪费一些空间。

线段树将是

Segment Tree in Java

红色节点(叶子节点)代表数组中的元素,而橙色节点是合并其子节点的结果。

线段树表示中数组的大小

如果输入数组的大小是 2 的幂,则没有虚拟节点。因此,线段树的大小为 (2 * n - 1),其中 n 是输入数组的大小。这是因为叶子节点的总数为 s,内部节点为 n - 1。因此,n + n - 1 = 2 * n - 1 是数组的总大小。

如果输入数组的大小不是 2 的幂,则线段树的大小为 2 * y - 1,其中 y 是大于 n 的最小的 2 的幂。例如,如果 n 的值为 9,则大于 9 的最小的 2 的幂是 16。所以 y 是 16。

给定范围求和的伪代码

构建线段树后,挑战在于使用线段树计算和。以下伪代码解决了这个问题。

更新线段树中的值

线段树中的更新操作是递归完成的。假设需要更新第 j 个索引,值为 val。从线段树的根开始,将值 val 添加到其范围包含给定范围的所有节点中。如果给定节点的范围不包含给定索引,则不应对该节点进行任何更改。

线段树的实现

下面的程序展示了线段树的实现。

文件名:SegTree.java

输出

Sum of values in the given range 1 to 4 = 33
Updated sum of values in the given range = 34