问题

  1. 给出一个数组,多次求下标区间[i,j]的元素和
    解决:前缀和
  2. 给出一个数组,对其进行多次单点修改,期间穿插求下标区间[i,j]的元素和
    解决:树状数组

问题2的分析:

  • 有单点修改,若仍用前缀和,则修改i处的值,需要更新i后面所有前缀和
    单次修改复杂度O(N),单次查询复杂度O(1),最坏时K次操作都是修改,总复杂度O(KN)
  • 使用普通数组存储和修改
    单次修改复杂度O(1),单次查询复杂度O(N),最坏时K次操作都是查询,总复杂度O(KN)
  • 更高效的解决方式是使用树状数组,单次查询复杂度O(logN),单次修改复杂度O(logN),有K次不同操作,总复杂度O(KlogN)

树状数组可以理解为升级版的前缀和数组,两者都是预处理某段区间的和,不同点在于:

  • 前缀和数组preSum[i]求和覆盖范围过大([0,i)区间的元素和)
  • 树状数组每一个C[i]覆盖的求和范围仅仅为[i-lowbit(i)+1,i]

当我们需要穿插单点修改操作时,每次修改会引起很大部分preSum[i]随之受影响,而树状数组通过二进制位合理划分求和覆盖范围,使得每次受影响的C[i]很少,若元素总数为N,每次修改最多有logNC[i]受影响,复杂度O(logN)

树状数组/二进制索引树(Binary Index Tree, BIT)

树状数组是一种动态维护前缀和(支持单点修改+区间查询)的数据结构

首先明确,树状数组的本质思想,是二进制规律的应用
树状数组中,需要用到lowbit(n)=n&-n,函数取出二进制数最右侧的1和其更右边部分

记原数组为nums[i],其前缀和数组preSum[i]表示[0,i)区间元素的总和

树状数组定义

  • 树状数组C也是保存了nums数组某个区间内的元素总和,但其定义为:
    C[i]保存:nums[i](包含本身)及其之前的lobit(i)个元素的总和
  • 注意,使用树状数组,numsC下标i一定要从1开始,否则下面所有结论出错

例如,对于i=4或12,其lowbit(i)=100
故树状数组C[4]C[12]都保存了其本身及之前的长度为4的区间和结果
C[4]=nums[1]+nums[2]+nums[3]+nums[4]C[12]=nums[9]+nums[10]+nums[11]+nums[12]

如何实现区间[l,r]元素和的查询

  1. 我们仍然需要用前缀和来实现:
    定义前缀和:preSum[i]表示[1,i]区间内的元素和,且preSum[0]=0(和普通前缀和定义不同)
    那么,[l,r]区间元素和 = preSum[r] - preSum[l-1]
  2. 我们实际维护的是树状数组C,因此要利用CpreSum[i]
    由图可见,preSum[i]希望求解[1,i]区间的元素和
    当前的C[i]覆盖了lowbit(i)范围的区间和,还剩下[1, i-lowbit(i)]区间的元素和没有解决
    [1, i-lowbit(i)]区间的元素和,又转化为小规模的相同问题preSum[ i-lowbit(i)]

总结:preSum[i] = C[i] + preSum[i-lowbit(i)],可以不断递归求解,直到i=0

def preSum(i):"""求解nums[1...i]区间的元素和preSum[0]=0"""ans = 0while i != 0:ans += C[i]# C[i]覆盖了lowbit(i)个元素之和,接下来要求解[1,i-lowbit(i)]子问题,直到右边界为0i -= lowbit(i)return ans

理解:

  • 首先,i = i - lowbit(i)实际上就是不断把数字i的最右边的1置为0的过程,故循环次数=i的二进制表示中1的个数,时间复杂度:O(logN)
  • 其次,这里的原理可以从图上理解:随便给一个i,求preSum[i],然后不断往“左上角”走,收集总和

如何实现元素的单点修改

  1. 我们维护树状数组C,因此改动一个元素i,可能会对多个C[idx]造成影响
    例如,在图中让nums[6]增加val,则C[6]C[8]C[16]都要同步增加val
  2. 如何完整找出所有受到影响的C[idx]
    当前的C[i]覆盖了i及其之前的lowbit(i)范围的区间和,我们修改C[i]
    对于更小的idx,显然C[idx]不会涉及当前的nums[i]
    因此,我们要找下一个更大的idx=i+delta,并且delta的值尽量小,这样才能找出下一个受到影响的C[idx]
    可以证明,最小的delta = lowbit(i),故下一个要更新C[i+loiwbit(i)],不断递归更多的C[idx]

总结:更新C[i]后,下一个受影响并需要更新的是C[i+loiwbit(i)],可以不断递归求解,直到i>=len(C)

