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

一 什么是線段樹悲酷?

線段樹也叫區(qū)間樹言询;線段樹是一種二叉搜索樹,它將一個(gè)區(qū)間劃分成一些單元區(qū)間真屯,每個(gè)單元區(qū)間對(duì)應(yīng)線段樹中的一個(gè)葉結(jié)點(diǎn)脸候;

二 為什么要使用線段樹?

在解釋這個(gè)問題先讓我們看一個(gè)經(jīng)典的問題绑蔫,區(qū)間染色;

假設(shè)有一面墻泵额,長(zhǎng)度為n配深,每次選擇一段墻來進(jìn)行染色,如下圖所示:
區(qū)間染色1
首先定義一端長(zhǎng)度為n的墻嫁盲;
區(qū)間染色2

然后將4-9這個(gè)區(qū)間染成黃色篓叶;
區(qū)間染色3
在將7-15染成綠色;
區(qū)間染色4
再將1-5染成藍(lán)色羞秤;
區(qū)間染色5
再將6-12染成紅色缸托;
最后問題來了:

1 問m次操作后我們可以看見多少種顏色?
2 m次操作后瘾蛋,我們可以在[i, j]區(qū)間看見多少種顏色俐镐?
不管怎樣,對(duì)于這個(gè)問題來說哺哼,我們關(guān)注的是一個(gè)一個(gè)的區(qū)間佩抹,其實(shí)對(duì)于這個(gè)問題只需要兩種操作就可以搞定,一個(gè)是染色操作(也就是所謂的區(qū)間更新)取董,一個(gè)是查詢操作(區(qū)間查詢)棍苹;但是由于我們這個(gè)是使用的數(shù)組來定義的一面墻,所以查詢和更新的操作的時(shí)間復(fù)雜度都為O(n)級(jí)別的茵汰,如果我們直接使用數(shù)組進(jìn)行操作系統(tǒng)的系統(tǒng)開銷就太大了枢里;因?yàn)閷?duì)于這個(gè)問題我們關(guān)注點(diǎn)的是一個(gè)一個(gè)的區(qū)間,所以線段樹在這里就有了用武之地了;

接下來我們?cè)诳匆粋€(gè)計(jì)算機(jī)領(lǐng)域經(jīng)典的區(qū)間查詢問題
區(qū)間查詢1

查詢一個(gè)區(qū)間[i, j]的最大值 最小值或者該區(qū)間的數(shù)字之和栏豺;
之前我們的數(shù)據(jù)結(jié)構(gòu)都是對(duì)單個(gè)的數(shù)據(jù)進(jìn)行操作梭灿,顯然是不適用于這這樣的場(chǎng)景的;這里如果我們將這個(gè)問題換成對(duì)區(qū)間內(nèi)的數(shù)據(jù)進(jìn)行操作會(huì)是怎么樣的冰悠?其實(shí)這個(gè)問題的本質(zhì)就是基于區(qū)間的統(tǒng)計(jì)查詢堡妒。

放在現(xiàn)在的互聯(lián)網(wǎng)環(huán)境下也有很多這樣的問題需要使用到區(qū)間查詢的操作來完成?
比如:一個(gè)電商網(wǎng)站去年一年注冊(cè)的用戶中消費(fèi)最高的用戶是誰溉卓?消費(fèi)最少的用戶是誰皮迟?

其實(shí)這樣的問題我們需要注意的是:我們關(guān)注的仍然是動(dòng)態(tài)的情況,在這種情況下我們使用線段樹會(huì)是一個(gè)很好的選擇桑寨;因?yàn)閯?dòng)態(tài)統(tǒng)計(jì)的時(shí)候會(huì)伴有兩個(gè)操作一個(gè)更新一個(gè)查詢伏尼,如果使用數(shù)組來進(jìn)行操作會(huì)極大的增加性能開銷;接下來我們來對(duì)比一下線段樹和數(shù)組在對(duì)這類問題方面的性能開銷
性能對(duì)比

三 線段樹概念詳解

以上的實(shí)例我們都可以將數(shù)組直接轉(zhuǎn)換成線段樹尉尾,
轉(zhuǎn)換過程1

