線段樹

線段樹

本文總結自己學習線段樹的一些知識點。我最初是通過區間最值查詢問題學習到線段樹,查詢一個區間的最值可以使用RMQ離線算法,該離線算法需要O(nlgn)的預處理時間和O(1)的查詢時間。但是一個區間的某個值修改後,又需要重新計算,對於區間的值頻繁的修改的情況,RMQ離線算法並不合適。

線段樹確是可以針對區間的值頻繁的修改的情況作出應對。線段樹是用O(lgn)的時間處理修改,用O(lgn)的時間進行區間最值查詢,相對RMQ離線算法來講是一種折中。


定義

先來看一下百度百科的定義…:

線段樹是一種二叉搜索樹,與區間樹相似,它將一個區間劃分成一些單元區間,每個單元區間對應線段樹中的一個葉結點。

對於線段樹中的每一個非葉子節點[a,b],它的左兒子表示的區間爲[a,(a+b)/2],右兒子表示的區間爲[(a+b)/2+1,b]。因此線段樹是平衡二叉樹,最後的子節點數目爲N,即整個線段區間的長度。

下面就是一個簡單的線段樹的結構(摘自網絡)

這裏寫圖片描述

給定10個元素的值,按照其順序最終構成了線段樹的10個葉子節點。根節點表示區間[1,10]的最值;假設我們要初始化確定區間[1,5]的最小值,(1+5)/2 =3 那麼分爲以下幾步:

  • 確定其左半邊區間[1,3]的最小值

  • 確定其右半邊的[4,5]的最小值

  • 取[1,3]區間和[4,5]區間的之中的最小值

區間[1.3]和區間[4,5]可以遞歸的操作下去,那麼構建一個線段樹的時間複雜度爲O(N)(可自行計算)

此外,對於每個查詢操作都可以在O(lgN)(樹的高度)時間內完成;同理修改某個值後影響的區間數目也是在O(lgN)量級。

線段樹的操作

線段樹構建

線段樹的構建需要O(N)的時間,構建之後未優化的樹的空間複雜度是2N. 構建的過程用遞歸實現較容易理解,對於一個區間節點,構建好左區間兒子和右區間兒子之後,將該區間的最值設定爲min(tree[lson].s,tree[rson].s)。代碼如下:

struct TreeNode{//線段樹節點
    int l, r;
    int s;//區間最值
}tree[max*2];

int a[max];//對應區間裏面每個元素的值

void bulidSegTree(int i,int left ,int right)
{
    tree[i].l =left;
    tree[i].r = right;
    if(left == right)
    {
        tree[i].s = a[left];
        return;
    }
    int mid = left+(right-left)/2;
    bulidSegTree(lson,left,mid);
    bulidSegTree(rson,mid+1,right);

    tree[i].s = min(tree[lson].s,tree[rson].s);
}

查詢

要查詢一個區間[a,b]的最值,我們首先要從線段樹的根節點開始查詢,若根節點的表示的區間爲[l,r](一開始l<=a<=b<=r)。那麼需要判斷[a,b]是落在[l,r]的哪個區間當中,mid= l+(l-r)/2,有三種情況:

  • [a,b]落在了[l,r]左邊,那麼我們只需要在該節點的左子樹中查找對應[a,b]區間的最值
  • [a,b]落在了[l,r]右邊,那麼我們只需要在該節點的右子樹中查找對應[a,b]區間的最值
  • [a,b]橫跨了[l,r]的左右兩半邊,因此要取區間[a,mid]和[mid+1,b]兩個最小值中的最小值

同時根據平衡二叉樹的性質,我們可以通過節點的下標i,迅速定位到其左孩子和右孩子的下標:
lson = i*2(等價於i<<1)
rson = i*2+1(等價於i<<1|1)

代碼如下:


#define lson (i<<1)
#define rson (i<<1|1)

//查詢區間的最值
int Query(int l, int r, int i) 
{
    if( l==tree[i].l && r== tree[i].r ) return tree[i].s;

    int mid = tree[i].l +(tree[i].r-tree[i].l)/2;

    if(mid<l)
        return Query(l, r, rson);//在右半邊
    else if(mid>=r)
        return Query(l,r,lson);//在左半邊
    else
        return min(Query(l, mid, lson), query(mid+1, r, rson));
}

線段樹的查詢區間總是在減小的,並且查詢的深度等同於樹的高度,因此線段樹的查詢時間複雜度爲O(lgN).

修改

假設我們修改了某個葉子節點的值,我們只需要把該的節點的所有祖先節點的應對最值進行修改,時間複雜度是O(lgN),這事相對於RMQ離線算法的一大優勢。思路大致和查詢一致,判斷要改的點區間是落在當前的左半邊還是右半邊,遞歸的修改完子樹節點的值之後,再更新當前節點最值。
修改代碼如下:

/**
*l代表要修改元素的下標(對應於某一個葉子節點)
*num是要被設定的新的值
*i表示當前的線段樹節點的下標
*/
void modify(int l, int num, int i)
{
    if(l == tree[i].l && l == tree[i].r) {
          tree[i].s = num;
          return;
     }
     int mid = tree[i].l +(tree[i].r-tree[i].l)/2;
     if(mid < l) {
          modify(l, r, num, rson);
     }
     else {
          modify(l, r, num, lson);     
     }

     tree[i].s = min(tree[lson].s,tree[rson].s);
}

總結

本文簡單介紹了線段樹以及線段樹的基本操作,希望自己以後常回顧,不斷鞏固線段樹的知識,並期待加以應用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章