數據結構線段樹介紹與筆試算法題-LeetCode 307. Range Sum Query - Mutable--Java解法

此文首發於我的個人博客:zhang0peter的個人博客


LeetCode題解文章分類:LeetCode題解文章集合

LeetCode 所有題目總結:LeetCode 所有題目總結


線段樹(Segment Tree)常用於解決區間統計問題。求最值,區間和等操作均可使用該數據結構。

線段樹的最簡單的實現是通過數組(通過數組是爲了讓查找單個元素可以在O(1)的時間內做到),就像最小堆可以用數組實現一樣。

比較好的資料可以參考:Segment Tree Set 1 (Sum of given range) - GeeksforGeeks

下面我們以數組[18, 17, 13, 19, 15, 11, 20, 12, 33, 25]爲例。

具體構造如圖所示:

1.葉節點代表的是原始數組中的值

2.內部節點代表該節點下的葉子節點的和

3.根節點代表所有數的和


既然結構定了,那麼就要決定如何構造數組,實現這個構造。

模仿最小堆,根節點放在數組的最前面,構造大小爲2N的數組,原始數組的N個數放在後面。

Java代碼如下:

class SegmentTree {

    int[] segmentTree;
    int n;

    public SegmentTree(int[] nums) {
        if (nums.length > 0) {
            n = nums.length;
            segmentTree = new int[n * 2];
            //把原數組拷貝到新數組的後半部分
            System.arraycopy(nums, 0, segmentTree, 0 + n, n);
            //計算區域和
            for (int i = n - 1; i > 0; i--) {
                segmentTree[i] = segmentTree[i * 2] + segmentTree[i * 2 + 1];
            }
        }
    }
}

以數組[18, 17, 13, 19, 15, 11, 20, 12, 33, 25]爲例,建樹完成後的結果爲[null, 183, 125, 58, 90, 35, 32, 26, 32, 58, 18, 17, 13, 19, 15, 11, 20, 12, 33, 25]

既然建樹完成了,那麼可以進行更新值和求區域和的操作。

更新值需要先更新值本身,然後再依次更新父節點:

     public void update(int pos, int val) {
        pos = pos + n;
        //更新葉節點值
        segmentTree[pos] = val;
        pos /= 2;
        while (pos > 0) {
            //更新父節點值
            segmentTree[pos] = segmentTree[pos * 2] + segmentTree[pos * 2 + 1];
            pos /= 2;
        }
    }

求區域和的操作如下:

    public int sumRange(int left, int right) {
        left += n;
        right += n;
        int sum = 0;
        while (left <= right) {
            //判斷左索引節點是否是父節點的右子節點
            if ((left % 2) == 1) {
                sum += segmentTree[left];
                left++;
            }
            //判斷右索引節點是否是父節點的左子節點
            if ((right % 2) == 0) {
                sum += segmentTree[right];
                right--;
            }
            left /= 2;
            right /= 2;
        }
        return sum;
    }

求和的思路比更新值的思路更復雜,下面以如下這張圖爲例,數組爲[2, 4, 5, 7, 8, 9],求[4, 5, 7, 8]的和

在這裏插入圖片描述

1.先建樹,建樹結果爲[0, 35, 29, 6, 12, 17, 2, 4, 5, 7, 8, 9],索引從0到11.

2.我們要求從索引1到索引4的和,也就是樹中索引從7到11的值:sum=index(7)+index(8)+index(9)+index(10)+index(11)

3.左索引7號位不能整除2,意味着是父節點的右子樹,而父節點包含的左字數是不在求和範圍內的,因此直接加上該索引的值,並將左索引右移一位,左索引變爲8,此時結果如下:sum=4+index(8)+index(9)+index(10)+index(11)

4.左索引經過變更,此時已經成爲父節點的左子樹,因此左索引整除2獲得父節點索引4,此時結果如下:sum=4+index(4)+index(10)+index(11)

4.右索引11不能整除2,意味着是父節點的右子樹,而左子樹剛好在求和範圍內,無需變更索引。

5.右索引整除2獲得父節點,索引變爲5,此時結果如下:sum=4+index(4)+index(5)

6.左索引可以整除2,意味是父節點的左子樹,無需變更索引

7.右節點不能整除2,意味着是父節點的右子樹,而左子樹剛好在求和範圍內,無需變更索引

