一 概述
线段树,类似区间树,它在各个节点保存一条线段(数组中的一段子数组),主要用于高效解决连续区间的动态查询问题,由于二叉结构的特性,它基本能保持每个操作的复杂度为O(logn)。
线段树的每个节点表示一个区间,子节点则分别表示父节点的左右半区间,例如父亲的区间是[a,b],那么(c=(a+b)/2)左儿子的区间是[a,c],右儿子的区间是[c+1,b]。
二。从一个例子来理解线段树
给定一个数字n ,初始化数组arr(下标从1开始)为1 2 3 4 5 6........n,求任意区间段的数字之和
例如n为100 1到100 的和为5050;1到2 的和为 3;50到51的和为101
对这个问题一个简单的解法是:遍历数组区间累加求和,时间复杂度是O(n),额外的空间复杂度O(1)。当数据量特别大,而查询操作 很频繁的时候,耗时可能会不满足需求。
另一种方法:使用一个数组(num)在存入数值时求出当前位置(m)之前的和,求a到b区间数字的和直接就num[b]-num[a];
但是多次更新arr数组某个位置的值,会使操作很麻烦,而且费时间
我们可以用线段树来解决这个问题:预处理耗时O(n),查询、更新操作O(logn),需要额外的空间O(n)。根据这个问题我们构造如下的二叉树
- 叶子节点是原始组数arr中的元素
- 非叶子节点代表它的所有子孙叶子节点所在区间的
1.创建线段数
对于线段树我们可以选择和普通二叉树一样的链式结构。由于线段树是完全二叉树,我们也可以用数组来存储,下面的讨论及代码都是数组来存储线段树,节点结构如下(注意到用数组存储时,有效空间为2n-1,实际空间确不止这么多,比如上面的线段树中叶子节点1、3虽然没有左右子树,但是的确占用了数组空间,实际空间是满二叉树的节点数目: , 是树的高度,但是这个空间复杂度也是O(n)的 )。
树结点
struct node{
int vel;
}
代码:
#include<stdio.h> #include<algorithm> #define MAX 1000 #define Inf 0x3f3f3f3f using namespace std; struct node{ int vel; }tree[MAX*4]; //定义线段树 /* 功能:新建线段树 root:当前线段树的根结点下标 arr: 用来构造线段树的数组 left:树组起始下标 right:数组结束下标 */ void build(int root,int arr[],int left,int right){ if(left==right){ //找到叶结点 tree[root].vel=arr[left]; return ; } int mid=(left+right)/2; build(root*2,arr,left,mid); //递归构造左子树 build(root*2+1,arr,mid+1,right);//递归构造右子树 //根据左右子树根节点的值,更新当前根节点的值 tree[root].vel=tree[root*2].vel+tree[root*2+1].vel; //存放对应区间段的和 } /* 功能:线段树的区间查询 root:当前线段树的根节点下标 [nleft, nright]: 当前节点所表示的区间 [qletf, qright]: 此次查询的区间 */ int query(int root,int nleft,int nright,int qleft,int qright){ if(qleft>nright||qright<nleft) return 0; //查询区间于当前区间没有交集 if(qleft<=nleft&&qright>=nright) return tree[root].vel; //当前区间在查询区间内 int mid=(nleft+nright)/2; if(qleft>mid) return query(root*2+1,mid+1,nright,qleft,qright); //查询区间在当前区间的右区间内 if(qright<=mid) return query(root*2,nleft,mid,qleft,qright);//查询区间在当前区间的左区间内 //查询区间在当前区间的中间区间内 //先求在当前区间左边部分的和,再求在当前区间右边部分的和 二者相加 else return query(root*2,nleft,mid,qleft,mid)+query(root*2+1,mid+1,nright,mid+1,qright); } /* 功能:更新线段树中某个叶子节点的值 root:当前线段树的根节点下标 [nleft, nright]: 当前节点所表示的区间 pos: 待更新节点在原始数组arr中的下标 newnum: 更新的值(原来的值换成newnum) */ void updateOne(int root,int nleft,int nright,int pos,int newnum){ if(nleft==nright){ //找到相应的结点 tree[root].vel=newnum; //更新结点的值 return; } int mid=(nleft+nright)/2; if(pos<=mid) //在左子树中更新 updateOne(root*2,nleft,mid,pos,newnum); else //在右子树中更新 updateOne(root*2+1,mid+1,nright,pos,newnum); //根据左右子树的值回溯更新当前节点的值 tree[root].vel=tree[root*2].vel+tree[root*2+1].vel; } int main(){ int i,n; int arr[MAX]; scanf("%d",&n); for(i=1;i<=n;i++)//输入arr数组的值从1到n arr[i]=i; build(1,arr,1,n); /* for(i=1;i<50;i++) printf("tree[%d] : %d\n",i,tree[i].vel);*/ // printf("请输入要查询的次数:\n"); int m; scanf("%d",&m); //输入要查询的次数 for(i=0;i<m;i++){ int a,b; printf("请输入要查询的区间:\n"); scanf("%d%d",&a,&b); printf("%d--%d sum : %d\n",a,b,query(1,1,n,a,b)); } int pos,num; printf("请输入要更新点的位置及新值:\n"); scanf("%d%d",&pos,&num); updateOne(1,1,n,pos,num); printf("请输入要查询的次数:\n"); scanf("%d",&m); //输入要查询的次数 for(i=0;i<m;i++){ int a,b; printf("请输入要查询的区间:\n"); scanf("%d%d",&a,&b); printf("%d--%d sum : %d\n",a,b,query(1,1,n,a,b)); } return 0; }