Segment Tree 線段樹 原理及實現(xiàn)

關(guān)于我的 Leetcode 題目解答,代碼前往 Github:https://github.com/chenxiangcyr/leetcode-answers


問題的引出

我們有時會遇到這樣的問題: 給你一些數(shù)耳鸯,組成一個序列竞川,如[1 4 2 3],有兩種操做:

  • 操作一:給序列的第i個數(shù)加上X (X可以為負(fù)數(shù))
  • 操作二:詢問序列中最大的數(shù)是什么? 格式query(start, end),表示區(qū)間[start, end]內(nèi)酒唉,最大值是多少矩桂?

我們很容易得到一個樸素的算法: 將這些數(shù)存入一個數(shù)組, 由于要詢問最大值, 我們只需要遍歷這個區(qū)間[start, end]即可,找出最大值痪伦。
對于改變一個數(shù), 我們就在這個數(shù)上加上X侄榴,比如A[i] = A[i] + X雹锣。
這個算法的缺點是什么呢?Query詢問最大值復(fù)雜度O(N), 修改復(fù)雜度為O(1)癞蚕,在有Qquery的情況下這樣總的復(fù)雜度為O(QN), 那么對于查詢來說這樣的復(fù)雜度是不可以接受的蕊爵。

Segment Tree 線段樹

一顆線段樹的構(gòu)造就是根據(jù)區(qū)間的性質(zhì)的來構(gòu)造的, 如下是一棵區(qū)間[0, 3]的線段樹,每個[start, end]都是一個二叉樹中的節(jié)點涣达。

[0, 3]的線段樹

區(qū)間劃分大概就是上述的區(qū)間劃分在辆。可以看出每次都將區(qū)間的長度一分為二,數(shù)列長度為n,所以線段樹的高度是log(n),這是很多高效操作的基礎(chǔ)度苔。
上述的區(qū)間存儲的只是區(qū)間的左右邊界匆篓。我們可以將區(qū)間的最大值加入進(jìn)來,也就是樹中的Node需要存儲leftright左右子節(jié)點外寇窑,還需要存儲start, end, val區(qū)間的范圍和區(qū)間內(nèi)表示的值鸦概。
可以儲存不同的值,例如區(qū)間內(nèi)的最大值甩骏,最小值窗市,區(qū)間的求和等等。

將區(qū)間的最大值加入進(jìn)來

因為每次將區(qū)間的長度一分為二,所有創(chuàng)造的節(jié)點個數(shù)饮笛,即底層有n個節(jié)點咨察,那么倒數(shù)第二次約n/2個節(jié)點,倒數(shù)第三次約n/4個節(jié)點福青,依次類推:

n + 1/2 * n + 1/4 * n + 1/8 * n + ...
=   (1 + 1/2 + 1/4 + 1/8 + ...) * n
=   2n

所以構(gòu)造線段樹的時間復(fù)雜度和空間復(fù)雜度都為O(n)摄狱。

線段樹中的Node如何定義

二叉樹的節(jié)點區(qū)間定義,[start, end]代表節(jié)點的區(qū)間范圍无午,max 是節(jié)點在[start, end]區(qū)間上的最大值 left , right 是當(dāng)前節(jié)點區(qū)間劃分之后的左右節(jié)點區(qū)間:

// 節(jié)點區(qū)間定義
// [start, end] 代表節(jié)點的區(qū)間范圍
// max 是節(jié)點在(start,end)區(qū)間上的最大值
// left , right 是當(dāng)前節(jié)點區(qū)間劃分之后的左右節(jié)點區(qū)間
public class SegmentTreeNode {
    public int start, end, max;
    public SegmentTreeNode left, right;
    public SegmentTreeNode(int start, int end, int max) {
        this.start = start;
        this.end = end;
        this.max = max
        this.left = this.right = null;
    }
}

線段樹區(qū)間最大值維護(hù)

給定一個區(qū)間媒役,我們要維護(hù)線段樹中存在的區(qū)間中最大的值。這將有利于我們高效的查詢?nèi)魏螀^(qū)間的最大值宪迟。給出A數(shù)組酣衷,基于A數(shù)組構(gòu)建一棵維護(hù)最大值的線段樹,我們可以在O(logN)的復(fù)雜度內(nèi)查詢?nèi)我鈪^(qū)間的最大值:

比如原數(shù)組 A = [1, 4, 2, 3]

