最近在学习线段树这个东东,有点难,但是学完之后确实收获挺多
第一:线段树的优势
对于我们平常对于一段区间的存储,删改,查询区间的值,我们一般会选择用普通的一维数组去存储
如下面:我们输入n个数,求第s个到第t个数的和
#include<cstdio>
#include<iostream>
#include<algorithm>
using namespace std;
int main()
{
int n.s,t;
int sum=0;
cin>>n>>s>>t;
int a[n+1];
for(int i=0;i<n;i++)
cin>>a[i];
for(int i=s-1;i<=t-1;i++)
sum+=a[i];
cout<<sum<<endl;
}
在这段代码中,我们修改某一个值的时间复杂度是O(1),我们求一段区间的和的时间复杂度是O(n);
除此外,我们还会选择前缀和的方式,来应对和的输出
#include<cstdio>
#include<iostream>
#include<algorithm>
using namespace std;
int main()
{
int n,s,t,x;
cin>>n>>s>>t;
int a[n+1];
a[0]=0;
for(int i=1;i<=n;i++)
{
cin>>x;
a[i]=a[i-1]+x;
}
cout<<a[t]-a[s]<<endl;
}
对于上面的代码,其对于求某一段和的时间复杂度是O(1)
但是对于修改某一个数值后的修改的时间复杂度是O(n)。
综上所述,对于对一段数值有着重复求和和修改值的操作时,无论是普通的一维数组还是前缀和的形式,都是有着相当大的时间复杂度。
那么有没有两种操作的时间复杂度都很低的算法,那就是我们今天要讲的线段树。
线段树的原理就是将区间为[1,n]分解成多个小的子区间(不超过4*n),线段树对于上述的两种操作的时间复杂度都是O(logn)的级别,相对于其他两种,时间复杂度降低了不少。
第二:线段树的图解
上图就是对【1~13】所构建的线段树
当我们对任意一区间进行求和时:例如:【6~9】
有如图的操作:
在线段树中,我们只用找到6这个点和(8,9)这个区间,就可以求得(6,9)的和
将6+8+9的三次操作减少到了6+(8,9)两次操作,在区间范围越大的时候,就越明显。
而进行删除操作的时候,线段树的优点也体现了出来
eg:当我们将修改4这个点的值为2时
这个图片上面是区间或者,这个点所代表的值,最下面,是这个点所代表的值。
当我们修改第4点的值为2的时候
修改流程如下:
我们可以很明确的看到,在线段树的修改中,我们只用修改一条线路即可,其时间复杂度为O(logn)
第三:代码解释
首先我们定义一个线段树:
我们先定义arr[]来储存点的值,定义tree[]来存储树的值(即区间的和)
建立线段树:
void build_date(int l,int r,int arr[],int tree,int node)
{
if(l==r)
tree[node]=arr[l];
else
{
int mid=(l+r)>>1;//即(l+r)/2,使用>>1是因为会更快一些
int right_node=node*2+1;
int left_node=node*2;
build_date(l,mid,arr,tree,left_date);
build_date(mid+1,r,arr,tree,right_date);
tree[node]=tree[left_node]+tree[right_node];
}
}
在我们处理左子节点和右子节点的时候:
当根节点为0的时候:left-node=2node+1,right-node=2node+2;
当根结点为1的时候:left-node=2node ,right-node=2node+1;
在build_date中我们通过递归的方式将其tree值算出,其递归终止的条件是当(l==r),即当递归到根节点的时候,我们就跳出递归,执行tree[node]=tree[right_node]+tree[left_node];操作。向上返回,直到根节点,完成线段树的建立。
对于值的更改//指的是对于线段树中某一点的值的更改
void update_tree(int l,int r,int arr[],int tree[],int node,int idx,int val)
//即把idx处的值改为val
{
if(l==r)
{
arr[idx]=val;
tree[idx]=val;
}
else
{
int mid=(l+r)>>1;
int left_node=node<<1;
int right_node=node<<1|1;
if(idx>=l&&mid>=idx)//即在左区间时
update_tree(l,r,arr,tree,left_node,idx,val);
else
update_tree(l,r,arr,tree,right_node,idx,val);
tree[node]=tree[left_node]+tree[right_node];
}
}
对于点的更改,我们先递归至(l==r),即为叶子节点的时候就终止递归
否则,当idx>=l&&idx<=mid的时候,即当需要改变的值在当前的node的左子树的时候,向其左子树递归,否则,向其右子树递归,直到( l == r )的时候,停止递归,向前需要改变的父节点依次回归改变其值。
查询某一区间的和
int query_tree(int l,int r,int node,int arr[],int tree[],int s,int t)
//s,t分别为起点与终点
{
if(l>s||r<t)//即完全不相容的情况
return 0;
else if(l==r)
return tree[node];
else if(l=>s&&r<=t)
return tree[node];
else
{
int mid=(l+r)>>1;
int left_node=(l+r)<<1;
int right_node=(l+r)<<1|1;
int sum_left=query(l,mid,left_node,arr,tree,s,r);
int sum_right=query(mid+1,r,right_node,arr,tree,s,t);
return sum_left+sum_right;
}
}
我知道有很多人都有疑惑,为什么不在对左右子树递归的时候加上if(如果只在左子树上||只在右子树上)的判断,因为在开始有:if(s<l||r<t) return 0;的判断语句,可以在某一方被完全不包括时,在第一次判断的时候就return 0;跳出,不会增大时间复杂度;
终于讲完了,最后给各位如下完整的代码:
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
int maxl=1000;//如果用99999这种数字,tree[99999]所占用的连续存储过大,电脑内存会不够,会爆掉
void build_tree(int arr[],int tree[],int node,int start,int endll)//建立一个线段树
{
if(start==endll)
tree[node]=arr[start];//在叶节点时,返回此点的值(在叶节点的时候,tree的值和arr的值是相等的)
else
{
int mid=(start+endll)/2;
int left_node=2*node+1;
int right_node=2*node+2;
build_tree(arr,tree,left_node,start,mid);//向左边递归
build_tree(arr,tree,right_node,mid+1,endll);//向右边递归
tree[node]=tree[left_node]+tree[right_node];//求node点的值
}
}
void update_tree(int arr[],int tree[],int node,int start,int endll,int idx,int val)//在线段树中,进行值的更替
//idx和val的意思是:eg:arr[idx]=val,即把第idx个格子改成val
{
if(start==endll)
{
arr[idx]=val;
tree[node]=val;//在叶子处对idx处的tree和arr的值进行更新
}
else
{
int mid=(start+endll)/2;
int left_node=2*node+1;
int right_node=2*node+2;
if(idx>=start&&idx<=mid)//在左区间时
update_tree(arr,tree,left_node,start,mid,idx,val);
else//在右区间时
update_tree(arr,tree,right_node,mid+1,endll,idx,val);
tree[node]=tree[left_node]+tree[right_node];//对更新的叶子节点值往上的tree的值进行更新
}
}
int query_tree(int arr[],int tree[],int node,int start,int endll,int L,int R)//求区间的和
{
if(R<start||L>endll)//区间完全不相容
return 0;
else if(start==endll)//当计算到叶节点时
return tree[node];//返回点的值
else if(L<=start&&endll<=R)//当一个node节点区域完全被所求的区间包括时,无需对此区域的子节点区域进行访问
return tree[node];//直接返回该node节点的值(对时间复杂度进行优化)
else
{
int mid=(start+endll)/2;
int left_node=2*node+1;
int right_node=2*node+2;
int sum_left=query_tree(arr,tree,left_node,start,mid,L,R);
int sum_right=query_tree(arr,tree,right_node,mid+1,endll,L,R);
return sum_left+sum_right;
}
}
int main()
{
int arr[]={
1,3,5,7,9,11};//即各个点的值
int sizel=6;
int tree[maxl]={
0};//树上的点和的值
build_tree(arr,tree,0,0,sizel-1);
update_tree(arr,tree,0,0,sizel-1,4,6);
for(int i=0;i<15;i++)
cout<<"tree["<<i<<"]="<<tree[i]<<endl;
int s=query_tree(arr,tree,0,0,sizel-1,2,5);
cout<<"2~5的和为:"<<s<<endl;
return 0;
}