热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

IndexTree(树状数组)

1、引入线段树解决的是区间查询和区间更新的问题,O(logn)O(logn)O(logn)复杂度。人为规定:数组下标从1开始。如果要计算数组某个范


1、引入

线段树解决的是 区间查询区间更新 的问题,




O


(


l


o


g


n


)



O(logn)


O(logn)
复杂度。

人为规定:数组下标从 1 开始。

如果要计算数组某个范围 L 到 R 的累加和,那么可以准备一个前缀和数组 help,得到前缀和数组后,可以




O


(


1


)



O(1)


O(1)
复杂度 快速求出 L 到 R 的累加和。

如原数组 arr = [5, 3, 2, 4, 9, 0, 1],则 help = [5, 8, 10, 14, 23, 23, 24],如果要计算下标 3 ~ 5 的值的累加和,直接用 help[5] - help[2] = 15,求解速度非常快。但是这种求解数组某个范围的累加和的方法是基于原数组 arr 不再变化的前提。

如果原数组中的某个数修改了值,那么 help 数组就存在需要大量更新的问题。

那么对于原数组中的值可能变化的情况,但是还能查询范围上的累加和,需要换个结构。即该结构同时满足以下两个方法:
1)update(i, v):将 i 位置的值修改为 v;
2)sum[L...R]:计算 L 到 R 范围的累加和。

这个结构就是 Index Tree,上述的两个方法线段树可以完美实现,但是 Index Tree 有比线段树更优的地方。

Index Tree 没法像线段树一样做到范围更新,只能单点更新后执行范围累加和的查询,这两个方法都快。


2、Index Tree 的特点
  1. 支持区间查询
  2. 没有线段树那么强,但是非常容易改成一维、二维、三维的结构
  3. 只支持单点更新

3、Index Tree讲解

原始数组 arr:[3, 1, -2, 3, 6, 0, 9]
下标: 1 2 3 4 5 6 7

准备一个 help 数组(相同长度的配成一对)
1)1位置的3长度为1,前面没有长度为1的可以与之组成一对,于是就拷贝自己的值 help[1] = arr[1] = 3 (长度为1)
2)2位置的1长度为1,前面有长度为1的3可以与之组成一对,于是 help[2] = arr[1] + arr[2] = 4 (长度为2)
3)3位置的-2长度为1,前面没有长度为1的可以与之组成一对,于是拷贝自己的值 help[3] = arr[3] = -2 (长度为1)
4)4位置的3长度为1,前面有长度为1的-2可以与之组成一对,二者相加 = 1,此时已经长度为2,而前面又有长度为2的可以与之组成一对,于是 help[4] = arr[1] + arr[2] + arr[3] + arr[4] = 5 (长度为4)
5)5位置的6长度为1,附近前面没有长度为1的,于是 help[5] = arr[5] = 6 (长度为1)
6)6位置的0长度为1,前面长度为1的6可以与之组成一对,help[6] = arr[5] + arr[6] = 6 (长度为2)
7)7位置的9长度为1,前面没有长度为1的可以与之组成一对,于是help[7] = arr[7] = 9 (长度为1)

抛开数组中的值,仅看 help 数组每个元素管理的原数组的下标:
请添加图片描述
相同长度进行合并

规律1 :help 数组的 index 管理的是 index 的二进制形式中的从右往左的第一个1拆开后加1到它自己这个范围的数

例如 index = 010111000,那么它管理的是原数组中的 010110001 ~ 010111000 这个范围的值,即将 index 中从右往左数的第一个1拆开后加1到它自己的范围。

例 原数组 arr 下标从 1 到 8,那么help数组中的 index = 8 的位置管理的是原数组1到8范围的值,即 8 = 01000,拆开1加1 = 00001,所以管理的范围为 00001 ~ 01000 就是 1~8 范围。

同理 index = 12时,12 = 01100,第一个1去掉后加1 = 01001,所以管理的范围是 01001 ~ 01100,即原数组的9 ~ 12范围。

利用help数组求原数组中1 ~




i



i


