數(shù)據(jù)結(jié)構(gòu)與算法-線段樹

數(shù)據(jù)結(jié)構(gòu)與算法-線段樹

圖片來自慕課網(wǎng)辅柴,liuyubobobo講師的課程“玩轉(zhuǎn)數(shù)據(jù)結(jié)構(gòu) 從入門到進(jìn)階”

線段樹介紹

有時(shí)候需要對某個(gè)區(qū)間進(jìn)行操作,比如求和伪节、求最大值最/小值等。下面的問題:

有數(shù)組arr = [2,6,3,5,7,1]臀脏,求數(shù)組任意[L, R]區(qū)間上的和虏冻。

最直觀的方法就是對[L, R]區(qū)間遍歷求和肤粱,最差情況時(shí)間復(fù)雜度O(N)。使用線段樹可以達(dá)到O(lg n)的時(shí)間復(fù)雜度厨相。

什么是線段樹领曼?線段樹又叫區(qū)間樹,將某個(gè)區(qū)間用一個(gè)樹結(jié)點(diǎn)表示蛮穿,樹可以像二叉堆那樣用數(shù)組在構(gòu)建庶骄。如下圖所示數(shù)組A的長度是8,根結(jié)點(diǎn)表示整個(gè)區(qū)間践磅,即[0, 7]单刁;根結(jié)點(diǎn)的左孩子表示區(qū)間的左半部分[0, 3],右孩子表示區(qū)間的右半部分[4, 7]府适。根據(jù)二叉樹的遞歸結(jié)構(gòu)羔飞,對于A[0, 3]A[4, 7]像上面一樣繼續(xù)構(gòu)建。當(dāng)達(dá)到樹的葉子結(jié)點(diǎn)時(shí)檐春,區(qū)間中只有一個(gè)元素逻淌。

更廣泛的說,線段樹的每個(gè)結(jié)點(diǎn)都表示數(shù)組A的[L, R]區(qū)間疟暖,在樹的葉子結(jié)點(diǎn)處卡儒,L == R,是單元素區(qū)間俐巴,表示數(shù)組中的某一個(gè)元素骨望。

我們要找某個(gè)[L, R]區(qū)間上的某種值(最大、最小窜骄、和)锦募,只需要從對應(yīng)的樹結(jié)點(diǎn)上獲取即可。比如求A[2, 3]的和邻遏,只需要像二叉搜索樹一樣糠亩,從根結(jié)點(diǎn)出發(fā)虐骑、選擇左孩子A[0, 3]再選擇右孩子A[2, 3]返回其值即可。但是樹結(jié)點(diǎn)表示的區(qū)間并不能囊括所有的區(qū)間可能赎线。比如現(xiàn)在要求A[2, 5]的和廷没。其實(shí)可以看作是A[2, 3]A[4, 5]兩個(gè)結(jié)點(diǎn)的結(jié)點(diǎn)的和,也就是結(jié)果分散在樹中的多個(gè)結(jié)點(diǎn)中垂寥;或者說可以將區(qū)間拆分成多個(gè)子區(qū)間颠黎,不斷拆分,直到能在樹中找到對應(yīng)的區(qū)間滞项,然后將找到的所有結(jié)點(diǎn)的結(jié)果匯總起來即可狭归。因此,要求A[2, 6]其實(shí)就是將A[2, 3]文判、A[4, 5]过椎、A[6]的和匯總起來。

image

上面元素個(gè)數(shù)是8戏仓,樹的最后一層剛好可以容納下所有的元素疚宇,此時(shí)樹是一棵滿二叉樹。但是如果元素個(gè)數(shù)有10個(gè)的話赏殃,這一層就容納不下了敷待,因此需要再增加一層,如下仁热,此時(shí)樹不是滿二叉樹榜揖,甚至完全二叉樹也不是。但是線段樹是平衡二叉樹股耽。

image

為了將樹表示成滿二叉樹根盒,可以將null結(jié)點(diǎn)也補(bǔ)上。

image

我們還看到對于奇數(shù)個(gè)元素的數(shù)組物蝙,區(qū)間不能平均分炎滞,可以固定讓左孩子的區(qū)間比右孩子的區(qū)間小。如上圖中的A[2]A[3, 4]诬乞。

那么對于n個(gè)元素的數(shù)組册赛,需要多大的空間(包含補(bǔ)上的null結(jié)點(diǎn))?