這個(gè)一個(gè)數(shù)組爆阶,通過以上的實(shí)例我們不難發(fā)現(xiàn)在線段樹中是沒有添加和刪除操作的,所以轉(zhuǎn)換以后的線段樹是這樣的
線段樹圖示
前邊也說過線段樹是二分搜索樹的一種沙咏,不同的地方在與線段樹每個(gè)節(jié)點(diǎn)存儲(chǔ)的是一個(gè)區(qū)間辨图,我們以求區(qū)間數(shù)字之和的問題來進(jìn)行解析,上圖中每一個(gè)節(jié)點(diǎn)存儲(chǔ)一個(gè)區(qū)間的數(shù)據(jù)肢藐,例如根節(jié)點(diǎn)存儲(chǔ)的就是整個(gè)數(shù)組的數(shù)據(jù)故河,左右兩個(gè)子節(jié)點(diǎn)存儲(chǔ)的就是[0 - 3][4 - 7]這個(gè)區(qū)間的數(shù)據(jù),以此類推吆豹;如果我們要操作這些區(qū)間數(shù)據(jù)我們只需要找到這些節(jié)點(diǎn)即可鱼的,以數(shù)組中[4 - 7]這個(gè)區(qū)間為例:
線段樹求和圖示
對(duì)于線段樹來說有時(shí)候我們需要查詢的區(qū)間需要進(jìn)行一次合成的操作,比如說在這個(gè)實(shí)例中我們要查詢區(qū)間[2 - 5]的數(shù)據(jù)痘煤,就會(huì)變成下邊的情況:
線段樹求和圖示

我們需要先找到A[2 - 3]和A[4 - 5]這兩個(gè)節(jié)點(diǎn)凑阶,然后對(duì)這兩個(gè)節(jié)點(diǎn)在進(jìn)行合并的操作;
所以在大數(shù)量的情況下衷快,如果我們要操作區(qū)間數(shù)據(jù)的話宙橱,使用線段樹可以很高效的解決我們的問題,而不會(huì)像使用數(shù)組那樣需要先遍歷一邊所有的元素烦磁,這就是線段樹的優(yōu)勢(shì)所在养匈。

四 線段樹的基礎(chǔ)表示

1 在之前的例子中我們看到了線段樹是一個(gè)二分搜索樹,但是線段樹不一定是一顆滿的二叉樹都伪,比如說如下圖所示:
當(dāng)數(shù)組為10個(gè)元素的時(shí)候線段樹的圖示1

這是一位呕乎,我們的數(shù)組是10個(gè)元素的數(shù)組,根節(jié)點(diǎn)保存著10個(gè)元素的數(shù)據(jù)陨晶,根節(jié)點(diǎn)下邊的左右子節(jié)點(diǎn)分別保存5個(gè)元素猬仁,但是左右子節(jié)點(diǎn)在往下分的話因?yàn)?不能被整除的元素就導(dǎo)致其下邊的節(jié)點(diǎn)必然會(huì)出現(xiàn)元素個(gè)數(shù)不一致的情況帝璧,就像這樣的情況:
當(dāng)數(shù)組為10個(gè)元素的時(shí)候線段樹的圖示2
從圖示中可以看出,這個(gè)線段樹的葉子節(jié)點(diǎn)不一定是在最下邊一層的湿刽,這也表示線段樹不一定是滿的二叉樹同時(shí)也不一定是完全二叉樹的烁,但是線段樹確實(shí)一顆平衡二叉樹;
平衡二叉樹:從根節(jié)點(diǎn)開始到葉子節(jié)點(diǎn)的深度的差值(最大深度和最小深度)不超過1诈闺,從這里我們也可以得出我們的堆也是一顆平衡二叉樹渴庆,因?yàn)橥耆鏄浔旧砭褪且环N平衡二叉樹;
  1. 平衡二叉樹的好處就是不會(huì)像二分搜索樹那樣退化成一個(gè)鏈表雅镊,在平衡二叉樹上進(jìn)行搜索是非常高效的襟雷;
  1. 我們可以使用數(shù)組來表示平衡二叉樹,因?yàn)槲覀兛梢詫?shù)組A看成是一個(gè)完全二叉樹仁烹,雖然它最底層的葉子節(jié)點(diǎn)有一些是沒有的耸弄,我們可以將這些節(jié)點(diǎn)看成null,這樣這個(gè)平衡二叉樹就會(huì)變成完全二叉樹卓缰,而完全二叉樹我們完全可以使用數(shù)組來表示计呈;
  1. 那么問題就來了,如果這個(gè)區(qū)間有n個(gè)元素征唬,我們使用數(shù)組表示需要多少個(gè)節(jié)點(diǎn)捌显?
    線段樹存儲(chǔ)空間圖示1

    如圖所示,如果區(qū)間中有n個(gè)元素的話鳍鸵,我們的空間需要2n苇瓣,因?yàn)樯线叺目臻g是下邊空間之和,但是這有一個(gè)最壞的情況偿乖,就是我們的數(shù)組是奇數(shù)的,比如說大小為5這樣的情況哲嘲,那么此時(shí)的線段樹存儲(chǔ)空間就應(yīng)該是下邊這樣的
    線段樹存儲(chǔ)空間圖示2
    因?yàn)閰^(qū)間元素個(gè)數(shù)為n的話贪薪,我們需要2n的空間來進(jìn)行存儲(chǔ),如果多出一個(gè)元素的話我們就需要4n的空間來進(jìn)行存儲(chǔ)眠副,最后我們的線段樹模型就應(yīng)該像下邊圖示的那樣
    線段樹存儲(chǔ)空間圖示3
    我們將空的節(jié)點(diǎn)全部復(fù)制為null画切,這樣就可以讓它滿足完全二叉樹的定義;

    注意:在這里我們是浪費(fèi)了一些存儲(chǔ)空間的囱怕,由于我們定義的線段樹是沒有插入操作的也就是說是靜態(tài)的霍弹,那么我們?yōu)榱诵阅苁峭耆梢誀奚暨@些空間的;