原數(shù)組 A = [1, 4, 2, 3]

// 構(gòu)造的代碼及注釋
public SegmentTreeNode build(int[] A) {
    // write your code here
    return buildhelper(0, A.length - 1, A);
}
public SegmentTreeNode buildhelper(int left, int right, int[] A){
    if(left > right){
        return null;
    }
    SegmentTreeNode root = new SegmentTreeNode(left, right, A[left]); // 根據(jù)節(jié)點區(qū)間的左邊界的序列值為節(jié)點賦初值
    if(left == right){
        return root; // 如果左邊界和右邊界相等,節(jié)點左邊界的序列值就是線段樹節(jié)點的接節(jié)點值
    }
    int mid = (left + right) / 2; // 劃分當(dāng)前區(qū)間的左右區(qū)間
    root.left = buildhelper(left, mid, A);
    root.right = buildhelper(mid + 1, right, A);
    root.max = Math.max(root.left.max, root.right.max); // 根據(jù)節(jié)點區(qū)間的左右區(qū)間的節(jié)點值得到當(dāng)前節(jié)點的節(jié)點值
    return root;
}

舉一反三:
如果需要區(qū)間的最小值:
root.min = Math.min(root.left.min, root.right.min);
如果需要區(qū)間的和:
root.sum = root.left.sum + root.right.sum;

線段樹的單點更新

原線段樹

更新序列中的一個節(jié)點次泽,如何把這種變化體現(xiàn)到線段樹中去穿仪,例如,將序列中的第4個點A[3]更新為5, 要變動3個區(qū)間中的值,分別為[3,3],[2,3],[0,3]

A[3] 更新為5

提問:為什么需要更新這三個區(qū)間意荤?:因為只有這三個在線段樹中的區(qū)間牡借,覆蓋了3這個點。

更新所以需要從葉子節(jié)點一路走到根節(jié)點, 去更新線段樹上的值袭异。因為線段樹的高度為log(n),所以更新序列中一個節(jié)點的復(fù)雜度為log(n)钠龙。

// 單點更新的代碼及注釋
public void modify(SegmentTreeNode root, int index, int value) {
    // write your code here
    if(root.start == root.end && root.start == index) { // 找到被改動的葉子節(jié)點
        root.max = value; // 改變value值
        return ;
    }
    int mid = (root.start + root.end) / 2; // 將當(dāng)前節(jié)點區(qū)間分割為2個區(qū)間的分割線
    if(index <= mid){ // 如果index在當(dāng)前節(jié)點的左邊
        modify(root.left, index, value); // 遞歸操作
        root.max = Math.max(root.right.max, root.left.max); // 可能對當(dāng)前節(jié)點的影響
    }
    else {            // 如果index在當(dāng)前節(jié)點的右邊
        modify(root.right, index, value); // 遞歸操作
        root.max = Math.max(root.left.max, root.right.max); // 可能對當(dāng)前節(jié)點的影響
    }
    return ;
}

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

線段樹的區(qū)間查詢操作就是將當(dāng)前區(qū)間分解為較小的子區(qū)間,然后由子區(qū)間的最大值就可以快速得到需要查詢區(qū)間的最大值。

線段樹

query(1,3) = max(query(1,1),query(2,3)) = max(4,3) = 4

任意長度的線段,最多被拆分成logn條線段樹上存在的線段碴里,所以查詢的時間復(fù)雜度為O(log(n))沈矿。

// 區(qū)間查詢的代碼及注釋
public int query(TreeNode root, int start, int end) {
    if (start <= root.start && root.end <= end) {
        // 如果查詢區(qū)間在當(dāng)前節(jié)點的區(qū)間之內(nèi),直接輸出結(jié)果
        return root.max;
    }
    int mid = (root.start + root.end) / 2; // 將當(dāng)前節(jié)點區(qū)間分割為左右2個區(qū)間的分割線
    int ans = Integer.MIN_VALUE; // 給結(jié)果賦初值
    if (mid >= start) {   // 如果查詢區(qū)間和左邊節(jié)點區(qū)間有交集,則尋找查詢區(qū)間在左邊區(qū)間上的最大值
        ans = Math.max(ans, query(root.left, start, end));
    }
    if (mid + 1 <= end) { // 如果查詢區(qū)間和右邊節(jié)點區(qū)間有交集,則尋找查詢區(qū)間在右邊區(qū)間上的最大值
        ans = Math.max(ans, query(root.right, start, end));
    }
    return ans; // 返回查詢結(jié)果
}