def update(i, val):"""nums[i]元素的值增加val,更新树状数组中,与之有关的所有C[idx]"""L = len(C)while i < L:# 更新C[i]C[i] += val# 下一个受影响,需要更新的是C[i+loiwbit(i)]i += lowbit(i)return

理解:

  • 首先,i = i + lowbit(i)实际上就是不断在数字i的最右边的1位置上产生进位(由此定位到最右边1的左侧第一个0),时间复杂度:O(logN)
  • 其次,这里的原理可以从图上理解:随便给一个i,更新C[i],然后不断往“右上角”走,更新所有受影响的C[idx]

树状数组的应用

模板题

LeetCode 307. 区域和检索 - 数组可修改
注意两点:

  1. 通常使用下标从0开始,在树状数组中需要转化为从1开始;
  2. 这题的单点修改定义为[重新赋值],而树状数组中的修改是[增加一个增量v],我们可以转化:nums[i]改为val,等价于增加val-nums[i]

经典运用:统计序列中各元素左侧小于它的元素的个数

给出正整数序列,对于每个元素,求其左侧有多少个元素比它小

  • 基本思路:从左到右遍历元素,哈希表hash[num]实时维护元素num的出现次数,则对于当前的元素n,答案为hash[0]+hash[1]+...+hash[n-1],并且维护hash[n]+=1
  • 上面的操作完全可以用树状数组的“单点修改”和“前缀和”功能实现,使得每次求和快速完成
  • 进阶变式问题:
    改为 [求各元素右侧小于它的元素的个数]:改为从右到左遍历即可
    改为 [求各元素左侧大于它的元素的个数]:改为求区间和hash[n+1]+...+hash[n_max],即preSum[n_max]-preSum[n]
    元素取值改为 [可能为负/可能非常大]:使用离散化技巧

离散化

思路:

实际上我们只关注元素之间的大小关系,因此在这里[-9999,111,1,11111111]等价于[1,3,2,4]
因此,我们要做的就是把具体数值映射到排名,从而回到上面的 [正整数序列]的问题

具体实现:

  1. 方法一:类似于“对于考试分数排名,分数相同则排名相同”的问题
  • 预处理:对于(val,pos)排序,根据排序结果计算各元素排名,用排名替换原数组中的具体数值val(位置在pos处)
  • 排名rank的逻辑:遍历排序结果,
    如果当前元素!=上一元素,则rank=当前已遍历元素数(遍历下标+1);
    如果当前元素==上一元素,则rank不变
  1. 方法二:直接去重后排序,元素的rank=元素在排序后的数组中的下标

可见,离散化的优点:将不在合适范围内的整数/非整数映射为正整数,或者将分布稀疏的元素聚集到一起,总之就是将元素值映射到一个连续的整数区间,且保持其大小相对关系
离散化的缺点:仅用于离线查询,因为获取排名的前提是已知所有元素的值。

例题:

LeetCode 315. 计算右侧小于当前元素的个数
问题中元素取值范围-10^4 <= nums[i] <= 10^4,使用上面的离散化树状数组即可

class Solution:def countSmaller(self, nums: List[int]) -> List[int]:class BIT:def __init__(self, len):"""下标1~len的数组"""self.L = len + 1self.C = [0] * (len + 1)def lowbit(self, x):return x & (-x)def update(self, idx, val):while idx < self.L:self.C[idx] += validx += self.lowbit(idx)def preSum(self, idx):"""1~idx的和"""res = 0while idx:res += self.C[idx]idx -= self.lowbit(idx)return res# 从左到右遍历元素,树状数组`hash[num]`实时维护元素num的出现次数,# 则对于当前的元素`n`,答案为`hash[0]+hash[1]+...+hash[n-1]`,并且维护`hash[n]+=1`# 离散化,`[-9999,111,1,11111111]`等价于`[1,3,2,4]`rank = {}s = sorted(set(nums))  # 去重后得到唯一的排名for i, n in enumerate(s):rank[n] = i + 1bit = BIT(len(s))ans = [0] * len(nums)for i in range(len(nums) - 1, -1, -1):# 处理时不是处理数字,而是处理其离散化的排名r = rank[nums[i]]ans[i] = bit.preSum(r - 1)  # 答案为`hash[0]+hash[1]+...+hash[n-1]`bit.update(r, 1)  # 维护`hash[n]+=1`return ans

变式:

剑指 Offer 51. 数组中的逆序对
在统计逆序对的时候,只需要统计每个位置右侧小于当前元素的个数,再对它们求和,就可以得到逆序对的总数