代碼實(shí)現(xiàn)線段樹基礎(chǔ):

package com.mufeng.segmentTree;

/**
 * Created by wb-yxk397023 on 2018/7/8.
 */
public class SegmentTree<E> {

    // 線段樹使用的數(shù)組
    private E[] tree;

    private E[] data;

    public SegmentTree(E[] arr){
        data = (E[]) new Object[arr.length];

        for (int i = 0; i < arr.length; i++){
            data[i] = arr[i];
        }

        tree = (E[]) new Object[4 * arr.length];
    }

    /**
     * 獲取數(shù)組長(zhǎng)度
     * @return
     */
    public int getSize(){
        return data.length;
    }

    /**
     * 獲取當(dāng)前索引上的元素
     * @param index
     * @return
     */
    public E get(int index){
        if (index < 0 || index >= data.length){
            throw new IllegalArgumentException("Index is illega.");
        }
        return data[index];
    }

    /**
     * 返回完全二叉樹的數(shù)組表示中娃弓,一個(gè)索引上元素的左節(jié)點(diǎn)索引典格;
     * @param index
     * @return
     */
    private int leftChild(int index){
        return 2 * index + 1;
    }

    /**
     * 返回完全二叉樹的數(shù)組表示中,一個(gè)索引上元素的右節(jié)點(diǎn)索引台丛;
     * @param index
     * @return
     */
    private int rightChild(int index){
        return 2 * index + 2;
    }
}

五 創(chuàng)建線段樹

  1. 首先以求和為例我們先來看一下線段樹的模型
    線段樹求和模型1

    在這個(gè)圖示中耍缴,我們的數(shù)組長(zhǎng)度為10,所以線段樹的根節(jié)點(diǎn)存儲(chǔ)的就是10個(gè)元素的和,下邊的左右子節(jié)點(diǎn)以及各個(gè)節(jié)點(diǎn)存儲(chǔ)的都是相應(yīng)區(qū)間元素的和防嗡,如果要?jiǎng)?chuàng)建這樣的一個(gè)線段樹我們就需要使用遞歸的方法進(jìn)行創(chuàng)建变汪;

