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

本文详细介绍线段树的数据结构,包括其基本概念、性质、作用及实现方法。文章通过实例讲解了如何利用线段树进行区间求和和单点更新操作,并提供了完整的C++代码模板。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

是自学的笔记。
引用和参考:
关于线段树
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[],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; 
	}		 
}

注意:原数组大小为N,树要开4N,如果RE了就开5N

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

karshey

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值