根據(jù)滿二叉樹的性質(zhì)震嫉,樹的最后一層結(jié)點(diǎn)個(gè)數(shù)是2 ^ {k -1}森瘪,而樹總的結(jié)點(diǎn)樹是2^k - 1,可以認(rèn)為樹的最后一層結(jié)點(diǎn)個(gè)數(shù)是樹總結(jié)點(diǎn)的一半票堵。

image
image

因?yàn)槲覀儾豢紤]往線段樹中添加元素扼睬,因此使用4n大小的靜態(tài)數(shù)組即可,雖然大多數(shù)情況下會有很多null結(jié)點(diǎn)浪費(fèi)了數(shù)組空間悴势,但是能保證在最壞情況下也不至于數(shù)組下標(biāo)越界窗宇。

線段樹的實(shí)現(xiàn)

根據(jù)上面對線段樹的描述措伐,樹的葉子結(jié)點(diǎn)存放的是數(shù)組中的每一個(gè)元素。我們需要一個(gè)data數(shù)組作為傳入數(shù)組的一個(gè)副本军俊,同時(shí)用一個(gè)tree數(shù)組表示線段樹侥加。樹的根結(jié)點(diǎn)使用tree[0],表示了數(shù)組的整個(gè)區(qū)間。對于任何一個(gè)非葉子結(jié)點(diǎn)i,其左孩子在數(shù)組中的2 * i + 1處粪躬,右孩子在數(shù)組中的2 * i + 2處担败。

線段樹的實(shí)現(xiàn)骨架如下

package tree;

import java.util.Arrays;

public class SegmentTree<E> {
    /**
     * 存放數(shù)據(jù)的數(shù)組
     */
    private E[] data;
    /**
     * 線段樹,使用4n的靜態(tài)空間
     */
    private E[] tree;
    private int size;

    public int size() {
        return size;
    }

    public E get(int index) {
        if (index < 0 || index >= size) {
            throw new ArrayIndexOutOfBoundsException(index);
        }
        return data[index];
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("[");
        for (int i = 0; i < tree.length; i++) {
            sb.append(tree[i]);
            if (i != tree.length - 1) sb.append(", ");
        }
        return sb.append("]").toString();
    }

}


get方法簡單的返回傳入數(shù)組中index處的元素而已镰官。

現(xiàn)在要將傳入的數(shù)組轉(zhuǎn)換成線段樹提前,以方便對任何區(qū)間進(jìn)行操作。這個(gè)操作可以是求和朋魔,求最大/最小等等岖研。不如實(shí)現(xiàn)一個(gè)接口,讓用戶自己實(shí)現(xiàn)要完成的操作警检。如下Merger接口,對兩個(gè)元素操作返回一個(gè)結(jié)果害淤。

package tree;

public interface Merger<E> {
    E merge(E a, E b);
}

來看構(gòu)造方法扇雕,data是傳入的數(shù)組尔邓,Merger就是我們要進(jìn)行的操作地熄。

public SegmentTree(E[] data, Merger<E> merger) {
    this.size = data.length;
    this.merger = merger;
    this.data = Arrays.copyOf(data, data.length);
    this.tree = (E[]) new Object[4 * data.length];
    buildSegmentTree(0, 0, data.length - 1);
}

比如要求區(qū)間和,匿名內(nèi)部類就可以寫成下面這樣

new Merger<Integer>() {
    @Override
    public Integer merge(Integer a, Integer b) {
        return a + b;
    }
});

線段樹的構(gòu)建

構(gòu)造方法中的buildSegmentTree(treeIndex, L, R);方法是關(guān)鍵莹捡。從根結(jié)點(diǎn)開始崭放,遞歸地構(gòu)建線段樹:

  • 遞歸終止條件是到樹的葉子結(jié)點(diǎn)哨苛。即區(qū)間的L和R相等時(shí),此時(shí)區(qū)間中只有一個(gè)元素币砂,將data[l]或者data[r]值賦給treeIndex處的結(jié)點(diǎn)即可建峭,表示treeIndex處的結(jié)點(diǎn)保存著該單元素區(qū)間的值。
  • 否則决摧,遞歸的構(gòu)建左右子樹:先找到當(dāng)前結(jié)點(diǎn)的左亿蒸、右孩子treeIndex,然后將區(qū)間[l, r]平分成兩部分掌桩,左子樹leftChild表示區(qū)間[l, mid]边锁,右子樹rightChild表示區(qū)間[mid + 1, r],在這兩個(gè)區(qū)間遞歸的構(gòu)建左右子樹
  • 左右子樹構(gòu)建好了之后波岛,可以利用左右孩子的區(qū)間值匯總成根結(jié)點(diǎn)的值(樹的后序遍歷思想)茅坛,因?yàn)楦Y(jié)點(diǎn)的區(qū)間正是左右孩子表示區(qū)間的結(jié)合。

