线段树笔记

来源:互联网 发布:网络维修属于什么工作 编辑:程序博客网 时间:2024/06/01 09:00

线段树是是平衡二叉树,每个节点对应一段区间,线段树通过将整个区间不断划分为更小的区间并记录上这个区间上的信息来实现快速查询

比如,给一个数组,每个数组元素存放一个数值,允许修改数组部分元素的值,最后查询某一连续区间上数值的和
首先是建树,因为是平衡二叉树,我们可以直接用数组来存放
平衡二叉树下父母节点的编号是N,那么左孩子节点的编号就是2*N,右孩子节点的编号是2*N+1
我们可以先写一个函数来进行区间求和

long long sum(int* Array,int left,int right){   int s=0;   for(int i=left;i<=right;i++)      s+=Array[i];   return s;}
建树
//Array是存放数据的数组,tree是存放线段树的数组,空间是Array的四倍,left和right是建树的区间,可以只是整个Array数组的一部分void build(int* Array,int* tree,int left,int right,int num){   tree[num]=sum(Array,left,right);              //tree[num]记录的就是[left,right]区间上的和,num是递数组编号   if(right>left){       //递归的终止条件是达到最大划分的时候停止,也就是left==right的时候      int middle=(left+right)/2;      build(Array,tree,left,middle,num<<1);      //这里是位运算效率会比较高,num<<1等价于num*2      build(Array,tree,middle+1,right,num<<1|1); //同理num<<1|1等价于num*2+1   }}
线段树是如何实现的快速查询?我们假设有数组a(n),1<n<2000,数值随机,我们要查询 [520 , 1314] 这个区间上的数值和,在建树过程中我们已经将1~2000的数组划分成了大大小小的子数组的和,我们在建树过程中已经算出来了[520,521],[522,524],[525,531],[532,562],[563,625],[626,750],[751,1000],[1001,1250],[1250,1312],[1313,1314]这些区间的和了,如果我们在Array数组中查询和,我们需要计算 794 数组元素的和,而在线段树中我们只需要查询上面的10个区间对应的10个tree数组中元素并计算其和就能得到结果了,这样就大大减少了运算。
下面是代码部分
//tree是存放线段树的数组,我们要求和的区间是[start,end],left和right是当次调用函数查询的区间的左右,num传递查询区间对应的数组下标long long query(int* tree,int left,int right,int start,int end,int num){   int middle=(left+right)/2;   if(start==left&&end==right)   //case1      return tree[num];   else if(end<=middle)          //case2      return query(tree,left,middle,start,end,num<<1);   else if(start>middle)         //case3      return query(tree,middle+1,right,start,end,num<<1|1);   else                          //case4      return (query(tree,left,middle,start,middle,num<<1)+query(tree,middle+1,right,middle+1,end,num<<1|1));}
我对比上面查询[520,1314]区间和选取出的10个区间,我们先找到中点middle=(left+right)/2,现在查询时有下面四种情况
case1:[left,right]区间恰好就是我们要查询的[start,end]区间,我们就可以直接返回这个区间对应的节点记录的值
case2:[start,end]区间完全位于[left,middle]区间内,这时我们递归在更小的范围[left,middle]内查找[start,end]
case3:[start,end]区间完全位于[middle+1,right]区间内,和上面一样,缩小范围递归查找
case4:这种情况是[start,end]区间跨过中点middle,则我们将[start,end]拆分为两个区间[start,middle],[middle+1,right],然后再分别查找到这两个子区间的和再相加
比如对 1~2000进行第一次划分,中点是1000,[520,1314]跨过了1000,则进行第四种操作,然后递归下去,我们就能查找到上面的10个区间的和,可以看出,上面10个区间的合并是由case4操作进行的。
修改操作
void add(int* tree,int left,int right,int num,int data){   int n=1;   while(left<right){      tree[n]+=data;      int middle=(left+right)/2;      if(num<=middle){         n=n*2;         right=middle;      }      else{         n=n*2+1;         left=middle+1;      }   }   tree[n]+=data;  //在修改最后的一个需要修改的tree数组元素前循环已经结束,所以单独处理}
这里和原理和query相类似,我们如果要修改Array数组的一个元素的值,我们就必须更新所有包含这个元素的线段树的节点
比如说我们将Array[1]的值加1,则包含Array[1]的区间[1,2000],[1,1000],[1,500],[1,250]....[1,1]对应的tree数组元素应该相应的加1.
修改完全可以通过递归来实现。


原创粉丝点击