Segment Tree 線段樹 的數(shù)組實現(xiàn)

雖然 Segment Tree 線段樹邏輯上是一棵二叉樹,但是實際存儲時可以通過數(shù)組來實現(xiàn)咬腋,通過父子節(jié)點的下標(biāo)的數(shù)值關(guān)系可以訪問父節(jié)點的子節(jié)點羹膳。

例如通過如下代碼將一個數(shù)組轉(zhuǎn)換為一個 Segment Tree:

    // a segment tree
    private int[] seg;
    
    // the element count
    private int n;
    
    public NumArray(int[] nums) {
        n = nums.length;
 
        if(n > 0) {
            seg = new int[4 * n];
        
            build(nums, 0, n - 1, 0);
        }
    }
    
    // build segment tree, set the value of seg[idx]
    public void build(int[] nums, int start, int end, int idx) {
        if(start == end) {
            seg[idx] = nums[start];
        }
        else {
            int mid = (start + end) / 2;
            
            // build left tree
            build(nums, start, mid, 2 * idx + 1);
            
            // build right tree
            build(nums, mid + 1, end, 2 * idx + 2);
            
            seg[idx] = seg[2 * idx + 1] + seg[2 * idx + 2];
        }
    }

更新元素:更新元素需要更新兩個地方,一是原數(shù)組對應(yīng)的下標(biāo)的值根竿,另外一個是包含了這個元素的 Segment Tree 中的節(jié)點的值陵像。具體也是通過遞歸實現(xiàn):

    public void update(int i, int val) {
        update(0, n - 1, i, val, 0);
    }
    
    public void update(int start, int end, int i, int val, int idx) {
        // leaf node. update element.
        if (start == end) {
            seg[idx] = val;
            return;
        }
        
        int mid = (start + end) / 2;
        
        // left tree
        if (i <= mid) {
            update(start, mid, i, val, 2 * idx + 1);
        }
        // right tree
        else {
            update(mid + 1, end, i, val, 2 * idx + 2);
        }
        
        seg[idx] = seg[2 * idx + 1] + seg[2 * idx + 2];
    }

區(qū)間求和:區(qū)間求和也是通過遞歸實現(xiàn),關(guān)鍵在于根據(jù)當(dāng)前節(jié)點表示的范圍以及需要求的區(qū)間的范圍的關(guān)系進(jìn)行求和:

    public int sumRange(int i, int j) {
        return sumRange(0, n - 1, i, j, 0);
    }
    
    public int sumRange(int start, int end, int i, int j, int idx) {
        // segment completely outside range, represents a null node
        if (start > j || end < i)
            return 0;
        
        // segment completely inside range
        if (i <= start && j >= end)
            return seg[idx];
        
        int mid = (start + end) / 2;
        
        return sumRange(start, mid, i, j, 2 * idx + 1) +
            sumRange(mid + 1, end, i, j, 2 * idx + 2);
    }

Segment Tree 線段樹 使用示例

LeetCode 題目:

LintCode題目:207. 區(qū)間求和 II
在類的構(gòu)造函數(shù)中給一個整數(shù)數(shù)組, 實現(xiàn)兩個方法 query(start, end)modify(index, value):

  • 對于 query(start, end), 返回數(shù)組中下標(biāo) startend 的 和寇壳。
  • 對于 modify(index, value), 修改數(shù)組中下標(biāo)為 index 上的數(shù)為 value醒颖。