代碼如下

/**
 * 構(gòu)建線段樹
 * @param treeIndex SegmentTree中以treeIndex為根的樹
 * @param l treeIndex對應(yīng)的樹的左區(qū)間
 * @param r treeIndex對應(yīng)的樹的右區(qū)間
 */
private void buildSegmentTree(int treeIndex, int l, int r) {
    // 葉子結(jié)點(diǎn)则拷,左右區(qū)間一樣贡蓖,index處存放的數(shù)據(jù)是data[l]或者data[r]
    if (l == r) {
        tree[treeIndex] = data[l];
        return;
    }
    int leftChild = 2 * treeIndex + 1;
    int rightChild = leftChild + 1;
    int mid = (r - l) / 2 + l;
    buildSegmentTree(leftChild, l, mid);
    buildSegmentTree(rightChild, mid + 1, r);
    // 左右樹建立好后可以針對具體業(yè)務(wù)場景曹鸠,將左右樹結(jié)果“匯總”到當(dāng)前樹結(jié)點(diǎn)中
    // 如對區(qū)間求和那么 tree[treeIndex] = tree[leftChild] + tree[rightChild]
    // 再如求區(qū)間的最大/最小值 tree[treeIndex] = Math.max(tree[leftChild], tree[rightChild])
    tree[treeIndex] = merger.merge(tree[leftChild], tree[rightChild]);
}

線段樹的區(qū)間查詢

這是核心操作,用于查詢區(qū)間[L, R]上的某操作后的值摩梧。

public E query(int queryL, int queryR) {
    if (queryL < 0 || queryR < 0 || queryL >= data.length || queryR >= data.length || queryL > queryR) {
        throw new IllegalArgumentException("index is illegal");
    }
    return query(0, 0, data.length - 1, queryL, queryR);
}

/**
 * 查詢[queryL, queryR]之間的結(jié)果
 * @param treeIndex SegmentTree中以treeIndex為根的樹
 * @param l treeIndex對應(yīng)的樹的左區(qū)間
 * @param r treeIndex對應(yīng)的樹的右區(qū)間
 * @param queryL 查找的左范圍
 * @param queryR 查找的右范圍
 * @return [queryL, queryR]之間的結(jié)果
 */
private E query(int treeIndex, int l, int r, int queryL, int queryR) {
    // 如果查找的區(qū)間剛好樹的區(qū)間對應(yīng)上了物延,直接返回treeIndex處的結(jié)果
    if (l == queryL && r == queryR) {
        return tree[treeIndex];
    }
    int leftChild = 2 * treeIndex + 1;
    int rightChild = leftChild + 1;
    int mid = (r - l) / 2 + l;
    // 如果查找的左范圍比mid還大,只需要在右子樹中查找
    if (queryL > mid) return query(rightChild, mid + 1, r, queryL, queryR);
    // 如果查找的右范圍比mid + 1小仅父,只需要在左子樹中查找
    if (queryR < mid + 1) return query(leftChild, l, mid, queryL, queryR);
    // 在左右子樹查找并將結(jié)果融合叛薯,將查詢的區(qū)間[queryL,queryR]拆分成:在左子樹中查找[queryL, mid]笙纤,在右子樹中查找[mid + 1, queryR]
    return merger.merge(query(leftChild, l, mid, queryL, mid), query(rightChild, mid + 1, r, mid + 1, queryR));
}