8.左索引除以2獲得父節點索引2,而右節點除以2後與左節點相等,所以此時結果如下:sum=4+index(2)

完整代碼如下:

class SegmentTree {

    int[] segmentTree;
    int n;

    public SegmentTree(int[] nums) {
        if (nums.length > 0) {
            n = nums.length;
            segmentTree = new int[n * 2];
            //把原數組拷貝到新數組的後半部分
            System.arraycopy(nums, 0, segmentTree, 0 + n, n);
            //計算區域和
            for (int i = n - 1; i > 0; i--) {
                segmentTree[i] = segmentTree[i * 2] + segmentTree[i * 2 + 1];
            }
        }
    }

    public void update(int pos, int val) {
        pos = pos + n;
        //更新葉節點值
        segmentTree[pos] = val;
        pos /= 2;
        while (pos > 0) {
            //更新父節點值
            segmentTree[pos] = segmentTree[pos * 2] + segmentTree[pos * 2 + 1];
            pos /= 2;
        }
    }

    public int sumRange(int left, int right) {
        left += n;
        right += n;
        int sum = 0;
        while (left <= right) {
            //判斷左索引節點是否是父節點的右子節點
            if ((left % 2) == 1) {
                sum += segmentTree[left];
                left++;
            }
            //判斷右索引節點是否是父節點的左子節點
            if ((right % 2) == 0) {
                sum += segmentTree[right];
                right--;
            }
            left /= 2;
            right /= 2;
        }
        return sum;
    }
}

題目地址:Range Sum Query - Mutable - LeetCode


Given an integer array nums, find the sum of the elements between indices i and j (i ≤ j), inclusive.

The update(i, val) function modifies nums by updating the element at index i to val.

Example:

Given nums = [1, 3, 5]

sumRange(0, 2) -> 9
update(1, 2)
sumRange(0, 2) -> 8

Note:

The array is only modifiable by the update function.
You may assume the number of calls to update and sumRange function is distributed evenly.


這道題目應該是很久之前看到的一道算法筆試題。

這道題目看起來不難,如果直接用暴力的做法會超時。
看到了別人有個取巧的做法:

class NumArray:
    def __init__(self, nums: List[int]):
        self.nums = nums
        self.sum = sum(nums)
        self.len = len(nums)

    def update(self, i: int, val: int) -> None:
        self.sum += val-self.nums[i]
        self.nums[i] = val

    def sumRange(self, i: int, j: int) -> int:
        ran = j-i
        if ran < self.len-ran:
            return sum(self.nums[i:j+1])
        else:
            return self.sum-sum(self.nums[:i])-sum(self.nums[j+1:])

時間的耗時顯著減少,可以勉強通過。

這道題目真正的做法是線段樹,Segment Tree。我承認之前我沒聽說過這個數據結構,但在網上查找資料後發現,線段樹好像也是一個經典的高級的數據結構。

Java解法如下:

class NumArray {

    int[] segmentTree;
    int n;

    public NumArray(int[] nums) {
        if (nums.length > 0) {
            n = nums.length;
            segmentTree = new int[n * 2];
            //把原數組拷貝到新數組的後半部分
            System.arraycopy(nums, 0, segmentTree, 0 + n, n);
            //計算區域和
            for (int i = n - 1; i > 0; i--) {
                segmentTree[i] = segmentTree[i * 2] + segmentTree[i * 2 + 1];
            }
        }
    }

    public void update(int pos, int val) {
        pos = pos + n;
        //更新葉節點值
        segmentTree[pos] = val;
        pos /= 2;
        while (pos > 0) {
            //更新父節點值
            segmentTree[pos] = segmentTree[pos * 2] + segmentTree[pos * 2 + 1];
            pos /= 2;
        }
    }

    public int sumRange(int left, int right) {
        left += n;
        right += n;
        int sum = 0;
        while (left <= right) {
            //判斷左索引節點是否是父節點的右子節點
            if ((left % 2) == 1) {
                sum += segmentTree[left];
                left++;
            }
            //判斷右索引節點是否是父節點的左子節點
            if ((right % 2) == 0) {
                sum += segmentTree[right];
                right--;
            }
            left /= 2;
            right /= 2;
        }
        return sum;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章