👉线段树详解
文章目录
🚀一. 引入线段树
示例引入:

暴力法: 在没有了解线段树之前,我们会用暴力法进行求解上述两个问题:
用普通数组存储数列:
- 求区间内最值:遍历数组,时间复杂度为: O ( n ) O(n) O(n);
- 修改元素:由于是数组,具有随机存储的特性,所以修改第k个元素的值的时间复杂度为: O ( 1 ) O(1) O(1)

那么,有没有什么更好的方法呢?
线段树:一种用于区间处理的数据结构,基于二叉树,也就是对于一个线段,我们会用一个二叉树来表示

对n个数进行m次"修改+查询"的时间复杂度为: O ( m l o g 2 n ) O(mlog_2n) O(mlog2n)
e . g e.g e.g 查询{1,2,5,8,6,4,3}的最小值
step:
- 首先,将每一个数字放在二叉树的叶子结点上
- 从叶子结点开始,两两合并为一棵子树,将包含的两个叶子结点的最小值存入该子树的父结点
- 每个结点上的数字就是这个结点的子树的最小值
- 不断合并,直到合并为完整的一棵二叉树
图解:

🚀二. 线段树的应用
🔆1. 线段树的构造
线段树是建立在线段(或区间)基础上的树,树的每个结点代表一条线段 [ L , R ] [L,R] [L,R]
性质:
-
每一个叶子结点都是一个数,其余结点均为区间
-
线段 [ L , R ] [L,R] [L,R]:L是左子结点,R是右子结点
① L = R L=R L=R,它是一个叶子结点
② L < R L<R L<R,它有两个儿子,左儿子代表区间 [ L , M ] [L,M] [L,M],右儿子代表区间 [ M + 1 , R ] [M+1,R] [M+1,R],其中 M = ( L + R ) / 2 M=(L+R)/2 M=(L+R)/2
-
通过结构体数组来存放线段树,数组中每一个元素都是一个结构体,用于存放结点值和左右结点 l , r l,r l,r的信息( l , r l,r l,r即为区间 [ l , r ] [l,r] [l,r]的左右端点),因此,若已知父结点为 p p p,对于左右结点的访问:
①左结点: p < < 1 p<<1 p<<1
②右结点: p < < 1 ∣ 1 p<<1|1 p<<1∣1
由上可知,线段树是由二分法操作所构造而成的二叉树,建树是一个从下往上的过程,先更新子节点,再用子节点去更新父节点
既然是数组,那么这个数组的大小如何确定呢?
线段树大小推导:
对于区间 [ 1 , n ] [1,n] [1,n],假设 n = 2 k n=2^k n=2k,此时线段树为一棵有n个叶子结点的满二叉树,当n更大,不是2的幂次,但又 ≤ 2 k + 1 ≤2^{k+1} ≤2k+1时,有:
- 2 k ≤ n ≤ 2 k + 1 2^k≤n≤2^{k+1} 2k≤n≤2k+1,即 k ≤ l o g 2 n ≤ k + 1 k≤log_2n≤k+1 k≤log2n≤k+1时,线段树所开空间大小与叶子结点数为 2 k + 1 2^{k+1} 2k+1的满二叉树大小相同, s i z e = 2 k + 2 − 1 size=2^{k+2}-1 size=2k+2−1
- 所以,
⌈
l
o
g
2
n
=
k
+
1
⌉
=
k
+
1
⌈log_2n=k+1⌉=k+1
⌈log2n=k+1⌉=k+1,则
2
k
+
2
−
1
=
2
⌈
l
o
g
2
n
+
1
⌉
−
1
=
2
⌈
l
o
g
2
2
∗
n
⌉
−
1
2^{k+2}-1=2^{⌈log_2n+1⌉-1}=2^{⌈log_22*n⌉}-1
2k+2−1=2⌈log2n+1⌉−1=2⌈log22∗n⌉−1
即== 2 k + 2 = 2 ⌈ l o g 2 2 ∗ n ⌉ 2^{k+2}=2^{⌈log_22*n⌉} 2k+2=2⌈log22∗n⌉== - 因为 l o g 2 ( 2 ∗ n ) ≤ ⌈ l o g 2 ( 2 ∗ n ) ⌉ ≤ l o g 2 ( 2 ∗ n ) + 1 = l o g 2 4 n log_2(2*n)≤⌈log_2(2*n)⌉≤log_2(2*n)+1=log_24n log2(2∗n)≤⌈log2(2∗n)⌉≤log2(2∗n)+1=log24n
- 所以 2 l o g 2 2 n ≤ 2 ⌈ l o g 2 2 ∗ n ⌉ < 2 l o g 2 4 n 2^{log_22n}≤2^{⌈log_22*n⌉}<2^{log_24n} 2log22n≤2⌈log22∗n⌉<2log24n,即 2 n ≤ 2 k + 2 < 4 n 2n≤2^{k+2}<4n 2n≤2k+2<4n
- 最终有: 2 n − 1 ≤ s i z e < 4 n − 1 2n-1≤size<4n-1 2n−1≤size<4n−1
求区间最小值代码实现:
a [ ∗ ] a[*] a[∗]为已知数组,即存放叶子结点的值
代码实现:
struct node{
int data;
int l,r;
}t[N<<2]; //大小为4N
//push_up为线段树的核心
void push_up(int i){
t[i].data=min(t[i*2].data,t[i*2+1].data);
}
//构造左右结点
void build(int id,int l,int r){ //id为数组下标
t[id].l=l,t[id].r=r;
if(l==r){ //当区间左右相等时(即为叶子结点)
t[id].data=a[r];
return;
}
int mid=(l+r)>>1; //mid=(l+r)/2
build(id*2,l,mid); //构造左子树
build(id*2+1,mid+1,r); //构造右子树
push_up(id); //构造父结点
}
结论:数组开辟空间大小为 4n
🔆2. 区间查询I
🔱思路分析:
我们知道线段树的每个结点存储的都是一段区间的信息 ,如果现在要查询[4,9]区间的最值,这时候该怎么办呢?我们可以看哪些区间被[5,9]包含了

