数据结构零基础线段树笔记1

Posted karshey

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构零基础线段树笔记1相关的知识,希望对你有一定的参考价值。

是自学的笔记。
引用和参考:
关于线段树
B站视频:【数据结构】线段树(Segment Tree) 灯神讲得好!!

引入

假设我们现在有一个非常大的数组,我们要反复地做两个操作:

  • 求某个区间的和。O(n)
  • 修改数组中某个值。O(1)

如果要做很多次,时间复杂度会很高。

我们怎么想办法降低求某个区间的和的复杂度呢?

我们建立一个和原数组a一样大的数组s,s是a的前缀和
如果想求[L,R]这个区间的和,只需要求S[R]-S[L-1]即可。 O(1)
但是如果想要更新数组的值,又变复杂了。O(n)

这两种方法的时间复杂度都高。
于是,我们引入了线段树,它可以把这两个操作的时间复杂度平均一下,把求区间和、更新数值的时间复杂度都落在O(nlogn)上,从而增加算法的效率。

线段树的性质和作用

1、线段树的每个节点都代表一个区间
2、线段树具有唯一的根节点,代表的区间是整个统计范围,如[1,N]。
3、线段树的每个叶节点都代表一个长度为1的元区间[x,x]
4、对于每个内部节点[l,r],它的左子结点是[l,mid],右子节点是[mid+1,r],其中mid=(l+r)/2(向下取整)。

接下来我们根据数组来建线段树。


怎么用呢?

如果我们想求[2,5]区间的和,根据上图,则是求[2,2]+[3,5]=5+27=32;
如果我们想把a[4]改为6,根据上图,则值为9,16,27,36的值都要改,即都减3.
变成这样:

如何保存线段树

我们用数组来保存线段树。

如图,若已知根节点,我们求左右子节点的方法如下:

left=2*node+1;
right=2*node+2;

代码

建树build

//参数分别是:原数组,线段数组,当前节点,数组左、右范围 
void build(int a[],int tree[],int node,int start,int end) 

	//递归边界:遇到叶子节点时 即[x,x]
	if(start==end)
	
		tree[node]=a[start];
		//当前tree结点值就是a数组的值 因为是叶子节点 
	 
	else
	
		int mid=(start+end)/2;
		
		//求左右节点的下标
		int left=2*node+1;
		int right=2*node+2;
		
		//递归地建左子树 
		build(a,tree,left,start,mid); 
		//递归地建右子树
		build(a,tree,right,mid+1,end); 
		
		//当前结点的值等于左右子节点的值之和 
		tree[node]=tree[left]+tree[right];
	
 

让我们看看树长成什么样:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e3+10;
int a[]=1,3,5,7,9,11;
int size=6;//存数组大小
int tree[N]=0; 

//参数分别是:原数组,线段数组,当前节点,数组左、右范围 
void build(int a[],int tree[],int node,int start,int end) 

	//递归边界:遇到叶子节点时 即[x,x]
	if(start==end)
	
		tree[node]=a[start];
		//当前tree结点值就是a数组的值 因为是叶子节点 
	 
	else
	
		int mid=(start+end)/2;
		
		//求左右节点的下标
		int left=2*node+1;
		int right=2*node+2;
		
		//递归地建左子树 
		build(a,tree,left,start,mid); 
		//递归地建右子树
		build(a,tree,right,mid+1,end); 
		
		//当前结点的值等于左右子节点的值之和 
		tree[node]=tree[left]+tree[right];
	
 
int main()

	build(a,tree,0,0,5);
	for(int i=0;i<=14;i++)
	
		printf("tree[%d] = %d\\n",i,tree[i]);
	
	return 0;

输出:

tree[0] = 36
tree[1] = 9
tree[2] = 27
tree[3] = 4
tree[4] = 5
tree[5] = 16
tree[6] = 11
tree[7] = 1
tree[8] = 3
tree[9] = 0
tree[10] = 0
tree[11] = 7
tree[12] = 9
tree[13] = 0
tree[14] = 0


