此文首發於我的個人博客: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;
}
}