要查询任意区间 [ i , j ] [i,j] [i,j]的最小值(如 [ 4 , 9 ] [4,9] [4,9]),我们可以递归地查询到区间 [ 4 , 5 ] , [ 6 , 8 ] , [ 9 , 9 ] [4,5],[6,8],[9,9] [4,5],[6,8],[9,9],得到最小值 m i n = 6 , 2 , 10 = 2 min={6,2,10}=2 min=6,2,10=2,查询在 l o g 2 n log_2n log2n时间内完成

实现step:
在线段树构造完成后,对于所要寻找的区间 [ x , y ] [x,y] [x,y],从根节点开始往下递归:
我们要尽可能地让 [ x , y ] ⊇ 当前 [ l , r ] [x,y]⊇当前[l,r] [x,y]⊇当前[l,r],因为这样才能不断逼近,最终使区间相等,找到对应子树的父结点
- 如果当前结点⊇要查询的区间,即 [ x , y ] ⊇ [ l , r ] [x,y]⊇[l,r] [x,y]⊇[l,r],则返回这个结点的data;
- 如果的当前结点不⊇查询的区间,我们让
m
i
d
=
(
l
+
r
)
/
2
mid=(l+r)/2
mid=(l+r)/2,依次检查:
①若 x ≤ m i d x≤mid x≤mid,表示x要比当前结点的左结点的右端点mid小,则需要查询左半区间,递归;
②若 y ≥ m i d y≥mid y≥mid,表示y要比当前结点的右结点的左端点mid大,则需要查询右半区间,递归;
最后,在得到所有 [ x , y ] [x,y] [x,y]包含的区间在线段树中的 d a t a data data后,取他们中的最小值即为结果
代码实现:
int ans = 0x3f3f3f;
//x y表示查询的区间
int find(int i,int x,int y)
{
//需要查询的区间[x,y]包含当前区间[l,r]的时候
if(x <= t[i].l && r <= t[i].r)
return t[i].data; //即为结果
int mid=(t[i].r+t[i].l)/2;
//检查左子树
if(x <= mid) ans = min(ans,find(i * 2,x,y));
//检查右子树
if(y > mid) ans = min(ans,find(i * 2 + 1,x,y));
return ans;
}
总结:
线段树高效的原因是:每个结点的值,代表了以它为根的子树上所有结点的值,查询这个子树的值时,不需要遍历整棵树,而是直接读这个子树的根
🔆3. 单点修改
🔱思路分析:
从根节点递归去找 a [ d i s ] a[dis] a[dis](a为叶子结点的数组),找到了就返回,并在返回的一路上不断更新其父节点的min值