代碼實(shí)現(xiàn)創(chuàng)建過程
private Merger<E> merger;

    public SegmentTree(E[] arr, Merger<E> merger){

        this.merger = merger;

        data = (E[]) new Object[arr.length];

        for (int i = 0; i < arr.length; i++){
            data[i] = arr[i];
        }

        tree = (E[]) new Object[4 * arr.length];
        buildSegmentTree(0, 0, data.length - 1);
    }

    /**
     * 在treeIndex的位置創(chuàng)建表示區(qū)間[l....r]的線段樹
     * @param treeIndex
     * @param l
     * @param r
     */
    private void buildSegmentTree(int treeIndex, int l, int r){
        if (l == r){
            tree[treeIndex] = data[l];
            return;
        }

        int leftTreeIndex = leftChild(treeIndex);
        int rightTreeIndex = rightChild(treeIndex);

        int mid = l + (r - l) / 2;

        buildSegmentTree(leftTreeIndex, l, mid);
        buildSegmentTree(rightTreeIndex, mid + 1, r);

        tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]);
    }

Merger接口的實(shí)現(xiàn)

package com.mufeng.segmentTree;

/**
 * Created by wb-yxk397023 on 2018/7/8.
 */
public interface Merger<E> {
    E merge(E a, E b);
}
重寫toString
/**
     * 重寫toString方法
     * @return
     */
    @Override
    public String toString(){
        StringBuilder res = new StringBuilder();
        res.append('[');
        for (int i = 0; i < tree.length; i++){
            if (tree[i] != null){
                res.append(tree[I]);
            }else {
                res.append("null");
            }

            if (i != tree.length - 1){
                res.append(", ");
            }
        }
        res.append(']');
        return res.toString();
    }
測(cè)試
package com.mufeng;

import com.mufeng.segmentTree.SegmentTree;

public class Main {

        public static void main(String[] args) {

            Integer[] nums = {-2, 0, 3, -5, 2, -1};
            
            SegmentTree<Integer> segTree = new SegmentTree<>(nums, (a, b) -> a + b);
            
            System.out.println(segTree);
        }
}
測(cè)試結(jié)果

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

基于遞歸我們可以很輕松的實(shí)現(xiàn)線段樹的區(qū)間查詢操作,
線段樹的區(qū)間查詢1

比如說基于這個(gè)線段樹我們要查詢區(qū)間為2-5的統(tǒng)計(jì)信息蚁趁;
線段樹的區(qū)間查詢2
我們可以先從根節(jié)點(diǎn)進(jìn)行查詢裙盾;
線段樹的區(qū)間查詢3
根據(jù)圖示我們可以得出要查詢[2,5]這個(gè)區(qū)間的數(shù)據(jù)就需要查詢根節(jié)點(diǎn)下左右兩個(gè)節(jié)點(diǎn)的元素,左節(jié)點(diǎn)查詢[2,3]他嫡,右節(jié)點(diǎn)查詢[4,5]番官;
線段樹的區(qū)間查詢4

由于左右[2,3]和[4,5]這兩個(gè)節(jié)點(diǎn)都有父節(jié)點(diǎn),在這里我們就可以使用遞歸進(jìn)行查詢涮瞻;

代碼實(shí)現(xiàn)線段樹區(qū)間的查詢操作:

/**
     * 返回區(qū)間[queryL,queryR]的值
     * @param queryL
     * @param queryR
     * @return
     */
    public E query(int queryL, int queryR){

        if(queryL < 0 || queryL >= data.length ||
                queryR < 0 || queryR >= data.length || queryL > queryR)
            throw new IllegalArgumentException("Index is illegal.");

        return query(0, 0, data.length - 1, queryL, queryR);
    }

    /**
     * 線段樹區(qū)間查詢的核心方法
     * 在以treeID為根的線段樹中[l...r]的范圍里鲤拿,查找[queryL...queryR]的值
     * @param treeIndex
     * @param l
     * @param r
     * @param queryL
     * @param queryR
     * @return
     */
    private E query(int treeIndex, int l, int r, int queryL, int queryR){

        if (l == queryL && r == queryR){
            return tree[treeIndex];
        }

        int mid = l + (r - l) / 2;
        int leftTreeIndex = leftChild(treeIndex);
        int rightTreeIndex = rightChild(treeIndex);

        if (queryL >= mid + 1){
            return query(rightTreeIndex, mid + 1, r, queryL, queryR);
        }else if (queryR <= mid){
            return query(leftTreeIndex, l, mid, queryL, queryR);
        }

        E legtResult = query(leftTreeIndex, l, mid, queryL, mid);
        E rightResult = query(rightTreeIndex, mid + 1, r, mid + 1, queryR);

        return merger.merge(legtResult, rightResult);
    }

