昨天刷力扣认识的这个数据结构,原题在这里。记录一下力扣
https://leetcode.cn/problems/my-calendar-iii/
例题
给定数组[1, 2, 3, 4, 5, ....],编写函数来返回数组任意区间元素的和
朴素的想法
直接把每个元素相加,时间复杂度为O(n),n是区间长度
def getSum(arr, start, end):
val = 0
for i in range(start, end + 1):
val += arr[i]
return val
前缀和算法
前缀和是一种常用的、较为高效的预处理方式。能够有效降低查询的时间复杂度。前缀和可以理解为数组前项的和。查询的时间复杂度为O(1)
class PartialSumArr:
def __init__(self, arr):
val = 0
self.partialSumArr = []
for i in arr:
val += i
self.partialSumArr.append(val)
def getSum(self, start, end):
if start == 0:
return self.partialSumArr[end]
return self.partialSumArr[end] - self.partialSumArr[start-1]
partialSumArr = PartialSumArr([1, 2, 3, 4, 5, 6, 7])
print(partialSumArr.partialSumArr)
print(partialSumArr.getSum(0, 4))
print(partialSumArr.getSum(3, 6))
>>>[1, 3, 6, 10, 15, 21, 28]
15
22
新的问题
如果要修改数组中某一项,即更新数组。
对于方法一:更新的时间复杂度为O(1);
对于方法二:更新的时间复杂度为O(n),因为需要把前缀和数组中这个元素之后每个元素都更新一遍
如果要实现区间更新呢?
对于方法一:更新的时间复杂度为O(n);n为区间大小
对于方法二:更新的时间复杂度为O(m*n),m为区间最右下标位置距离数组最右距离,n为区间长度
线段树
线段树是一种数据结构,可以有效地对数组进行范围内的查询,同时可以足够灵活地修改数组。
给定一个数组(arr),编写函数来求其任意区间元素和

建树
这是根节点

通过求两个下标的平均值来确定其子节点

同理

至此我们的树就建立好了,下面创建一个数组(nodes)来储存树的节点。当父节点为node[index]时,我们把它的子节点放在node[index * 2]和node[index * 2 + 1]位置。我们通常把根节点放在下标1的位置。如下图,数组里0, 4表示下标0~4的元素和,这么写方便和上面树图对比

替换成对应值

对应代码:
class SegmentTreeSum:
def __init__(self, arr):
self.arr = arr
self.nodes = [0] * len(arr) * 2
self.construct(1, 0, len(arr) - 1)
def construct(self, index, left, right):
# left = right,确定节点
if left == right:
self.nodes[index] = self.arr[left]
else:
mid = (left + right) // 2 # 取整
# 递归先确定子节点,再把子节点值相加获取该节点的值
self.construct(index * 2, left, mid) # 左节点
self.construct(index * 2 + 1, mid + 1, right) # 右节点
self.nodes[index] = self.nodes[index * 2] + self.nodes[index * 2 + 1]
segmentTreeSum = SegmentTreeSum([1, 2, 3, 4, 5])
print(segmentTreeSum.nodes)
>>>[0, 15, 6, 9, 3, 3, 4, 5, 1, 2]
查询
index表示nodes下标,start和end表示查询区间,left和right表示nodes[index]表示的区间范围
def findSum(index, start, end, left, right):从根节点开始查询:findSum(1, start, end, 0, len(arr)-1)
查询需要考虑三种情况
1. [start, end]在[left, right]之外,没有符合元素,返回0
2. [start, end]和[left, right]表示区间相同,直接返回nodes[index]
3. [start, end]和[left, right]表示区间不同
比如我们从根节点开始查询,两个区间不同,接下来通过递归对两个子树进行查询,再把它们结果相加

