【數據結構】線段樹(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; }

運行結果:

學習視頻連接

相關文章
相關標籤/搜索