- 线段树(segment tree)又称「区间树」,是一个高级数据结构,应用的对象是「数组」;
- 线段树是一种实现了高效的「区间查询」与「区间更新」的数据结构。
前置知识:理解「前缀和」数组
-
preSum[i]
表示nums[0..i - 1]
里全部元素的和(一个数代表了原始数组的一个前缀区间的和); -
前缀和数组,就是一堆前缀和,可以用于:快速计算区间和(查询区间和 );
-
区间
[i..j]
的和:preSum[j + 1] - preSum[i]
。
「前缀和数组」与「线段树」都是原始数组的预处理数组
-
有了前缀和数组,就可以把原始数组丢弃了;
-
有了线段树(数组),也可以把原始数组丢弃了。
可以认为都是对原始数组的预处理数组,把一些需要用到的值提前计算出来,思想:空间换时间。
例题:「力扣」第 303 题:区域和检索 - 数组不可变
- 题目链接:303. 区域和检索 - 数组不可变
分析:
- 我们可以设计一个前缀和数组,在查询区间和的时候,只用 O ( 1 ) O(1) O(1) 时间复杂度,不过在数组元素有频繁更新的时候,会导致性能下降,即这种方式不适用于「力扣」第 307 题;
- 缺点:前缀和数组,在有更新需求的前提下,不能高效地工作。
Java 代码:
public class NumArray {
// 前缀和思想:注意:有一个单位的偏移
private int[] preSum;
public NumArray(int[] nums) {
int len = nums.length;
preSum = new int[len + 1];
for (int i = 0; i < len; i++) {
preSum[i + 1] = preSum[i] + nums[i];
}
}
public int sumRange(int i, int j) {
return preSum[j + 1] - preSum[i];
}
public static void main(String[] args) {
int[] nums = {
1, 2, 3, 4};
NumArray numArray = new NumArray(nums);
int result = numArray.sumRange(2, 3);
System.out.println(result);
}
}
/**
* Your NumArray object will be instantiated and called as such:
* NumArray obj = new NumArray(nums);
* int param_1 = obj.sumRange(i,j);
*/
Python 代码:
from typing import List
class NumArray:
def __init__(self, nums: List[int]):
"""
:type nums: List[int]
"""
self.size = len(nums)
if self.size == 0:
return
self.pre_sum = [0 for _ in range(self.size + 1)]
self.pre_sum[0] = 0
for i in range(self.size):
self.pre_sum[i + 1] = self.pre_sum[i] + nums[i]
def sumRange(self, i: int, j: int) -> int:
if self.size > 0:
return self.pre_sum[j + 1] - self.pre_sum[i]
return 0
# Your NumArray object will be instantiated and called as such:
# obj = NumArray(nums)
# param_1 = obj.sumRange(i,j)
「力扣」第 307 题:区域和检索 - 数组可修改
- 题目链接:307. 区域和检索 - 数组可修改。
分析:
- 如果我们不使用任何数据结构,每次求「区间和」,都会遍历这个区间里的所有元素。如果区间里包含的元素很多,并且查询次数很频繁,时间复杂度是 O ( N ) O(N) O(N);
- 如果使用前缀和,更新操作的时间复杂度是 O ( N ) O(N) O(N);
- 如果我们使用线段树,就可以把时间复杂度降低到 O ( log N ) O(\log N) O(logN)。
这里要注意的是「线段树」解决的区间问题不涉及「添加」与「删除」操作。即「CURD」,只负责「U」 和 「R」。
使用「遍历」与使用「线段树」对于「区间更新」与「区间查询」操作的复杂度
遍历 | 线段树 | |
---|---|---|
区间查询 | O ( N ) O(N) O(N) | O ( log N ) O(\log N) O(logN) |
区间更新 | O ( N ) O(N) O(N) | O ( log N ) O(\log N) O(logN) |
说明:由于我们的线段树(区间树)采用平衡二叉树实现,因此 O ( log N ) O(\log N) O(logN) 中的对数函数以 2 为底,即 O ( log N ) = O ( log 2 N ) O(\log N) = O(\log_2 N) O(logN)=O(log2N)。
总结
线段树只回答了以下两个问题,不回答区间里有「删除」和「添加」操作的场景。
- 区间和查询
- 单点(区间)更新