昨天刷力扣认识的这个数据结构,原题在这里。记录一下力扣https://leetcode.cn/problems/my-calendar-iii/

例题

给定数组[1, 2, 3, 4, 5, ....],编写函数来返回数组任意区间元素的和

朴素的想法

直接把每个元素相加,时间复杂度为O(n),n是区间长度

def getSum(arr, start, end):val = 0for i in range(start, end + 1):val += arr[i]return val

前缀和算法

前缀和是一种常用的、较为高效的预处理方式。能够有效降低查询的时间复杂度。前缀和可以理解为数组前项的和。查询的时间复杂度为O(1)

class PartialSumArr:def __init__(self, arr):val = 0self.partialSumArr = []for i in arr:val += iself.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 = arrself.nodes = [0] * len(arr) * 2self.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 0elif left == right:return self.nodes[index]else:if start == left and end == right:return self.nodes[index]mid = (left + right) // 2leftNode = self.findSum(index * 2, start, end, left, mid)rightNode = self.findSum(index * 2 + 1, start, end, mid + 1, right)return leftNode + rightNodesegmentTreeSum = 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] = valelif i < left or right < i:returnelse:mid = (left + right) // 2self.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:returnelif start == left and end == right:self.nodes[index] += (end - start + 1) * valself.lazy[index] += valelse:if left == right:self.nodes[index] += valself.lazy[index] += valreturnmid = (left + right) // 2self.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] += valself.nodes[index * 2 + 1] += valself.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 0elif left == right:return self.nodes[index]else:if start == left and end == right:return self.nodes[index]mid = (left + right) // 2leftNode = 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 = arrself.nodes = [0] * len(arr) * 2self.lazy = [0] * len(arr) * 2self.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] += valself.nodes[index * 2 + 1] += valself.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 0elif left == right:return self.nodes[index]else:if start == left and end == right:return self.nodes[index]mid = (left + right) // 2leftNode = 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] = valelif i < left or right < i:returnelse:mid = (left + right) // 2self.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:returnelif start == left and end == right:self.nodes[index] += (end - start + 1) * valself.lazy[index] += valelse:if left == right:self.nodes[index] += valself.lazy[index] += valreturnmid = (left + right) // 2self.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力扣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

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

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

  1. 【学习笔记】线段树的数组大小下限及证明

    手动博客搬家: 本文发表于20170820 20:23:52, 原地址https://blog.csdn.net/suncongbo/article/details/77432667 线段树是一种将一 ...

  2. 307. Range Sum Query - Mutable | 307. 区域和检索 - 数组可修改(数据结构:线段树,图文详解)

    题目 https://leetcode.com/problems/range-sum-query-mutable/ 吐槽官方题解 这题的 英文版官方题解,配图和代码不一致,而且描述不清:力扣国内版题解 ...

  3. 线段树的数组大小下限及证明

    线段树的数组大小下限及证明 手动博客搬家: 本文发表于20170820 20:23:52, 原地址https://blog.csdn.net/suncongbo/article/details/774 ...

  4. c语言线段树建树程序,c语言数据结构之线段树详解;例题:校门外的树(poj2808或者vijos1448)...

    线段树:它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个结点. 也就是说线段树的每一个结点对应一个区间,其中根节点对应区间[1,n] 对于线段树中的每一个非叶子节点[a,b],它的左儿子 ...

  5. hdu 2461(线段树求面积并)

    题意:给出N个矩形,M次询问 每次询问给出R个,问这R个矩形围成的面积 解题思路:对于每次询问,做一次线段树求面积的并操作. 每个节点保存的信息有l,r,cover,len分别表示该节点表示的区间[l ...

  6. 线段树求逆序对(hdu1394Minimum Inversion Number)

    说实话,线段树求逆序对我理解了半天诶,不知是否有人像我一样. 对于每个数来说,只有和已经出现过的.比它大的数才能形成逆序对,那么在给定的数列中,每给一个数就向前找比它大的数. 样例:10 1 3 6 ...

  7. 吉首大学2019年程序设计竞赛(重现赛) 干物妹小埋(线段树求最长上升子序列)

    链接:https://ac.nowcoder.com/acm/contest/992/B 来源:牛客网 时间限制:C/C++ 1秒,其他语言2秒 空间限制:C/C++ 32768K,其他语言65536 ...

  8. HDU - 1255 覆盖的面积(线段树求矩形面积交 扫描线+离散化)

    链接:线段树求矩形面积并 扫描线+离散化 1.给定平面上若干矩形,求出被这些矩形覆盖过至少两次的区域的面积. 2.看完线段树求矩形面积并 的方法后,再看这题,求的是矩形面积交,类同. 求面积时,用被覆 ...

  9. poj 1151(线段树求面积并)

    解题思路:线段树求面积并,水题 #include<iostream> #include<cstdio> #include<cstring> #include< ...

  10. uscao 线段树成段更新操作及Lazy思想(POJ3468解题报告)

    线段树成段更新操作及Lazy思想(POJ3468解题报告) 标签: treequerybuildn2cstruct 2011-11-03 20:37 5756人阅读 评论(0) 收藏 举报  分类: ...

最新文章

  1. BarTender安装常见问题集结
  2. windbg断点学习总结
  3. C语言 | 卡尔曼滤波算法2——算法分析
  4. FATAL: NO bootable medium found! System halted
  5. kali下sqliv:SQL注入URL扫描器
  6. java跟踪会话_JavaWeb会话跟踪
  7. 【架构设计】Android:配置式金字塔架构
  8. Codeforces Round #236 (Div. 2) C. Searching for Graph(水构造)
  9. paip.python错误解决15
  10. Android学习资源网站 1
  11. vfp报表纸张设置_VFP报表输出.doc
  12. 系统架构设计师考试心得与经验
  13. Dynamics CRM 2013学习伊始
  14. vue移动端复杂表格表头,固定表头与固定第一列
  15. bignumber.js API
  16. Visual Studio.net 2003 安装和卸载的教训(包括软件下载地址及安装方法)
  17. 侍魂胧月传说辅助【有八鸽】侍魂胧月传说全能免费辅助脚本功能和使用说明
  18. Dell服务器组建阵列-Raid(无阵列卡)
  19. 微软常用运行库合集(32+64位)
  20. 解决Sklearn ValueError: empty vocabulary; perhaps the documents only contain stop words

热门文章

  1. undo歌词中文音译_Undo - Sanna nielsen帮我看看这歌词翻译对么
  2. AES256加解密与异常处理
  3. 7个简单步骤解释区块链挖掘和交易如何工作
  4. 大写的贵,如履泥潭 DeFi 如何突破手续费困境?
  5. 浅谈交易开拓者程序化
  6. 情景英语-美国情景会话大全 精选
  7. my.cnf配置详解
  8. 深入java虚拟机 视频_深入理解Java虚拟机全套完整视频教程
  9. excel条件格式标记一整行
  10. 使用 nosqlBooster for mongoDB 连接 Mongodb