其实关于线段树已经有很多博客的水平远远超过本文,而本文的目的只是在于给出线段树的建立,点修改,求和的不同的并且详细的写法,想给与作者一样的小白一点帮助
·什么是线段树
线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
简单的来说线段树的作用是将一个区间划分为若干个子区间,它的每一个节点代表了一个子区间。线段树总是拥有这样的性质 :对于B>A的区间[A,B]总有其左子节点的区间为[A,(A+B)/2],其右子节点的的区间为[(A+B)/2+1,B]。
作为高级数据结构的一种,我们首先得了解我们使用线段树来干些什么:
1.数字之和——总数字之和 = 左区间数字之和 + 右区间数字之和
2. 最大公因数(GCD)——总GCD = gcd( 左区间GCD , 右区间GCD );
3.最大值——总最大值=max(左区间最大值,右区间最大值)
接下来是一些关于二叉树的原理图
引自 岩之痕 大佬 >https://blog.csdn.net/zearot/article/details/52280189
我们之所以使用线段树的原因,除了其固有的性质外还有很重要的一点在于我们可以用数组实现它,那我们如何用数组建立一个线段树呢?
这里是一个很详细的二叉堆实现过程,实际上的话二叉堆与线段树大同小异,所以将这个链接贴在这里,不明白基本定义可以看一下
skywang12345 大佬 http://www.cnblogs.com/skywang12345/p/3610187.html
几个需要注意的地方做一下笔记:
1.线段树开的是一维数组, 然后我们可以通过这样的计算得出我们应该释放的内存大小。
对于一个长度为N的区间(这里假设了N正好是2的一个n次幂),显然可以计算得到其层数为log2N+1,所以的话对于每一层其节点个数为2n-1,进行求和
∑ i = 1 l o g 2 N + 1 2 i − 1 \displaystyle\sum_{i=1}^{log_2N+1} 2^{i-1}i=1∑log2N+12i−1
结果等于2logN+2=4N
其实说了这么多,结论其实很简单:对于N的数组开4N空间作为线段树。而通过上面的计算也可以很清楚的知道,实际上所需要的空间总是小于4N,如果愿意优化的话也可以通过进一步计算得出。
2.在进行节点访问时我们这样实现(这里以从1开始编号的树为例)
Left_child=Node<<2;//访问该节点的左子节点
Right_child=Node<<2|1;//访问该节点的右子节点
·线段树的实现
一个简单的线段树是由这样的三个部分组成的
1.构造线段树,建树函数
我们一般使用递归自底向上构造线段树,所以Build的作用是 在position这个节点构造一个区间的线段树,且这个节点的区间为 [interval_left,interval_right]
int Build(int interval_left,int interval_right,int position);
2.对线段树进行点修改
由于对于一个元素进行修改,意味着对所有的相关父节点进行修改,所以一般来说使用递归修改,但是其实还有一个更加容易理解的方式,非递归方式、从上至下修改。本文给出非递归做法。
int Update_Loop(int arry_position,int add,int interval_left,int interval_right,int position);
3.进行线段树的查询,实际上是查询某一个区间的值
线段树的区间查询利用的是多个区间合并成一个区间,本文给出一个不那么复杂的写法。
int Query(int L,int R,int interval_left,int interval_right,int position);
接下来是具体的实现方式,并对每一个函数给出一个调用例子,该实例基于一个全局变量数组arry[11],该数组从arry[1]开始赋值,1-10分别赋值1-10
·建树函数
int Assign(int position)
{
seg_tree[position]=seg_tree[position<<1]+seg_tree[position<<1|1];
// 线段树一个节点的值 = 其左子节点的值 + 其右子节点的值
return 0;
}
int Build(int interval_left,int interval_right,int position)//参数分别代表,左区间,右区间,节点在线段树中的节点编号
{
if(interval_left==interval_right){//若到达叶节点
seg_tree[position]=arry[interval_left]; //则此线段树的值就是对应的数组内的值
return 0;//返回
}
int interval_middle;//当区间可继续分割,则计算区间中部,切割区间
interval_middle=(interval_left+interval_right)>>1;
Build(interval_left,interval_middle,position<<1);//对区间左部进行构造
Build(interval_middle+1,interval_right,position<<1|1);//对区间右部进行构造
Assign(position);//上面两行递归可以保证此时该节点的子节点都已经被赋值,故此时更新该节点的值
return 0;
}
此时我们要构造arry[11]的线段树则如此调用:
Build(1,10,1);
在第一个节点构造一个区间为[1,10]的线段树,由于是递归所以如此调用便可以保证线段树构造完全
·点修改函数
int Update_Loop(int arry_position,int add,int interval_left,int interval_right,int position)
{ //这个点在区间的位置,增加多少,左区间 ,右区间 ,节点编号
seg_tree[position]+=add;//将编号为1的节点增加add
int interval_middle;
while( interval_left!=interval_right )//只要还没有抵达叶节点,就继续分割区间
{
interval_middle=(interval_left+interval_right)>>1;
if( arry_position<=interval_middle )//检测arry_position在分割后的左边还是右边
{
interval_right=interval_middle;
position=position<<1;
seg_tree[position]+=add;
}else{
interval_left=interval_middle;
position=position<<1|1;
seg_tree[position]+=add;
}
}
seg_tree[position]+=add;//增加叶节点的值
return 0;
}
假如我们要修改arry[1]的值,把其增加10,则如此调用:
Update_Loop(1,10,1,10,1);
·线段树的查询
//Query的作用是在给定的区间查询一个区间的和的值
//在这里关键的思想在于查询一个区间的值,那么这个值一定是等于二分之后左半部分的值加上右半部分的值
int Query(int L,int R,int interval_left,int interval_right,int position)
{
if(L==interval_left&&R==interval_right)//如果正好线段树记录的左右区间与查询的左右区间一样,则直接返回
return seg_tree[position];
if(interval_left>R||interval_rightR)//如果查询区间不在线段树记录的区间内,则返回0
return 0;
int ans=0;
int interval_middle=(interval_left+interval_right)>>1;
int Middle;//Middle的作用是分割所要查询的区间
if(L<=interval_middle&&R>interval_middle) Middle=interval_middle;
else if(R<=interval_middle) Middle=R;
else Middle=L-1;
ans= Query(L,Middle,interval_left,interval_middle,position<<1)+Query(Middle+1,R,interval_middle+1,interval_right,position<<1|1);
return ans;
}
若查询2-10区间的和,则如此调用:
Query(2,10,1,10,1);
最后总结一下,其实线段树并不是一种非常难得数据结构,它与其他树类型可以说是异曲同工,所以重点是掌握一个,那么其他的也差不多都能掌握