樣例
給定數(shù)組 A = [1,2,7,8,5].
query(0, 2), 返回 10.
modify(0, 4), 將 A[0] 修改為 4.
query(0, 1), 返回 6.
modify(2, 1), 將 A[2] 修改為 1.
query(2, 4), 返回 14.

    /* you may need to use some attributes here */
    
     class SegmentTreeNode {
        public int start, end;
        public int sum;
        public SegmentTreeNode left, right;
        public SegmentTreeNode(int start, int end, int sum) {
              this.start = start;
              this.end = end;
              this.sum = sum;
              this.left = this.right = null;
        }
    }
    SegmentTreeNode root;
    public SegmentTreeNode build(int start, int end, int[] A) {
        // write your code here
        if(start > end) {  // check core case
            return null;
        }
        
        SegmentTreeNode root = new SegmentTreeNode(start, end, 0);
        
        if(start != end) {
            int mid = (start + end) / 2;
            root.left = build(start, mid, A);
            root.right = build(mid+1, end, A);
            
            root.sum = root.left.sum + root.right.sum;
        } else {
            root.sum =  A[start];
            
        }
        return root;
    }
    public int querySegmentTree(SegmentTreeNode root, int start, int end) {
        // write your code here
        if(start == root.start && root.end == end) { // 相等 
            return root.sum;
        }
        
        
        int mid = (root.start + root.end)/2;
        int leftsum = 0, rightsum = 0;
        // 左子區(qū)
        if(start <= mid) {
            if( mid < end) { // 分裂 
                leftsum =  querySegmentTree(root.left, start, mid);
            } else { // 包含 
                leftsum = querySegmentTree(root.left, start, end);
            }
        }
        // 右子區(qū)
        if(mid < end) { // 分裂 3
            if(start <= mid) {
                rightsum = querySegmentTree(root.right, mid+1, end);
            } else { //  包含 
                rightsum = querySegmentTree(root.right, start, end);
            } 
        }  
        // else 就是不相交
        return leftsum + rightsum;
    }
    public void modifySegmentTree(SegmentTreeNode root, int index, int value) {
        // write your code here
        if(root.start == index && root.end == index) { // 查找到
            root.sum = value;
            return;
        }
        
        // 查詢
        int mid = (root.start + root.end) / 2;
        if(root.start <= index && index <=mid) {
            modifySegmentTree(root.left, index, value);
        }
        
        if(mid < index && index <= root.end) {
            modifySegmentTree(root.right, index, value);
        }
        //更新
        root.sum = root.left.sum + root.right.sum;
    }
    /**
     * @param A: An integer array
     */
    public Solution(int[] A) {
        // write your code here
        root = build(0, A.length-1, A);
    }
    
    /**
     * @param start, end: Indices
     * @return: The sum from start to end
     */
    public long query(int start, int end) {
        // write your code here
        return querySegmentTree(root, start ,end);
    }
    
    /**
     * @param index, value: modify A[index] to value.
     */
    public void modify(int index, int value) {
        // write your code here
        modifySegmentTree(root, index, value);
    }
}

LeetCode題目:315. Count of Smaller Numbers After Self
You are given an integer array nums and you have to return a new counts array. The counts array has the property where counts[i] is the number of smaller elements to the right of nums[i].

Example:
Given nums = [5, 2, 6, 1]
To the right of 5 there are 2 smaller elements (2 and 1).
To the right of 2 there is only 1 smaller element (1).
To the right of 6 there is 1 smaller element (1).
To the right of 1 there is 0 smaller element.
Return the array [2, 1, 1, 0].

class Solution {
    class TreeNode {
        int val;
        int count; // **從左往右**,小于該val的數(shù)字的數(shù)目
        int duplicates; // 該val對應(yīng)的數(shù)字的重復(fù)個數(shù)
        
        TreeNode left;
        TreeNode right;
        
        TreeNode(int val) {
            this.val = val;
        }
    }
    
    public List<Integer> countSmaller(int[] nums) {
        Integer[] result = new Integer[nums.length];
        TreeNode root = null;
        
        // 從右往左以此插入數(shù)字
        for(int i = nums.length - 1; i >= 0; i--) {
            root = insert(nums[i], root, result, i, 0);
        }
        
        return Arrays.asList(result);
    }
    
    // 將值為num的節(jié)點插入
    public TreeNode insert(int num, TreeNode node, Integer[] result, int idx, int preSum) {
        // 創(chuàng)建節(jié)點
        if(node == null) {
            node = new TreeNode(num);
            node.count = 0;
            node.duplicates = 1;
            
            result[idx] = preSum;
        }
        // 更新節(jié)點
        else if (node.val == num) {
            node.duplicates++;
            
            result[idx] = preSum + node.count;
        }
        // 在左子樹添加
        else if (node.val > num) {
            node.count++;
            
            node.left = insert(num, node.left, result, idx, preSum);
        }
        // 在右子樹添加
        else {
            node.right = insert(num, node.right, result, idx, preSum + node.duplicates + node.count);
        }
        
        return node;
    }
    
}