如果要查詢的區(qū)間[queryL, queryR]剛好和樹中某個(gè)結(jié)點(diǎn)表示的區(qū)間一樣耗溜,便可以直接返回。否則在其左省容、右子樹中查詢抖拴,如果要查詢的區(qū)間左端點(diǎn)比左子樹的右端點(diǎn)還大,那么無需查詢左子樹腥椒,直接返回右子樹的查詢結(jié)果即可阿宅;類似的,如果要查詢的區(qū)間右端點(diǎn)比右子樹的左端點(diǎn)還小笼蛛,就不必在右子樹中查詢了洒放,直接返回左子樹的查詢結(jié)果即可;否則滨砍,要查詢的區(qū)間覆蓋了左右子樹往湿,需要將[queryL, queryR]拆分成[queryL,mid][]需要將左右子樹的結(jié)果匯總。

線段樹的更新

線段樹的更新將除了index處元素更新成e惋戏,由于data數(shù)組改變领追,tree也需要更新。和構(gòu)建線段樹一樣响逢, L == R時(shí)在tree中找到了存放data[index]的結(jié)點(diǎn)绒窑,更新并返回;否則龄句,如果查找的index比mid大回论,說明要更新的結(jié)點(diǎn)在右子樹中,如果比mid + 1小說明要更新的結(jié)點(diǎn)在左子樹中分歇。和在buildSegmentTree中一樣傀蓉,在set方法中更新了葉子結(jié)點(diǎn)的值后,其上直到根結(jié)點(diǎn)的父結(jié)點(diǎn)都需要更新职抡。

public void set(int index, E e) {
    if (index < 0 || index >= size) {
        throw new ArrayIndexOutOfBoundsException(index);
    }
    data[index] = e;
    // 接下來更新tree,從根結(jié)點(diǎn)開始
    set(0, 0, data.length - 1,index, e);

}

private void set(int treeIndex,int l, int r, int index, E e) {
    if (l == r) {
        tree[treeIndex] = e;
        return;
    }
    int leftChild = 2 * treeIndex + 1;
    int rightChild = leftChild + 1;
    int mid = (r - l) / 2 + l;
    if (index > mid) set(rightChild, mid + 1, r, index, e);
    if (index < mid + 1) set(leftChild, l, mid, index, e);
    tree[treeIndex] = merger.merge(tree[leftChild], tree[rightChild]);
}

測試

如下葬燎,對于數(shù)組[2, 3, 5, 6],求[1, 3]區(qū)間上的和。

public static void main(String[] args) {
    Integer[] data = {2, 3, 5, 6};

    SegmentTree<Integer> segmentTree = new SegmentTree<>(data, (a, b) -> a + b);
    System.out.println(segmentTree.query(1,3)); // 14

    segmentTree.set(2, 10);
    System.out.println(segmentTree.query(1,3)); // 19

}

set前和是14谱净,set后數(shù)組變成[2, 3, 10, 6]再查詢結(jié)果是19窑邦。


@sunhaiyu
2018.11.24

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市壕探,隨后出現(xiàn)的幾起案子冈钦,更是在濱河造成了極大的恐慌,老刑警劉巖李请,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瞧筛,死亡現(xiàn)場離奇詭異,居然都是意外死亡导盅,警方通過查閱死者的電腦和手機(jī)较幌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來白翻,“玉大人乍炉,你說我怎么就攤上這事÷蒜桑” “怎么了岛琼?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長巢株。 經(jīng)常有香客問我衷恭,道長,這世上最難降的妖魔是什么纯续? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮灭袁,結(jié)果婚禮上猬错,老公的妹妹穿的比我還像新娘。我一直安慰自己茸歧,他們只是感情好倦炒,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著软瞎,像睡著了一般逢唤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上涤浇,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天鳖藕,我揣著相機(jī)與錄音,去河邊找鬼只锭。 笑死著恩,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播喉誊,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼邀摆,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了伍茄?” 一聲冷哼從身側(cè)響起栋盹,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎敷矫,沒想到半個(gè)月后例获,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡沪饺,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年躏敢,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片整葡。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡件余,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出遭居,到底是詐尸還是另有隱情啼器,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布俱萍,位于F島的核電站端壳,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏枪蘑。R本人自食惡果不足惜损谦,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望岳颇。 院中可真熱鬧照捡,春花似錦、人聲如沸话侧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽瞻鹏。三九已至悲立,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間新博,已是汗流浹背薪夕。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留叭披,地道東北人寥殖。 一個(gè)月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓玩讳,卻偏偏與公主長得像,于是被迫代替她去往敵國和親嚼贡。 傳聞我的和親對象是個(gè)殘疾皇子熏纯,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容