《林林数据结构笔记》线段树求数组区间和,单点更新,区间更新+lazy思想

本文介绍了线段树这一数据结构,用于高效地处理数组的区间查询和更新问题。通过线段树,可以将查询和更新的时间复杂度降低到O(logN)。文章详细讲解了如何建立线段树、查询区间和单点更新,并引入了懒惰思想优化区间更新,避免不必要的节点更新。最后,作者提供了完整的代码示例并分享了参考资料。

昨天刷力扣认识的这个数据结构,原题在这里。记录一下力扣icon-default.png?t=M4ADhttps://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 Programminghttps://cp-algorithms.com/data_structures/segment_tree.html力扣icon-default.png?t=M4ADhttps://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

以上为我的笔记,接受友好地建议和指正~

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值