如 此时 d i s = 5 , v a l = 2 dis=5,val=2 dis=5,val=2
代码实现:
void change(int i,int dis,int val){ //将a[dis]改为val
if(t[i].l==t[i].r) //如果是叶子节点(i=dis),那么说明找到了
t[i].data=val;
if(dis<=t[i*2].r)
change(i*2,dis,val);
else
change(i*2+1,dis,val);
t[i] = min(t[i*2], t[i*2+1]);
}
🔆4. 区间修改
✨1. lazy-tag
lazy-tag技术:即延迟标记,解决区间修改问题
设当前结点对应区间
[
l
,
r
]
[l, r]
[l,r],待更新区间
[
a
,
b
]
[a, b]
[a,b],假设我们要对
[
a
,
b
]
[a,b]
[a,b] 上每一个元素加上
d
d
d
若对每一个元素进行单点修改,时间复杂度为
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
因此,我们需要使用 lazy-tag技术:
当 a ≤ l ≤ r ≤ b a ≤ l ≤ r ≤ b a≤l≤r≤b,即 [ l , r ] ∈ [ a , b ] [l, r]∈[a,b] [l,r]∈[a,b]时,不再向下更新,仅更新当前线段树结点 ( + d ) (+d) (+d),并在该结点加上懒标记 t a g tag tag
e . g :假设我们对 [ 4 , 9 ] 区间内每一个元素加 3 e.g :假设我们对[4,9]区间内每一个元素加3 e.g:假设我们对[4,9]区间内每一个元素加3
1. 找到子树 [ 4 , 5 ] [4,5] [4,5],我们对该子树的根节点加 2x3=6(所乘的个数为区间长度),相当于补改变根结点的值,只改变最上层

2. 用新的值更新父节点

3. 重复操作,直到找到所有区间对应的子树并更新

