【数据结构】线段树(Segment Tree)

 

假设咱们如今拿到了一个很是大的数组,对于这个数组里面的数字要反复不断地作两个操做。node

一、(query)随机在这个数组中选一个区间,求出这个区间全部数的和。c++

二、(update)不断地随机修改这个数组中的某一个值。算法

时间复杂度:数组

枚举数据结构

枚举L~R的每一个数并累加。ide

  • query:O(n)

找到要修改的数直接修改。函数

  • update:O(1)

若是query与update要作不少不少次,query的O(n)会被卡住,因此时间复杂度会很是慢。那么有没有办法把query的时间复杂度降成O(1)呢?其中一种方法以下:学习

  • 先创建一个与a数组同样大的数组。

  • s[1]=a[1];s[2]=a[1]+a[2];s[3]=a[1]+a[2]+a[3];...;s[n]=a[1]+a[2]+a[3]+...+a[n](在s数组中存入a的前缀和)

  • 此时a[L]+a[L+1]+...+a[R]=s[R]-s[L-1],query的时间复杂度降为O(1)。
  • 但若要修改a[k]的值,随之也需修改s[k],s[k+1],...,s[n]的值,时间复杂度升为O(n)。

前缀和ui

query:O(1)spa

update:O(n)

  • 咱们发现,当咱们想尽方法把其中一个操做的时间复杂度改为O(1)后,另外一个操做的时间复杂度就会变为O(n)。当query与update的操做特别多时,不论用哪一种方法,整体的时间复杂度都不会特别快。
  • 因此,咱们将要讨论一种叫线段树的数据结构,它能够把这两个操做的时间复杂度平均一下,使得query和update的时间复杂度都落在O(n log n)上,从而增长整个算法的效率。

线段树

假设咱们拿到了以下长度为6的数组:

在构建线段树以前,咱们先阐述线段树的性质:

一、线段树的每一个节点都表明一个区间。

二、线段树具备惟一的根节点,表明的区间是整个统计范围,如[1,N]。

三、线段树的每一个叶节点都表明一个长度为1的元区间[x,x]。

四、对于每一个内部节点[l,r],它的左子结点是[l,mid],右子节点是[mid+1,r],其中mid=(l+r)/2(向下取整)。

依照这个数组,咱们构建以下线段树(结点的性质为sum):

若咱们要求[2-5]区间中数的和:

若咱们要把a[4]改成6:

  • 先一层一层找到目标节点修改,在依次向上修改当前节点的父节点。

 

 

接下来的问题是:如何保存这棵线段树?

  • 用数组存储。

若咱们要取node结点的左子结点(left)与右子节点(right),方法以下:

  • left=2*node+1
  • right=2*ndoe+2

举结点5为例(左子结点为节点11,右子节点为节点12):

  • left5=2*5+1=11
  • right5=2*5+2=12

接下来给出建树的代码:

 

#include<bits/stdc++.h> using namespace std; const int N = 1000; int a[] = {1, 3, 5, 7, 9, 11}; int size = 6; int tree[N] = {0}; //创建范围为a[start]~a[end]  void build(int a[], int tree[], int node/*当前节点*/, int start, int end){ //递归边界(即遇到叶子节点时)  if (start == end){ //直接存储a数组中的值  tree[node] = a[start]; } else { //将创建的区间分红两半  int mid = (start + end) / 2; int left = 2 * node + 1;//左子节点的下标  int right = 2 * node + 2;//右子节点的下标 //求出左子节点的值(即从节点left开始,创建范围为a[start]~a[mid])  build(a, tree, left, start, mid); //求出右子节点的值(即从节点right开始,创建范围为a[start]~a[mid]) build(a, tree, right, mid+1, end); //当前节点的职位左子节点的值加上右子节点的值  tree[node] = tree[left] + tree[right]; } } int main(){ //从根节点(即节点0)开始建树,建树范围为a[0]~a[size-1] build(a, tree, 0, 0, size-1); for(int i = 0; i <= 14; i ++) printf("tree[%d] = %d\n", i, tree[i]); return 0; }

运行结果:

update操做:

  • 肯定须要改的分支,向下寻找须要修改的节点,再向上修改节点值。
  •  与建树的函数相比,update函数增长了两个参数x,val,即把a[x]改成val。

例:把a[x]改成6(代码实现)

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) {//若是x在左分支   update(a, tree, start, mid, x, val); } else {//若是x在右分支  update(a, tree, right, mid+1, end, x, val); } //向上更新值  tree[node] = tree[left] + tree[right]; } } 在主函数中调用: //把a[x]改为6 update(a, tree, 0, 0, size-1, 4, 6);

 

运行结果:

query操做:

  • 向下依次寻找包含在目标区间中的区间,并累加。
  • 与建树的函数相比,query函数增长了两个参数L,Rl,即把求a的区间[L,R]的和。

例:求a[2]+a[3]+...+a[5]的值(代码实现)

int query(int a[], int tree[], int node, int start, int end, int L,int R){ //若目标区间与当时区间没有重叠,结束递归返回0  if (start > R || end < L){ return 0; } //若目标区间包含当时区间,直接返回节点值  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; } } 在主函数中调用: //求区间[2,5]的和 int ans = query(a, tree, 0, 0, size-1, 2, 5); printf("ans = %d", ans); 

运行结果:

最后,献上完整的代码:

#include<bits/stdc++.h> using namespace std; const int N = 1000; int a[] = {1, 3, 5, 7, 9, 11}; int size = 6; int tree[N] = {0}; //创建范围为a[start]~a[end]  void build(int a[], int tree[], int node/*当前节点*/, int start, int end){ //递归边界(即遇到叶子节点时)  if (start == end) { //直接存储a数组中的值  tree[node] = a[start]; } else { //将创建的区间分红两半  int mid = (start + end) / 2; int left = 2 * node + 1;//左子节点的下标  int right = 2 * node + 2;//右子节点的下标 //求出左子节点的值(即从节点left开始,创建范围为a[start]~a[mid])  build(a, tree, left, start, mid); //求出右子节点的值(即从节点right开始,创建范围为a[start]~a[mid]) build(a, tree, right, mid+1, end); //当前节点的职位左子节点的值加上右子节点的值  tree[node] = tree[left] + tree[right]; } } 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) {//若是x在左分支   update(a, tree, left, start, mid, x, val); } else {//若是x在右分支  update(a, tree, right, mid+1, end, x, val); } //向上更新值  tree[node] = tree[left] + tree[right]; } } //求a[L]~a[R]的区间和  int query(int a[], int tree[], int node, int start, int end, int L,int R){ //若目标区间与当时区间没有重叠,结束递归返回0  if (start > R || end < L){ return 0; } //若目标区间包含当时区间,直接返回节点值  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(){ //从根节点(即节点0)开始建树,建树范围为a[0]~a[size-1] build(a, tree, 0, 0, size-1); for(int i = 0; i <= 14; i ++) printf("tree[%d] = %d\n", i, tree[i]); printf("\n"); //把a[x]改为6 update(a, tree, 0, 0, size-1, 4, 6); for(int i = 0; i <= 14; i ++) printf("tree[%d] = %d\n", i, tree[i]); printf("\n"); //求区间[2,5]的和 int ans = query(a, tree, 0, 0, size-1, 2, 5); printf("ans = %d", ans); return 0; }

运行结果:

学习视频连接

相关文章
相关标签/搜索