總結(jié) - 線段樹問題解決的框架

  • 如果問題帶有區(qū)間操作壳炎,或者可以轉(zhuǎn)化成區(qū)間操作泞歉,可以嘗試往線段樹方向考慮

    • 從面試官給的題目中抽象問題,將問題轉(zhuǎn)化成一列區(qū)間操作匿辩,注意這步很關(guān)鍵
  • 當(dāng)我們分析出問題是一些列區(qū)間操作的時候

    • 對區(qū)間的一個點的值進(jìn)行修改
    • 對區(qū)間的一段值進(jìn)行統(tǒng)一的修改
    • 詢問區(qū)間的和
    • 詢問區(qū)間的最大值腰耙、最小值
      我們都可以采用線段樹來解決這個問題
  • 套用我們前面講到的經(jīng)典步驟和寫法,即可在面試中完美的解決這些題目铲球!

什么情況下挺庞,無法使用線段樹?
如果我們刪除或者增加區(qū)間中的元素稼病,那么區(qū)間的大小將發(fā)生變化选侨,此時是無法使用線段樹解決這種問題的。


引用:
Segment Tree 簡介
A Recursive approach to Segment Trees, range sum queries & lazy propagation
線段樹入門教程

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末溯饵,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子锨用,更是在濱河造成了極大的恐慌丰刊,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,548評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件增拥,死亡現(xiàn)場離奇詭異啄巧,居然都是意外死亡,警方通過查閱死者的電腦和手機掌栅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評論 3 399
  • 文/潘曉璐 我一進(jìn)店門秩仆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人猾封,你說我怎么就攤上這事澄耍。” “怎么了?”我有些...
    開封第一講書人閱讀 167,990評論 0 360
  • 文/不壞的土叔 我叫張陵齐莲,是天一觀的道長痢站。 經(jīng)常有香客問我,道長选酗,這世上最難降的妖魔是什么阵难? 我笑而不...
    開封第一講書人閱讀 59,618評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮芒填,結(jié)果婚禮上呜叫,老公的妹妹穿的比我還像新娘。我一直安慰自己殿衰,他們只是感情好朱庆,可當(dāng)我...
    茶點故事閱讀 68,618評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著播玖,像睡著了一般椎工。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蜀踏,一...
    開封第一講書人閱讀 52,246評論 1 308
  • 那天维蒙,我揣著相機與錄音,去河邊找鬼果覆。 笑死颅痊,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的局待。 我是一名探鬼主播斑响,決...
    沈念sama閱讀 40,819評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼钳榨!你這毒婦竟也來了舰罚?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,725評論 0 276
  • 序言:老撾萬榮一對情侶失蹤薛耻,失蹤者是張志新(化名)和其女友劉穎营罢,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體饼齿,經(jīng)...
    沈念sama閱讀 46,268評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡饲漾,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,356評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了缕溉。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片考传。...
    茶點故事閱讀 40,488評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖证鸥,靈堂內(nèi)的尸體忽然破棺而出僚楞,到底是詐尸還是另有隱情勤晚,我是刑警寧澤,帶...
    沈念sama閱讀 36,181評論 5 350
  • 正文 年R本政府宣布镜硕,位于F島的核電站运翼,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏兴枯。R本人自食惡果不足惜血淌,卻給世界環(huán)境...
    茶點故事閱讀 41,862評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望财剖。 院中可真熱鬧悠夯,春花似錦、人聲如沸躺坟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽咪橙。三九已至夕膀,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間美侦,已是汗流浹背产舞。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留菠剩,地道東北人易猫。 一個月前我還...
    沈念sama閱讀 48,897評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像具壮,于是被迫代替她去往敵國和親准颓。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,500評論 2 359

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

  • 背景 一年多以前我在知乎上答了有關(guān)LeetCode的問題, 分享了一些自己做題目的經(jīng)驗棺妓。 張土汪:刷leetcod...
    土汪閱讀 12,748評論 0 33
  • 再堅硬的內(nèi)心攘已,也有軟弱的一面。就像烏龜堅硬的外殼怜跑,刺猬鋒利的刺样勃,軟弱的一面因為藏在了暗黑面,所以是很難看到和發(fā)現(xiàn)的...
    余生請多多指教3734閱讀 235評論 0 0
  • 長命百歲是吉祥如意的祝福妆艘;長命百歲是兒孫滿堂彤灶;長命百歲是時間最美的凄涼看幼。 清晨的時候批旺,和小豆一起在草叢里玩耍,遍地...
    梅園遺珠閱讀 259評論 0 1