所以,我们不需要遍历到叶子结点,可以对上层的结点进行等效修改并逐层返回,即可满足区间修改,降低了时间复杂度
✨2. push_down技术
push-down技术:用于解决多次修改
对于 lazy-tag技术,我们还存在一个问题:
发生多次修改时,若多次修改同一个结点,tag可能会发生冲突
例如:
做2次区间修改,第一次是 [ 4 , 9 ] [4,9] [4,9],第二次是 [ 5 , 8 ] [5,8] [5,8]
①第一次修改:
[
4
,
9
]
[4,9]
[4,9]覆盖了结点5,也就是
[
4
,
5
]
[4,5]
[4,5]区间,我们对结点5做tag[5]=3的标记
②第二次修改;
[ 5 , 8 ] [5,8] [5,8]不能覆盖结点5([4,5]),也就是说,破坏了对 [ 4 , 5 ] [4,5] [4,5]整体修改的 l a z y − t a g lazy-tag lazy−tag标记,所以此时原 t a g [ 5 ] tag[5] tag[5]必须要往它的子结点传递(也就是对子结点进行 + 3 +3 +3的更新操作),而传递后,tag[5]也就失去了用途,则清空标记
那这和 push-down技术有什么关系呢?
p u s h − d o w n push-down push−down函数的主要功能:
-
在每一次的 u p d a t e update update(更新)时,首先检查结点 p p p 的 t a g [ p ] tag[p] tag[p]
-
①如果 t a g [ p ] tag[p] tag[p]有值:
则说明之前更新时,将 p p p 结点打上了 t a g tag tag 标记,此时则需要将 t a g [ p ] tag[p] tag[p] 的值传递给左右结点,并让tag[p]=0②如果 t a g [ p ] tag[p] tag[p]没有值:
则找到子树区间的根结点,修改根结点即可
代码实现:
void push_down(int id, int l, int r)
{
if (lazy[id])//如果id有lazy标记
{
int mid = (l + r) / 2;
lazy[id * 2] += lazy[id]; //将它的左孩子的lazy加上它的lazy
lazy[id * 2 + 1] += lazy[id]; //将它的右孩子的lazy加上它的lazy
sumv[id * 2] += lazy[id] * (mid - l + 1);
sumv[id * 2 + 1] += lazy[id] * (r - mid);
lazy[id] = 0;//清空lazy标记
}
}
void update(int id, int l, int r, int x, int y, int v)//id:树的节点编号 目前搜索到的区间为[l,r] 目标是将[x,y]的所有数+v
{
if (l >= x && r <= y)//[l,r]被[x,y]包含了
{
lazy[id] += v; //标记
sumv[id] += v * (r - l + 1); //更新根结点
return;
}
push_down(id, l, r); //将懒标记传递给孩子
int mid = (l + r) / 2;
if (x <= mid)
update(id * 2, l, mid, x, y, v);
if (y > mid)
update(id * 2 + 1, mid + 1, r, x, y, v);
sumv[id] = sumv[id * 2] + sumv[id * 2 + 1]; //更新父结点
}
注意:这里push-down只向下传递了一次,是因为update是一个递归函数,在当前层的push-down返回后,update会继续向下检查,不断调用进入下一层继续判断
🔆5. 区间查询II
在区间修改后进行区间查询时,需要用到 p u s h − d o w n push-down push−down 技术
因为如果查询的区间 [ x , y ] [x,y] [x,y]在 [ l , r ] [l,r] [l,r]之下,相当于要求子树之下结点的值,若此时 [ l , r ] [l,r] [l,r]有懒标记,则需要向下传递至 [ x , y ] [x,y] [x,y] (此时则要用到push-down技术),更新该区间的值后( + t a g [ x ] +tag[x] +tag[x]),再不断向上返回
也就是要扫清上层的 t a g tag tag标记,让此次访问到的结点是修改后的值
代码实现:
int find(int id,int l,int r,int x,int y)//id:目前查到的节点编号 目前区间为[l,r] 目标是求出[x,y]的和
{
if(x <= l && r <= y) return sumv[id]; //[l,r]被[x,y]包含
push_down(id,l,r);
int mid = (l + r) / 2,ans = 0;
if(x <= mid)
ans += find(id * 2,l,mid,x,y); //ans+=左孩子和
if(y > mid)
ans += find(id * 2 + 1,mid + 1,r,x,y); //ans+=右孩子和
return ans;
}
*==
```cpp
int find(int id,int l,int r,int x,int y)//id:目前查到的节点编号 目前区间为[l,r] 目标是求出[x,y]的和
{
if(x <= l && r <= y) return sumv[id]; //[l,r]被[x,y]包含
push_down(id,l,r);
int mid = (l + r) / 2,ans = 0;
if(x <= mid)
ans += find(id * 2,l,mid,x,y); //ans+=左孩子和
if(y > mid)
ans += find(id * 2 + 1,mid + 1,r,x,y); //ans+=右孩子和
return ans;
}
💯三. 顺序存储实战应用
技能实战篇:
| 🌟题目 | 跳转👇 | 🌟题目 | 跳转👇 |
|---|---|---|---|
| 1.最大数 | 🚀GO | 2.选数异或 | 🚀GO |
| 3.区间修改、区间查询 | 🚀GO |
🚀四. 总结
本章介绍了高级数据结构—线段树,它利用数组实现了树形结构,可以快速实现一些操作,其应用范围也十分广泛,是算法入门的基础,以上如有错误欢迎指正~~,感谢!!!
觉得本篇有帮助的话,就赏个三连吧~

线段树是一种用于区间处理的数据结构,能高效地进行区间查询和修改操作。通过构建二叉树结构,每个节点代表一个区间,通过push_down技术处理区间修改的延迟标记,降低时间复杂度。线段树在动态维护区间信息时表现出色,常用于解决区间最值、区间求和等问题。

1531

被折叠的 条评论
为什么被折叠?