下图可见,两个区间不同,继续递归
下图可见, 查询区间超出范围,right = 1 < start = 2,返回0
查询另一个子树,区间不同,但是该节点没有子树无法继续递归
同理当查询到(3, 3)和(4, 4)节点时也虽然满足条件,但也无法继续递归 。我们发现把这三个节点相加得到的数就是我们需要的[2, 4]区间元素和。所以当left = right即没有子树时,我们返回nodes[index]
代码部分:时间复杂度为O(logN)
class SegmentTreeSum:
...
def getSum(self, start, end):
return self.findSum(1, start, end, 0, len(self.arr) - 1)
def findSum(self, index, start, end, left, right):
if end < left or start > right:
return 0
elif left == right:
return self.nodes[index]
else:
if start == left and end == right:
return self.nodes[index]
mid = (left + right) // 2
leftNode = self.findSum(index * 2, start, end, left, mid)
rightNode = self.findSum(index * 2 + 1, start, end, mid + 1, right)
return leftNode + rightNode
segmentTreeSum = SegmentTreeSum([1, 2, 3, 4, 5])
print(segmentTreeSum.getSum(2, 4))
单点更新
i表示原数组arr更新下标
def change(index, i, val, left, right):从根节点开始查询:findSum(1, start, end, 0, len(arr)-1)
和查询类似
1. left = i 且 right = i,说明找到该节点,直接更新 nodes[index] = val
2. i < left or i > right,超出范围
3. 没有超出范围,递归更新子树
代码部分:时间复杂度为O(logN)
class SegmentTreeSum:
...
def update(self, i, val):
self.change(1, i, val, 0, len(self.arr) - 1)
def change(self, index, i, val, left, right):
if left == i and right == i:
self.nodes[index] = val
elif i < left or right < i:
return
else:
mid = (left + right) // 2
self.change(index * 2, i, val, left, mid)
self.change(index * 2, i, val, mid + 1, right)
self.nodes[index] = self.nodes[index * 2] + self.nodes[index * 2 + 1]
segmentTreeSum = SegmentTreeSum([1, 2, 3, 4, 5])
print(segmentTreeSum.nodes)
segmentTreeSum.update(2, 6)
print(segmentTreeSum.nodes)
>>>[0, 15, 6, 9, 3, 3, 4, 5, 1, 2]
[0, 18, 9, 9, 6, 3, 4, 5, 1, 2]
区间更新(lazy算法)
所谓lazy算法就是在更新时,只更新要求更新的这一段,而不更新他的子节点。当我们要访问他的子节点时,我们才去更新他的值。这样我们就能减少很多更新的次数,从而减少时间。
创建lazy数组
self.lazy = [0] * len(arr) * 2
val是区间内每个元素增加的值
def changeRange(self, index, start, end, left, right, val):
和上面情况类似
1. [start, end]在[left, right]之外,不用进行操作

2. [start, end]和[left, right]表示区间相同,只更新nodes[index]和lazy[index],不用把区间内每个元素都更新。

