这里只说最基础的线段树,不涉及lazy标记和延迟修改的操作
用处
- 面向数组中的数据,针对其中编号是连续的区间进行修改或统计的操作
举一个小栗子,假设我们现在有10000个整数,存在
A
[
1
]
−
A
[
10000
]
A[1]-A[10000]
A[1]−A[10000] 中
统计操作: 统计
[
L
,
R
]
[L, R]
[L,R] 的数字之和
修改操作: 将第
L
L
L 个数增加
C
C
C
如果不使用线段树,基本的处理方法有这两种:
法1: 用原始数组进行存储,进行统计操作时候将 R-L+1个数进行相加,进行修改操作时直接修改对应位置的数字
法2: 新建一个求和数组
S
[
n
]
S[n]
S[n],其中
S
[
0
]
=
0
,
S
[
i
]
=
S
[
i
−
1
]
+
A
[
i
]
S[0] = 0, S[i] = S[i-1] + A[i]
S[0]=0,S[i]=S[i−1]+A[i],进行统计操作的时候计算
S
[
R
]
−
S
[
L
−
1
]
S[R] - S[L - 1]
S[R]−S[L−1],进行修改操作的时候同时修改
S
[
L
,
10000
]
S[L, 10000]
S[L,10000]
那么这两个方法的时间复杂度分别是
- | 法1 | 法1时间复杂度 | 法2 | 法2时间复杂度 |
---|---|---|---|---|
统计 | 计算 R − L + 1 R-L+1 R−L+1次 | O ( n ) O(n) O(n) | 计算1次 | O ( 1 ) O(1) O(1) |
修改 | 修改一个元素 | O ( 1 ) O(1) O(1) | 修改 10000 − L + 1 10000-L+1 10000−L+1个元素 | O ( n ) O(n) O(n) |
不难发现,统计和修改不能两全,这时候就用到了线段树。
原理
假设需要处理的数组含有
n
n
n 个元素,为
A
[
1
]
−
A
[
n
]
A[1]-A[n]
A[1]−A[n]
需要将
[
1
,
n
]
[1, n]
[1,n] 分割成若干特定的子区间,通过对少量特定子区间进行修改,来实现对
[
L
,
R
]
[L, R]
[L,R] 的快速修改和统计
用线段树进行统计的东西,必须符合区间加法。符合区间加法的例子有:
- 数字之和:sum = sum(左区间) + sum(右区间)
- 最大值:max = max(左区间) + max(右区间)
不符合区间加法的例子:
- 众数:more ≠ more(左区间) + more(右区间)
下面就逐一讲线段树的各个步骤
子区间的划分
对于给定区间
[
L
,
R
]
[L, R]
[L,R],只要
L
≠
R
L ≠ R
L=R,就将其分为两个区间,分割方式和二分法类似,求取中间值
m
i
d
=
(
L
+
R
)
/
2
mid = (L + R) / 2
mid=(L+R)/2,再求
左
区
间
=
[
L
,
m
i
d
]
左区间=[L, mid]
左区间=[L,mid],
右
区
间
=
[
m
i
d
+
1
,
R
]
右区间=[mid + 1, R]
右区间=[mid+1,R],直到
L
=
=
R
L==R
L==R为叶子节点
下图中的
[
]
[]
[] 为
[
L
,
R
]
[L, R]
[L,R],存储的内容为执行区间加法后的结果
如此这般划分之后,执行统计操作只要找到对应区间(如
[
2
,
12
]
=
[
2
]
+
[
3
,
4
]
+
[
5
,
7
]
+
[
8
,
10
]
+
[
11
,
12
]
[2, 12] = [2] + [3, 4] + [5, 7] + [8, 10] + [11, 12]
[2,12]=[2]+[3,4]+[5,7]+[8,10]+[11,12]),执行修改操作也是找到对应区间 (如修改
[
2
]
[2]
[2],即需要修改
[
2
,
2
]
→
[
1
,
2
]
→
[
1
,
4
]
→
[
1
,
7
]
→
[
1
,
13
]
[2, 2] → [1, 2] → [1, 4] → [1, 7] → [1, 13]
[2,2]→[1,2]→[1,4]→[1,7]→[1,13])
那么无论是统计还是修改,时间复杂度都为
O
(
log
n
)
O(\log n)
O(logn)
存储结构
- 用二叉树进行存储,每个节点包含left, right, value, left_child, right_child(左右区间,左右孩子,区间加法值)
- 用数组进行存储,建立时用 4 n 4n 4n 的空间大小,父节点 v v v 的两个孩子分别为 2 v + 1 2v+1 2v+1, 2 v + 2 2v+2 2v+2, r o o t = 0 root=0 root=0
这里给出使用数组的构建线段树方法
public int buildTree(int current, int left, int right){
if(left == right){
biTree[current] = nums[left];
return nums[left];
}
int mid = (left + right) / 2;
int leftNum = buildTree(2 * current + 1, left, mid);
int rightNum = buildTree(2 * current + 2, mid + 1, right);
biTree[current] = leftNum + rightNum;
return biTree[current];
}
查询方法
public int findRange(int currentNode, int left, int right, int currentLeft, int currentRight){
if(currentLeft == left && currentRight == right)
return biTree[currentNode];
int mid = (currentLeft + currentRight) / 2;
if(left > mid)
return findRange(2 * currentNode + 2, left, right, mid + 1, currentRight);
if(right <= mid)
return findRange(2 * currentNode + 1, left, right, currentLeft, mid);
int leftNum = findRange(2 * currentNode + 1, left, mid, currentLeft, mid);
int rightNum = findRange(2 * currentNode + 2, mid + 1, right, mid + 1, currentRight);
return leftNum + rightNum;
}
修改方法
public void change(int currentNode, int val, int index, int currentLeft, int currentRight){
if(currentLeft <= index && index <= currentRight)
biTree[currentNode] += val;
if(currentLeft == currentRight || index > currentRight || index < currentLeft)
return;
int mid = (currentLeft + currentRight) / 2;
change(currentNode * 2 + 1, val, index, currentLeft, mid);
change(currentNode * 2 + 2, val, index, mid + 1, currentRight);
return;
}