i
位置的前缀和

例1:求原数组1 ~ 33 范围的前缀和,33 的二进制形式是0100001,那么就是help数组中 33 这个位置的值 a,以及将33的二进制形式的最后一个1去掉得到的0100000,对应到help数组中的该位置的 b,将help数组中的这两个位置的值相加即是原数组 1 ~ 33 这个范围的前缀和。为什么正确呢?因为help数组中的 33 就管理原数组的33位置的数,而help数组中的32 位置管理1 ~ 32 位置的数,所以二者相加就是1 ~ 33 范围的和。

例2:求原数组 1 ~




i



i


i
范围的前缀和,其中




i


=


0101100110



i = 0101100110


i=0101100110
,那么就是取出 help 数组的如下位置的值进行累加:

help[0101100110] = a,拿出help数组中 $i$ 位置的数
help[0101100100] = b,在上一步的基础上抹掉一个1
help[0101100000] = c,在上一步的基础上抹掉一个1
help[0101000000] = d,在上一步的基础上抹掉一个1
help[0100000000] = e,在上一步的基础上抹掉一个1
直到没有 1 可以抹掉,即index = 0 为止。

所以 sum[1...i] = a + b + c + d + e

为什么这个流程正确呢?






i


=


0110100



i = 0110100


i=0110100
时,在上述流程中取的是 help 数组中的:

help[0110100],管理的是原数组的 arr[0110001] ~ arr[0110100]

help[0110000],管理的是原数组的 arr[0100001] ~ arr[0110000]

help[0100000],管理的是原数组的 arr[0000001] ~ arr[0100000]

可见,管理的原数组的范围全部是连续的,所以就正确得到了原数组 1 ~




i



i


i
的前缀和。


4、代码实现

public class IndexTree {
// 下标从1开始!
public static class IndexTree {
private int[] tree;
private int N;
// 0位置弃而不用!
public IndexTree(int size) {
N = size;
tree = new int[N + 1];
}
// 根据index的位信息处理的,index的二进制位中有多少个1就需要操作多少次,得到每位的信息就是每次右移1位,即除以2
// 所以时间复杂度 O(logn)
// 函数功能:求1~index范围的累加和
// 所以如果要求 L 到 R的累加和 = 1~R的累加和 - 1~L-1的累加和
public int sum(int index) {
int ret = 0;
while (index > 0) {
ret += tree[index];
index -= index & -index; //去掉最右侧的1
}
return ret;
}
// index & -index : 提取出index最右侧的1出来
// index : 0011001000
// index & -index : 0000001000
// x & (-x) = x & (~x + 1)
public void add(int index, int d) { //时间复杂度 O(logn)
//认为原始数组一开始全部为0,现在要给index位置的数加d,那么tree数组(即help数组)就会有位置的数受到影响
//比如原数组的 3 位置的数发生了变化,那么 tree 数组中的哪些位置会受牵连呢?
// 3 的二进制为011,将最右侧的1加1 = 100,即4
// 4 = 100 最右侧的1 加1 = 1000,即8
// 8 = 1000 最右侧的1 加1 = 10000,即16
// 即 011 -> 100 -> 1000 -> 10000 ->....
//规律:只需要将发生更新位置的值的二进制的最右侧1加1 就能得到它影响tree数组哪些位置的值
while (index <&#61; N) {
tree[index] &#43;&#61; d; //当前位置&#43;d
index &#43;&#61; index & -index; //最右侧的1加1&#xff0c;找出受到牵连的位置
}
}
}

//暴力解
public static class Right {
private int[] nums;
private int N;
public Right(int size) {
N &#61; size &#43; 1;
nums &#61; new int[N &#43; 1];
}
public int sum(int index) {
int ret &#61; 0;
for (int i &#61; 1; i <&#61; index; i&#43;&#43;) {
ret &#43;&#61; nums[i];
}
return ret;
}
public void add(int index, int d) {
nums[index] &#43;&#61; d;
}
}
public static void main(String[] args) {
int N &#61; 100;
int V &#61; 100;
int testTime &#61; 2000000;
IndexTree tree &#61; new IndexTree(N);
Right test &#61; new Right(N);
System.out.println("test begin");
for (int i &#61; 0; i < testTime; i&#43;&#43;) {
int index &#61; (int) (Math.random() * N) &#43; 1;
if (Math.random() <&#61; 0.5) {
int add &#61; (int) (Math.random() * V);
tree.add(index, add);
test.add(index, add);
} else {
if (tree.sum(index) !&#61; test.sum(index)) {
System.out.println("Oops!");
}
}
}
System.out.println("test finish");
}
}