算法学习笔记——数据结构:树状数组BIT相关推荐

  1. js 数组 实现 完全树_算法和数据结构 | 树状数组(Binary Indexed Tree)

    本文来源于力扣圈子,作者:胡小旭.点击查看原文 力扣​leetcode-cn.com 树状数组或二叉索引树(英语:Binary Indexed Tree),又以其发明者命名为 Fenwick 树.其初 ...

  2. 夜深人静写算法(三)- 树状数组

    目录   一.从图形学算法说起       1.Median Filter 概述       2.r pixel-Median Filter 算法 3.一维模型       4.数据结构的设计     ...

  3. 数据结构——树状数组

    我们今天来讲一个应用比较广泛的数据结构--树状数组 它可以在O(nlogn)的复杂度下进行单点修改区间查询,下面我会分成三个模块对树状数组进行详细的解说,分别是树状数组基本操作.树状数组区间修改单点查 ...

  4. 数据结构--树状数组

    文章目录 1. 树状数组 2. 单点修改 3. 区间修改 4. 完整代码 5. 参考文献 1. 树状数组 类似数据结构:线段树(Segment Tree) 树状数组 跟 线段树 的区别: 树状数组能做 ...

  5. 2017西安交大ACM小学期数据结构 [树状数组 离散化]

    Problem E 发布时间: 2017年6月28日 12:53   最后更新: 2017年6月29日 21:35   时间限制: 1000ms   内存限制: 64M 描述 给定一个长度为n的序列a ...

  6. 2017西安交大ACM小学期数据结构 [树状数组,极大值]

    Problem D 发布时间: 2017年6月28日 10:51   最后更新: 2017年6月28日 16:38   时间限制: 1000ms   内存限制: 32M 描述 给定一个长度为n的序列a ...

  7. 2017西安交大ACM小学期数据结构 [树状数组]

    Problem C 发布时间: 2017年6月28日 11:38   最后更新: 2017年6月28日 16:38   时间限制: 1000ms   内存限制: 32M 描述 给定一个长度为n的序列a ...

  8. 数据结构 —— 树状数组

    [概述] 树状数组又称二叉索引树,常用于高效计算数列的前缀和.区间和,其查询.修改的时间复杂度为 log(n),空间复杂度为 O(n) 树状数组通过将线性结构转化成树状结构,从而进行跳跃式扫描. 优点 ...

  9. Java数据结构-树状数组

    什么是树状数组?[面试5.0] 使用数组表示多叉树的结构,和优先队列有点类似,区别在于优先队列只表示二叉树 主要用来: 更新数组元素的数值并且求数组前K个元素的总和或平均值 时间复杂度为O(logN) ...

  10. 数据结构与算法学习笔记(python)——第一节 数组应用程序实战

    前言 本人是一个长期的数据分析爱好者,最近半年的时间的在网上学习了很多关于python.数据分析.数据挖掘以及项目管理相关的课程和知识,但是在学习的过程中,过于追求课程数量的增长,长时间关注于学习了多 ...

最新文章

  1. JavaScript创建或填充任意长度的数组
  2. HTML5调用redis,redis实现从数据库获取数据添加到html页面上
  3. java 有穷自动机_Java实现雪花算法(snowflake)
  4. 使用Vivado保存仿真波形数据并读取
  5. poj 2409 polya定理
  6. 与ea服务器连接中断770,测试ea出现 There has been a critical error 这是什么错误?如何解决? 谢谢!...
  7. 进程池和线程池 concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor
  8. 创建分布式爬虫的步骤
  9. windows 2003 server安装iis6,附下载文件
  10. 银行信贷系统java_java毕业设计_springboot框架的银行信贷系统
  11. [CS131] Lecture 1 Course Introduction
  12. java keytool证书cer,keytool 生成cer证书
  13. 时间轨迹图控件,自定义View
  14. 详细解读:大数据分析的学习
  15. 5W2H工作法,使工作更有条理,生活更好梳理
  16. 商城小程序线上线下结合能带来什么优势?
  17. Asp.Net Ajax (2)---ScriptManager
  18. 使用联通云OSS小程序直传
  19. CSS进阶(7)- 样式补充
  20. 2017年什么命_2017年在五行中属什么,2017年出生是什么命 五行

热门文章

  1. 战地1服务器怎么显示fps,战地1怎么显示FPS帧数_战地1显示FPS帧数方法图文攻略_玩游戏网...
  2. python中的Pickle文件和npy文件
  3. mysql如何实现透视表功能_SQL 实现数据透视表功能
  4. 在 Python 中使用机器学习进行人体姿势估计
  5. 写乐100道练习题_【写乐钢笔使用】_摘要频道_什么值得买
  6. gitee和gitHub的命令和详细步骤操作
  7. 设置页眉为计算机网络,word文档设置页眉线如题,怎么设置上边的 – 手机爱问...
  8. 一则 HTTP 405 Method Not Allowed 的解决办法
  9. C#开发串口调试助手的详细教程
  10. Ego的MyBatis框架笔记