測(cè)試:

public static void main(String[] args) {

        Integer[] nums = {-2, 0, 3, -5, 2, -1};

        SegmentTree<Integer> segTree = new SegmentTree<>(nums,
                (a, b) -> a + b);
        System.out.println(segTree);

        System.out.println(segTree.query(0, 2));
        System.out.println(segTree.query(2, 5));
        System.out.println(segTree.query(0, 5));
    }
測(cè)試結(jié)果

七 線段樹中的更新的操作

1?? 代碼實(shí)現(xiàn)更新操作
/**
     * 將index位置上的值更新為e
     * @param index
     * @param e
     */
    public void set(int index, E e){
        if (index < 0 || index >= data.length){
            throw new IllegalArgumentException("Index is illega.");
        }

        data[index] = e;

        set(0, 0, data.length - 1, index, e);
    }

    /**
     * 在以treeIndex為根的線段樹中,更新index的值為e
     * @param treeIndex
     * @param l
     * @param r
     * @param index
     * @param e
     */
    private void set(int treeIndex, int l, int r, int index, E e){
        if (l == r){
            data[treeIndex] = e;
            return;
        }

        int mid = l + (r - l) / 2;
        int leftTreeIndex = leftChild(treeIndex);
        int rightTreeIndex = rightChild(treeIndex);

        if (index >= mid + 1){
            set(rightTreeIndex, mid + 1, r, index, e);
        }else {
            set(leftTreeIndex, l, mid, index, e);
        }

        tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]);
    }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末署咽,一起剝皮案震驚了整個(gè)濱河市近顷,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌宁否,老刑警劉巖窒升,帶你破解...
    沈念sama閱讀 222,183評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異慕匠,居然都是意外死亡饱须,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門台谊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蓉媳,“玉大人,你說我怎么就攤上這事锅铅±疑耄” “怎么了?”我有些...
    開封第一講書人閱讀 168,766評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵盐须,是天一觀的道長(zhǎng)玩荠。 經(jīng)常有香客問我,道長(zhǎng)贼邓,這世上最難降的妖魔是什么阶冈? 我笑而不...
    開封第一講書人閱讀 59,854評(píng)論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮塑径,結(jié)果婚禮上女坑,老公的妹妹穿的比我還像新娘。我一直安慰自己晓勇,他們只是感情好堂飞,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評(píng)論 6 398
  • 文/花漫 我一把揭開白布灌旧。 她就那樣靜靜地躺著,像睡著了一般绰筛。 火紅的嫁衣襯著肌膚如雪枢泰。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,457評(píng)論 1 311
  • 那天铝噩,我揣著相機(jī)與錄音衡蚂,去河邊找鬼。 笑死骏庸,一個(gè)胖子當(dāng)著我的面吹牛毛甲,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播具被,決...
    沈念sama閱讀 40,999評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼玻募,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了一姿?” 一聲冷哼從身側(cè)響起七咧,我...
    開封第一講書人閱讀 39,914評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎叮叹,沒想到半個(gè)月后艾栋,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,465評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蛉顽,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評(píng)論 3 342
  • 正文 我和宋清朗相戀三年蝗砾,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片携冤。...
    茶點(diǎn)故事閱讀 40,675評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡悼粮,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出曾棕,到底是詐尸還是另有隱情矮锈,我是刑警寧澤,帶...
    沈念sama閱讀 36,354評(píng)論 5 351
  • 正文 年R本政府宣布睁蕾,位于F島的核電站,受9級(jí)特大地震影響债朵,放射性物質(zhì)發(fā)生泄漏子眶。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評(píng)論 3 335
  • 文/蒙蒙 一序芦、第九天 我趴在偏房一處隱蔽的房頂上張望臭杰。 院中可真熱鬧,春花似錦谚中、人聲如沸渴杆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽磁奖。三九已至囊拜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間比搭,已是汗流浹背冠跷。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留身诺,地道東北人蜜托。 一個(gè)月前我還...
    沈念sama閱讀 49,091評(píng)論 3 378
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像霉赡,于是被迫代替她去往敵國(guó)和親橄务。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評(píng)論 2 360

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