推荐阅读
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 本文介绍了使用Java实现大数乘法的分治算法,包括输入数据的处理、普通大数乘法的结果和Karatsuba大数乘法的结果。通过改变long类型可以适应不同范围的大数乘法计算。 ... [详细]
  • 本文介绍了一个Java猜拳小游戏的代码,通过使用Scanner类获取用户输入的拳的数字,并随机生成计算机的拳,然后判断胜负。该游戏可以选择剪刀、石头、布三种拳,通过比较两者的拳来决定胜负。 ... [详细]
  • 本文讨论了如何优化解决hdu 1003 java题目的动态规划方法,通过分析加法规则和最大和的性质,提出了一种优化的思路。具体方法是,当从1加到n为负时,即sum(1,n)sum(n,s),可以继续加法计算。同时,还考虑了两种特殊情况:都是负数的情况和有0的情况。最后,通过使用Scanner类来获取输入数据。 ... [详细]
  • 本文介绍了如何在给定的有序字符序列中插入新字符,并保持序列的有序性。通过示例代码演示了插入过程,以及插入后的字符序列。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • 猜字母游戏
    猜字母游戏猜字母游戏——设计数据结构猜字母游戏——设计程序结构猜字母游戏——实现字母生成方法猜字母游戏——实现字母检测方法猜字母游戏——实现主方法1猜字母游戏——设计数据结构1.1 ... [详细]
  • 多维数组的使用
    本文介绍了多维数组的概念和使用方法,以及二维数组的特点和操作方式。同时还介绍了如何获取数组的长度。 ... [详细]
  • Java中包装类的设计原因以及操作方法
    本文主要介绍了Java中设计包装类的原因以及操作方法。在Java中,除了对象类型,还有八大基本类型,为了将基本类型转换成对象,Java引入了包装类。文章通过介绍包装类的定义和实现,解答了为什么需要包装类的问题,并提供了简单易用的操作方法。通过本文的学习,读者可以更好地理解和应用Java中的包装类。 ... [详细]
  • This article discusses the efficiency of using char str[] and char *str and whether there is any reason to prefer one over the other. It explains the difference between the two and provides an example to illustrate their usage. ... [详细]
  • 重入锁(ReentrantLock)学习及实现原理
    本文介绍了重入锁(ReentrantLock)的学习及实现原理。在学习synchronized的基础上,重入锁提供了更多的灵活性和功能。文章详细介绍了重入锁的特性、使用方法和实现原理,并提供了类图和测试代码供读者参考。重入锁支持重入和公平与非公平两种实现方式,通过对比和分析,读者可以更好地理解和应用重入锁。 ... [详细]
  • C# 7.0 新特性:基于Tuple的“多”返回值方法
    本文介绍了C# 7.0中基于Tuple的“多”返回值方法的使用。通过对C# 6.0及更早版本的做法进行回顾,提出了问题:如何使一个方法可返回多个返回值。然后详细介绍了C# 7.0中使用Tuple的写法,并给出了示例代码。最后,总结了该新特性的优点。 ... [详细]
  • 从零学Java(10)之方法详解,喷打野你真的没我6!
    本文介绍了从零学Java系列中的第10篇文章,详解了Java中的方法。同时讨论了打野过程中喷打野的影响,以及金色打野刀对经济的增加和线上队友经济的影响。指出喷打野会导致线上经济的消减和影响队伍的团结。 ... [详细]
author-avatar
fedfedfv_249
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有