3. [start, end]和[left, right]表示区间不相同,递归子节点
写到这里我感觉我已经把线段树看透了,写上面的时候还有一知半解的地方。突然感觉这东西这么简单真的需要人教吗?不废话了,直接上代码。时间复杂度O(logN)
class SegmentTreeSum:
...
self.lazy = [0] * len(arr) * 2
...
def updateRange(self, start, end, val):
self.changeRange(1, start, end, 0, len(self.arr) - 1, val)
def changeRange(self, index, start, end, left, right, val):
if end < left or start > right:
return
elif start == left and end == right:
self.nodes[index] += (end - start + 1) * val
self.lazy[index] += val
else:
if left == right:
self.nodes[index] += val
self.lazy[index] += val
return
mid = (left + right) // 2
self.changeRange(index * 2, start, end, left, mid, val)
self.changeRange(index * 2 + 1, start, end, mid + 1, right, val)
self.nodes[index] = self.nodes[index * 2] + self.nodes[index * 2 + 1]
segmentTreeSum = SegmentTreeSum([1, 2, 3, 4, 5])
print(segmentTreeSum.nodes)
segmentTreeSum.updateRange(0, 4, 3)
print(segmentTreeSum.nodes)
>>>[0, 15, 6, 9, 3, 3, 4, 5, 1, 2]
[0, 30, 6, 9, 3, 3, 4, 5, 1, 2]
需要注意的地方
我们看这段测试
segmentTreeSum = SegmentTreeSum([1, 2, 3, 4, 5])
segmentTreeSum.updateRange(0, 4, 3)
print(segmentTreeSum.nodes)
print(segmentTreeSum.lazy)
print(segmentTreeSum.getSum(1, 2))
>>>[0, 30, 6, 9, 3, 3, 4, 5, 1, 2]
[0, 3, 0, 0, 0, 0, 0, 0, 0, 0]
5
答案很明显是错的,而且错误原因也很明显。上面说lazy算法,当我们要访问他的子节点时,我们才去更新他的值。所以当我们求和时我们首先需要去更新它的值
添加pushTree()方法来更新子节点
class SegmentTreeSum:
...
def pushTree(self, index, val):
if index * 2 < len(self.nodes):
self.nodes[index * 2] += val
self.nodes[index * 2 + 1] += val
self.pushTree(index * 2, val)
self.pushTree(index * 2 + 1, val)
更新求和函数
def findSum(self, index, start, end, left, right):
if self.lazy[index] != 0:
self.pushTree(index, self.lazy[index])
if end < left or start > right:
return 0
elif left == right:
return self.nodes[index]
else:
if start == left and end == right:
return self.nodes[index]
mid = (left + right) // 2
leftNode = self.findSum(index * 2, start, end, left, mid)
rightNode = self.findSum(index * 2 + 1, start, end, mid + 1, right)
return leftNode + rightNode
完整代码
class SegmentTreeSum:
def __init__(self, arr):
self.arr = arr
self.nodes = [0] * len(arr) * 2
self.lazy = [0] * len(arr) * 2
self.construct(1, 0, len(arr) - 1)
# 建树
def construct(self, index, left, right):
# left = right,确定节点
if left == right:
self.nodes[index] = self.arr[left]
else:
mid = (left + right) // 2 # 取整
# 递归先确定子节点,再把子节点值相加获取该节点的值
self.construct(index * 2, left, mid) # 左节点
self.construct(index * 2 + 1, mid + 1, right) # 右节点
self.nodes[index] = self.nodes[index * 2] + self.nodes[index * 2 + 1] # 相加
# 更新节点
def pushTree(self, index, val):
if index * 2 < len(self.nodes):
self.nodes[index * 2] += val
self.nodes[index * 2 + 1] += val
self.pushTree(index * 2, val)
self.pushTree(index * 2 + 1, val)
# 获取区间和
def getSum(self, start, end):
return self.findSum(1, start, end, 0, len(self.arr) - 1)
def findSum(self, index, start, end, left, right):
if self.lazy[index] != 0:
self.pushTree(index, self.lazy[index])
if end < left or start > right:
return 0
elif left == right:
return self.nodes[index]
else:
if start == left and end == right:
return self.nodes[index]
mid = (left + right) // 2
leftNode = self.findSum(index * 2, start, end, left, mid)
rightNode = self.findSum(index * 2 + 1, start, end, mid + 1, right)
return leftNode + rightNode
# 单点更新
def update(self, i, val):
self.change(1, i, val, 0, len(self.arr) - 1)
def change(self, index, i, val, left, right):
if left == i and right == i:
self.nodes[index] = val
elif i < left or right < i:
return
else:
mid = (left + right) // 2
self.change(index * 2, i, val, left, mid)
self.change(index * 2, i, val, mid + 1, right)
self.nodes[index] = self.nodes[index * 2] + self.nodes[index * 2 + 1]
# 区间更新
def updateRange(self, start, end, val):
self.changeRange(1, start, end, 0, len(self.arr) - 1, val)
def changeRange(self, index, start, end, left, right, val):
if end < left or start > right:
return
elif start == left and end == right:
self.nodes[index] += (end - start + 1) * val
self.lazy[index] += val
else:
if left == right:
self.nodes[index] += val
self.lazy[index] += val
return
mid = (left + right) // 2
self.changeRange(index * 2, start, end, left, mid, val)
self.changeRange(index * 2 + 1, start, end, mid + 1, right, val)
self.nodes[index] = self.nodes[index * 2] + self.nodes[index * 2 + 1]
参考网站:
Segment Tree - Algorithms for Competitive Programming
https://cp-algorithms.com/data_structures/segment_tree.html力扣
https://leetcode.cn/problems/my-calendar-iii/solution/xian-duan-shu-by-xiaohu9527-rfzj/前缀和 - 知乎前缀和(Partial_sum)简介(Introduction) 前缀和是一种常用的、较为高效的预处理方式。能够有效降低查询的时间复杂度。前缀和可以理解为数组前n项的和。 一维前缀和示例(Example) 数列:\begin{matrix} 1&2&…
https://zhuanlan.zhihu.com/p/350910138线段树(lazy用法)_起烟的博客-CSDN博客_线段树lazy在上一篇中,我们讨论了线段树的基础用法,其中我们对于线段树的修改,仅仅限制于对于线段树的点的修改,而不是对于某一个一段区间的修改。那么我们现在来想想如果对于线段树的一段区间来进行修改的话,如果我们还是来对线段树的每一个点都进行修改的话,那么,假设我们需要修改m个点,其时间复杂度岂不是为O(log(n))了,那么我们所需要的少的时间复杂度的要求就没有达成,为此,lazy数组应运而生,来解决这个问题。首先我们来认识一下lazy:如果当前区间被需要修改的目标区间完全覆盖,打一个标记。如果下一次的查询或者更改包
https://blog.csdn.net/malloch/article/details/107999709
以上为我的笔记,接受友好地建议和指正~
本文介绍了线段树这一数据结构,用于高效地处理数组的区间查询和更新问题。通过线段树,可以将查询和更新的时间复杂度降低到O(logN)。文章详细讲解了如何建立线段树、查询区间和单点更新,并引入了懒惰思想优化区间更新,避免不必要的节点更新。最后,作者提供了完整的代码示例并分享了参考资料。


被折叠的 条评论
为什么被折叠?