嗯,对比一下上面的图,确实是这样。

更新数值update-单点更新

//参数分别是:原数组a,线段树数组tree,当前结点,建树左、右范围,要改原数组的下标x,要改为的值 
void update(int a[],int tree[],int node,int start,int end,int x,int val)

	//找到a[x],修改值 
	if(start==end)
	
		//改了原数组和线段树的叶子节点 
		a[x]=val;
		tree[node]=val;		
	
	
	else
	
		int mid=(start+end)/2;
		
		int left=2*node+1;
		int right=2*node+2;
		
		//要修改的值在左子树:递归地更新 
		if(x>=start&&x<=mid)
		
			update(a,tree,left,start,mid,x,val); 
		
		else //在右子树 
		
			update(a,tree,right,mid+1,end,x,val);
		
		
		//因为更新了叶子节点,所以也要更新其祖先结点 
		tree[node]=tree[left]+tree[right];
	

让我们看看更新完的树长成什么样:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e3+10;
int a[]=1,3,5,7,9,11;
int size=6;//存数组大小
int tree[N]=0; 

//参数分别是:原数组,线段数组,当前节点,数组左、右范围 
void build(int a[],int tree[],int node,int start,int end) 

	//递归边界:遇到叶子节点时 即[x,x]
	if(start==end)
	
		tree[node]=a[start];
		//当前tree结点值就是a数组的值 因为是叶子节点 
	 
	else
	
		int mid=(start+end)/2;
		
		//求左右节点的下标
		int left=2*node+1;
		int right=2*node+2;
		
		//递归地建左子树 
		build(a,tree,left,start,mid); 
		//递归地建右子树
		build(a,tree,right,mid+1,end); 
		
		//当前结点的值等于左右子节点的值之和 
		tree[node]=tree[left]+tree[right];
	
 

//参数分别是:原数组a,线段树数组tree,当前结点,建树左、右范围,要改原数组的下标x,要改为的值 
void update(int a[],int tree[],int node,int start,int end,int x,int val)

	//找到a[x],修改值 
	if(start==end)
	
		//改了原数组和线段树的叶子节点 
		a[x]=val;
		tree[node]=val;		
	
	
	else
	
		int mid=(start+end)/2;
		
		int left=2*node+1;
		int right=2*node+2;
		
		//要修改的值在左子树:递归地更新 
		if(x>=start&&x<=mid)
		
			update(a,tree,left,start,mid,x,val); 
		
		else //在右子树 
		
			update(a,tree,right,mid+1,end,x,val);
		
		
		//因为更新了叶子节点,所以也要更新其祖先结点 
		tree[node]=tree[left]+tree[right];
	

int main()

	build(a,tree,0,0,5);
	update(a,tree,0,0,5,4,6);//想要把a[4]=6
	for(int i=0;i<=14;i++)
	
		printf("tree[%d] = %d\\n",i,tree[i]);
	 
	return 0;

输出:

tree[0] = 33
tree[1] = 9
tree[2] = 24
tree[3] = 4
tree[4] = 5
tree[5] = 13
tree[6] = 11
tree[7] = 1
tree[8] = 3
tree[9] = 0
tree[10] = 0
tree[11] = 7
tree[12] = 6
tree[13] = 0
tree[14] = 0


对比一下图,嗯,确实是这样的。

求区间和query

操作步骤:

  1. 向下依次寻找包含在目标区间中的区间,并累加
  2. 与建树的函数相比,query函数增加了两个参数L,R,即求a的区间[L,R]的和

假如我们想求:a[2]+a[3]+…a[5],即区间[2,5]=32

query模板代码:

//参数:原数组a、线段树数组tree、当前节点、树的区间范围,要求的区间范围 
int query(int a[],int tree[],int node,int start,int end,int L,int R) 

	//若目标区间与当前区间没有重叠,结束递归返回0
	//这样 start end L R 
	if(start>R||end<L)   
	
		return 0;
	 
	 
	//目标区间包含当时区间,返回节点值 
	// L start end R 
	else if(L<=start&&end<=R)
	
		return tree[node];
	
	
	
	else
	
		int mid=(start+end)/2;
		
		int left=2*node+1;
		int right=2*node+2;
		
		//递归地计算左边区间的值
		int sum_left=query(a,tree,left,start,mid,L,R); 
		//递归地计算右边区间的值
		int sum_right=query(a,tree,right,mid+1,end,L,R);
		
		//相加就是答案
		return sum_left+sum_right; 
			 

总代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e3+10;
int a[]=1,3,5,7,9,11;
int size=6;//存数组大小
int tree[N]=0; 

//参数分别是:原数组,线段数组,当前节点,数组左、右范围 
void build(int a[],int tree[],int node,int start,int end) 

	//递归边界:遇到叶子节点时 即[x,x]
	if(start==end)
	
		tree[node]=a[start];
		//当前tree结点值就是a数组的值 因为是叶子节点 
	 
	else
	
		int mid=(start+end)/2;
		
		//求左右节点的下标
		int left=2*node+1;
		int right=2*node+2;
		
		//递归地建左子树 
		build(a,tree,left,start,mid); 
		//递归地建右子树
		build(a,tree,right,mid+1,end); 
		
		//当前结点的值等于左右子节点的值之和 
		tree[node]=tree[left]+tree[right];
	
 

//参数分别是:原数组a,线段树数组tree,当前结点,建树左、右范围,要改原数组的下标x,要改为的值 
void update(int a[],int tree[],int node,int start,int end,int x,int val)

	//找到a[x],修改值 
	if(start==end)
	
		//改了原数组和线段树的叶子节点 
		a[x]=val;
		tree[node]=val;		
	
	
	else
	
		int mid=(start+end)/2;
		
		int left=2*node+1;
		int right=2*node+2;
		
		//要修改的值在左子树:递归地更新 
		if(x>=start&&x<=mid)
		
			update(a,tree,left,start,mid,x,val); 
		
		else //在右子树 
		
			update(a,tree,right,mid+1,end,x,val);
		
		
		//因为更新了叶子节点,所以也要更新其祖先结点 
		tree[node]=tree[left]+tree[right];
	


//参数:原数组a、线段树数组tree、当前节点、树的区间范围,要求的区间范围 
int query(int a[],int tree[],int node,int start,int end,int L,int R) 

	//若目标区间与当前区间没有重叠,结束递归返回0
	//这样 start end L R 
	if(start>R||end<L)   
	
		return 0;
	 
	 
	//目标区间包含当时区间,返回节点值 
	// L start end R 
	else if(L<=start&&end<=R)
	
		return tree[node];
	
	
	
	else
	
		int mid=(start+end)/2;
		
		int left=2*node+1;
		int right=2*node+2;
		
		//递归地计算左边区间的值
		int sum_left=query(a,tree,left,start,mid,L,R); 
		//递归地计算右边区间的值
		int sum_right=query(a,tree,right,mid+1,end,L,R);
		
		//相加就是答案
		return sum_left+sum_right; 
			 

int main()

	build(a,tree,0,0,5);
	//update(a,tree,0,0,5,4,6);//想要把a[4]=6
	cout<<query(a,tree,0,0,5,2,5);	 
	return 0;

输出:

32

嗯,确实是这样。

完整模板:build、update、query

//参数分别是:原数组,线段数组,当前节点,数组左、右范围 
void build(int a[]ACM学习笔记:线段树

主席树学习笔记

关于线段树的一些学习笔记——(无限施工中)

线段树笔记的补充(一些不太一样的线段树)

线段树笔记的补充(一些不太一样的线段树)

自学笔记-线段树(21.8.9)