資料結構:線段樹 Segment Tree

介紹

線段樹(Segment Tree)是一種常見的資料結構,用於解決區間查詢(range query)的問題

它主要用於處理數列或數組的區間操作,如區間求和、區間最大值、區間最小值等。

線段樹的基本思想是將原始的數據按照一定的方式分割成若干個區間,並在每個區間上存儲相應的信息,如區間的總和、最大值、最小值等。這樣,在需要查詢某個區間的統計信息時,可以通過合併相應區間的信息快速計算得到。

一般而言,線段樹是一種二叉樹結構每個節點表示一個區間,而樹的根節點表示整個數列或數組的區間。
每個節點包含了該區間的統計信息,以及指向左右子節點的指標。

來自於:https://yuihuang.com/hdu-4027/

基礎操作

建構線段樹的過程可以使用遞迴或迭代的方式進行。具體而言,建構過程中,將原始數據切分為兩半,然後遞迴構建左子樹和右子樹,最終將左子樹和右子樹的統計信息合併到當前節點。

線段樹的查詢操作也可以使用遞迴或迭代的方式進行。當查詢的區間與節點表示的區間有交集時,需要遞迴查詢左子節點和右子節點。如果查詢的區間完全包含在節點表示的區間中,則可以直接返回該節點的統計信息。

線段樹的修改操作通常也使用遞迴或迭代的方式進行。當需要修改某個位置的值時,需要遞迴更新涉及該位置的所有節點,以維護其統計信息的準確性。

時間複雜度,推薦必看👍

資料來自菜鳥工程師肉豬的部落格,推薦超好懂 👍:
【為什麼Binary Search 二元搜索法的時間複雜度是O(log(n))】

優點

線段樹的優點在於可以在O(logN)的時間複雜度下完成區間操作,其中N表示數列或數組的長度
它在許多需要頻繁進行區間操作的應用場景中具有重要的應用價值,如區間求和、區間最值查詢、區間更新等。

問題與限制

然而,線段樹也有一些限制和注意事項。首先,它需要較多的內存空間來存儲統計信息,因此在處理大型數據集時可能會面臨內存限制的問題。此外,線段樹的建構過程較為複雜,並且對於動態更新的情況,可能需要進行結構調整,增加了一定的複雜度。

結論

總體而言,線段樹是一種強大的數據結構,可以高效地處理區間操作的問題。在算法競賽和一些需要快速解決區間查詢的應用中,線段樹是一個重要的工具。

以下是使用 JavaScript 實現線段樹的基本範例碼:

class SegmentTree {
    constructor(nums) {
        this.nums = nums;
        this.tree = new Array(nums.length * 4); // 建立線段樹陣列
        this.buildTree(0, 0, nums.length - 1);
    }

    // 建立線段樹
    buildTree(node, start, end) {
        if (start === end) {
            this.tree[node] = this.nums[start];
        } else {
            const mid = Math.floor((start + end) / 2);
            const leftChild = 2 * node + 1;
            const rightChild = 2 * node + 2;

            this.buildTree(leftChild, start, mid);
            this.buildTree(rightChild, mid + 1, end);

            this.tree[node] = this.tree[leftChild] + this.tree[rightChild]; // 修改這裡可以實現不同的區間操作
        }
    }

    // 區間查詢
    query(node, start, end, left, right) {
        if (left > end || right < start) {
            return 0; // 區間不相交,返回初始值(這裡假設初始值為 0)
        } else if (left <= start && right >= end) {
            return this.tree[node]; // 區間完全包含在節點表示的區間中,返回該節點的值
        } else {
            const mid = Math.floor((start + end) / 2);
            const leftChild = 2 * node + 1;
            const rightChild = 2 * node + 2;
            const sumLeft = this.query(leftChild, start, mid, left, right);
            const sumRight = this.query(rightChild, mid + 1, end, left, right);
            return sumLeft + sumRight; // 修改這裡可以實現不同的區間操作
        }
    }

    // 修改數組中某個位置的值
    update(node, start, end, index, value) {
        if (start === end) {
            this.tree[node] = value;
        } else {
            const mid = Math.floor((start + end) / 2);
            const leftChild = 2 * node + 1;
            const rightChild = 2 * node + 2;
            if (index >= start && index <= mid) {
                this.update(leftChild, start, mid, index, value);
            } else {
                this.update(rightChild, mid + 1, end, index, value);
            }
            this.tree[node] = this.tree[leftChild] + this.tree[rightChild]; // 修改這裡可以實現不同的區間操作
        }
    }
}

// 測試
const nums = [1, 3, 5, 7, 9, 11];
const segmentTree = new SegmentTree(nums);

console.log(segmentTree.query(0, 0, nums.length - 1, 1, 4)); // 計算索引 1 到 4 的區間和,應為 24
segmentTree.update(0, 0, nums.length - 1, 2, 6); // 修改索引 2 的值為 6
console.log(segmentTree.query(0, 0, nums.length - 1, 1, 4)); // 再次計算索引 1 到 4 的區間和,應為 25

/**
 *               【以數字來分類】
 *             [1, 3, 5, 7, 9, 11]
 *              /              \
 *         [1, 3, 5]       [7, 9, 11] 
 *            /  \             /     \
 *         [1, 3] [5]       [7, 9]   [11]
 *          / \               /   \
 *        [1] [3]           [7]   [9] 
 * 
 * 
 * 
 *               【以 index 來分類】
 *                   (0){0-5}
 *               /             \
 *          (1){0-2}           (2){3-5}
 *          /     \            /       \
 *      (3){0-1} (4){2-2}  (5){3-4} (6){5-5}
 *       /    \              /    \ 
 *  (7){0-0} (8){1-1}   (11){3-3} (12){4-4}
 * 
 * 
 * 計算索引 1 到 4 的區間和:
 * 先找到 {0-2}
 *  往下再找 {0-1}
 *   往下再找 {1-1} 得到 {1} 的值 3
 *  找 {2-2} 得到 {2} 的值 5
 * 
 * 找 {3-5}
 *  往下再找 {3-4} 得 {3, 4} 的值 16
 *  
 * 結束尋找
 * 將找到的值加總,總和為 24
 * 
 */

上述範例中,我們首先創建了一個 SegmentTree 類,並在建構函數中初始化數組 nums 和線段樹 tree

然後,我們實現了線段樹的三個基本操作:buildTree 用於建立線段樹、query 用於執行區間查詢、update 用於修改數組中某個位置的值。

最後,我們使用測試數組 [1, 3, 5, 7, 9, 11] 創建了一個線段樹 segmentTree,並通過 query 方法計算了索引 1 到 4 的區間和。 接著,我們使用 update 方法將索引 2 的值修改為 6,並再次使用 query 方法計算了索引 1 到 4 的區間和,驗證了修改操作的正確性。

這只是線段樹的一個基本實現示例,你可以根據需要進一步擴展和修改線段樹的功能,以滿足不同的區間操作需求。

解釋

              【以數字來分類】
            [1, 3, 5, 7, 9, 11]
             /              \
        [1, 3, 5]       [7, 9, 11] 
           /  \             /     \
        [1, 3] [5]       [7, 9]   [11]
         / \               /   \
       [1] [3]           [7]   [9] 



              【以 index 來分類】
        表達格式:(node 編號){index-index}

                  (0){0-5}
              /             \
         (1){0-2}           (2){3-5}
         /     \            /       \
     (3){0-1} (4){2-2}  (5){3-4} (6){5-5}
      /    \              /    \ 
 (7){0-0} (8){1-1}   (11){3-3} (12){4-4}


計算索引 1 到 4 的區間和:
先找到 {0-2}
 往下再找 {0-1}
  往下再找 {1-1} 得到 {1} 的值 3
 往回找 {2-2} 得到 {2} 的值 5

再來找 {3-5}
 往下再找 {3-4} 得 {3, 4} 的值 16 

結束尋找,將找到的值加總,總和為 24

參考資料

最後,如果你覺得我的分享對你有幫助,請給予我一個愛心,並且分享這篇文章,這將是對我最大的鼓勵!