C++ 树进阶系列之树状数组的树形之路

news2024/10/6 16:30:24

1. 前言

树状数组也称二叉索引树,由Peter M. Fenwick1994发明,也可称为Fenwick树

树状数组的设计非常精巧,多用于求解数列的前缀和、区间和等问题,为区间类型问题提供了模板式解决方案。

数状数组简单易用,但对于初学者,初接触时会有理解上的壁垒,本文将深入细节,深入浅出还原数状数组的全貌。

2. 树状数组思想

树状数组,如名所义,本质上还是数组,但其内在有着树一样的灵魂。或者说数组中的数据之间存在树所描述的逻辑结构,即一对多的关系。

简单理解,树状数组是对另一个普通数组的映射。这里必然会引出 2 个值得思考的问题:

  • 映射的目的是什么?

  • 怎么映射才算完美?

下面带着这 2 个问题一一展开叙述。

2.1 映射目的

目的可以从一个简单的需求开始。

现有数组 arr,如下图所示:

3.png

求解从给定的起始位置终止位置区间内数组中数据之和。如 sum[5:10]=arr[5]+arr[6]+arr[7]+arr[8]+arr[9]+arr[10]

Tips: sum[5:10]表示区间求和,第一个数字表示起始位置,第二个数字表示结束位置。

4.png

显然,这个问题是简单的,一个循环便能解决,时间复杂度为O(n)

如果对于任意区间求和是一个频率较高的操作。必然会出现后一次的计算中会包括前一次的计算流程,如前一次求解sum[5:10],后一次求解sum[5:11],显然会出现重复计算sum[5:10]的过程。

能否缓存曾经求解过的结果,方便在另一次求解时直接使用。

自然的想法:使用一个缓存数组存储原数组中前面一段区间的和(也称前缀和)。如下图所示:

5.png
有了缓存数据,此时sum[5:10]=cache[10]-cache[4],求解sum[5:11]=cache[11]-cache[4]。当求解任意区间和时,不会出现对某个高频率区间的和重复计算。缓存后求和时间复杂度可以达到O(1)

但是,初始化缓存数组的时间复杂度为O(n)

#include <iostream>
using namespace std;
//原数组
int arr[13]= {3,6,1,9,7,11,8,5,12,2,5,10,13};
//缓存数组
int cache[13];
int main(int argc, char** argv) {
	for(int i=0; i<13; i++) {
		if(i==0)
			cache[i]=arr[i];
		else
			cache[i]=cache[i-1]+arr[i];
		cout<<cache[i]<<"\t";
	}
	return 0;
}

再就是,当arr数组中的值更新后,如arr[2]=arr[2]+2后,cache数组从索引号为2之后位置的值需要全部更新。

6.png

如果这种更新是频繁的,如有m次,则时间复杂度会变成O(m*n)

所以说,这种方案可以说是一种方案,但不能算是完美的方案。时间复杂度高,不适用于性能要求高的应用场景。

性能瓶颈的原因何在

cache数组的每一个位置都缓存了原数组此位置之前的和,当原数组中某个位置的值发生变化后,则缓存数组此位置之后的缓冲值都要更新。

其实,可以采用化整为零思想,把原数组分成很多区间,缓存这些区间的和便可,如需要更新,也只需区间更新。

在求某区间和时,如果能直接找到此区间的缓存值,自然很好,如果找不到,可以累加多个子区间的和。

于是问题就转化为怎么划分区间,树状数组帮助我们完美地解决了这个问题。

2.2 二进制索引映射

树状数组,利用二进制的特性,对原数组进行了化整为零的区间划分,无论更新还是求和的时间复杂度均保持在O(logn)

平时使用数组时,数组的索引号常用十进制描述。现在改一下习惯,换成二进制表示,为什么要这样,先可以不用管,而精彩部分也是从这里开始。如下图:

7.png

同样,提供一个缓存数组,命名为bit

刚说过,树状数组采用区间缓存。区间的划分就很重要,现在利用在二进制上的进位操作进行魔幻性的区间划分。

首先从arr数组索引号为1的位置开始划分且更新缓存数据。

Tips: 如下括号内的数字表示数组编号,括号外的数字表示进制。

  • (1)10的二进制为(00001)2。把(00001)2+(1)2=(00010)2=(2)10。则第一个区间范围为(1,2)10(00001,00010)2
  • 继续在(00010)2+(10)2=(00100)2=(4)10,则第二个区间范围为 (2,4)10(00010,00100)2
  • 继续在(00100)2+(100)2=(01000)2=(8)10,则第三个区间范围为 (4,8)10(00100,01000)2
  • 继续直到小于等于数组的最大索引号,本文只研究到数组中编号为 8位置。

如下图所示,以索引号1作为起始边界,分别划分出 3 个子区间,在bit数组的区间边界位置中缓存arr[1]中的值。

8.png

这里会有一个问题,在划分区间的过程中,二进制递增的值1,10,100是怎么来的?

如下图所示,本质是在低位第一个1的位置向高位做进位操作。

9.png

再在arr数组索引号为2的位置开始划分且更新。

根据前面的划分规则,其区间分别是(2,4)10(00010,00100)2(4,8)10(00100,01000)2。且更新此区间中的值。也意味着这 2 个区间被更新了 2 次。

18.png

在每一次划分和更新操作时,如果出现区间重复划分,则此区间内的缓存值会如波峰一样一层一层叠加。

10.png

再在arr数组索引号为3的位置开始划分且更新。

19.png

11.png

再扫描到arr数组索引号为4的位置开始划分且更新。

12.png

再扫描到arr数组索引号为5的位置开始划分且更新。

13.png

如下是扫描到arr数组索引号为 6时的演示图。

14.png

如下是扫描到arr数组索引号为 7时的演示图。

15.png

如下是当扫描到arr数组索引号为 8时的演示图。

16.png

17.png

最后可以看到,bit数组中的数据之间呈现出树形逻辑结构,这也是树状数组的由来。

缓存过程,犹如一层一层波浪,由子结点向根结点向上蔓延叠加。

21.png

有了不同区间的缓存数据,下面就可以求解给定区间内数字之和了。

求和过程是更新过程的逆操作,

更新是根据起始位置计算区间的结束位置,而求和是根据结束位置计算区间的起始位置。

bit[8]缓存了arr数组前 8 个位置中所有数字之和,因为对arr中的数值缓存时,最终都会跳跃到此位置。如下图所示:

22.png

但是如果求解arr数组前 7 个位置的和,需要累加如下 3 个区间的值, sum=bit[7]+bit[7]+bit[4]。如下图演示区间的跳跃过程。

23.png

在长度为 8 的数组中,无论是一次或多次跳跃,向上都会跳跃到 8 这个位置,向下都会跳跃到 0 这个位置。无论是更新还是求和,都是利用二进制的这种跳跃特性,真是令人惊叹。

3. 树状数组的 API

树状数组最优秀或让人惊艳的地方在于通过二进制的进位划分区间的思想,故树状数组也称为Binary Indexed Tree,算是求仁得仁。

树状数组中有一个核心的API,如何找到二进制中低位上第一次出现的 1。这个函数通常命名为 lowbit(x)。也是树状数组典型的象征。

另外至少还应该包含缓存(更新)函数和区间求和函数。

#include <iostream>
using namespace std;
class BinaryIndexedTree {
	private:
		//树状数组
		int* bit;
		//大小
		int size;
	public:
		BinaryIndexedTree(int size):size(size) {
			this->bit=new int[size]{0};	
		}
		/*
		* 二进制计算
		*/
		int lowbit(int x);

		/*
		*缓存或更新
		*/
		void update(int i,int val);

		/*
		*区间求和
		*/
		int getSum(int upBound,int lowBound);

		/*
		*输出缓存数据
		*/
		void showBit() {
			for(int i=1; i<this->size; i++)
				cout<<this->bit[i]<<"\t";
		}
};

lowbit函数:有 2 种实现方案。

注意:是查找低位上第一个 1 以及后面的数字,如10001返回(1)2=(1)1010010返回(10)2=(2)1010100返回(100)2=(4)10101000返回(1000)2=(8)2……

  • 消掉最后一位1,然后再用原数减去消掉最后一位1后的数。

25.png

int BinaryIndexedTree::lowbit(int x) {
	return x - (x & (x - 1));
}
  • 求负数的补码。
int BinaryIndexedTree::lowbit_(int x) {
	return x & -x;
}

update函数:此函数实现就较简单。

/*
* 参数说明
* i 数组索引号
* val 需要更新的值
*/
void BinaryIndexedTree::updata(int i,int val) {   
	while(i <= this->size) {
        //缓存数据
		bit[i] += val;
         //区间的下一个边界
		i += lowbit(i);
	}
}

getSum函数:区间求和与更新过程互为逆操作。

/*
* 参数说明
* upBound 区间的上边界,包含 
* lowBound 区间的下边界,包含 
*/
int BinaryIndexedTree::getSum(int upBound,int lowBound) {
	//上边界之和
    int sum=0;
	for(int i=upBound; i>0; i-=this->lowbit(i) ) {
		sum+=this->bit[i];
	}
    //下边界之和
	int sum_=0;
	for(int i=lowBound-1; i>0; i-=this->lowbit(i) ) {
		sum_+=this->bit[i];
	}
    //区间之差
	return sum-sum_;
}

测试更新:

int main(int argc, char** argv) {
	int arr[9]= {0,3,6,1,9,7,11,8,5};
	BinaryIndexedTree bt(9);
	for(int i=1; i<9; i++) {
		bt.update(i,arr[i]);
	}
	cout<<"原数组"<<endl;
	for(int i=1; i<9; i++) {
		cout<<arr[i]<<"\t";
	}
	cout<<"\n树状数组"<<endl;
	bt.showBit();
	return 0;
}

输出 bit 结果:

20.png

测试区间求和:

int main(int argc, char** argv) {
    //省略……
	int sum= bt.getSum(8,4);
	cout<<"\n区间[4:8]之和"<<sum;
	cout<<endl;
	return 0;
}

输出结果:

24.png

4. 总结

树状数组不仅用于区间求和,也可以用于区间求最值。只需要在更新是保证每次更新值最大(小)即可。

void BinaryIndexedTree::update(int i,int val) {
	while(i < this->size) {
		if(val>this->bit[i])
		    //保证每次更新值是最大的 
			this->bit[i] = val;
		i +=this->lowbit(i);
	}
}

再在树状数组中添加一个getMax(int pos)函数。

int BinaryIndexedTree::getMax(int pos) {
	int mx= 1<<31;
	for(int i=pos; i>0; i-=this->lowbit(i) ) {
		if(this->bit[i]>mx)
			mx=this->bit[i];
	}
	return mx;
}

便能求解指定区间的最大值。测试代码就留给大家自行实现。

只有洞穿了树状数组的底层原理,方能在应用场景自然想起它。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/185266.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【100个 Unity实用技能】 | 修改Unity UI控件中默认字体配置

Unity 小科普 老规矩&#xff0c;先介绍一下 Unity 的科普小知识&#xff1a; Unity是 实时3D互动内容创作和运营平台 。包括游戏开发、美术、建筑、汽车设计、影视在内的所有创作者&#xff0c;借助 Unity 将创意变成现实。Unity 平台提供一整套完善的软件解决方案&#xff…

【C++提高编程1】一文带你吃透函数模板和类模板(附测试用例源码、测试结果图及注释)

&#x1f4dd;我的个人主页 &#x1f381;欢迎各位→点赞&#x1f44d; 收藏⭐️ 留言&#x1f4dd;​&#x1f4ac;总结&#xff1a;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流 &#x1f58a;✉️今天你做别人不想做的事&…

域内权限维持:注入SSP

01、简介 SSP(Security Support Provider)是Windows操作系统安全机制的提供者。简单地说&#xff0c;SSP是个DLL文件&#xff0c;主要用来实现Windows操作系统的身份认证功能。在系统启动时&#xff0c;SSP 将被加载到lsass.exe进程中&#xff0c;攻击者通过自定义恶意的DLL文件…

解决ModuleNotFoundError: No module named ‘torch.fx‘

运行yolo v5 train python train.py 报错 ModuleNotFoundError: No module named ‘torch.fx’ torch版本不匹配 目前版本torchu1.7 #卸载pytorch pip uninstall torch 再安装 python -m pip install torch -i https://mirrors.aliyun.com/pypi/simple/ python -m pip是…

本周大新闻|Quest Pro降价至1099美元,传苹果AIGC或用于XR内容生成

本周大新闻正值春节假期&#xff0c;因此包含近两周&#xff08;1月16-1月29日&#xff09;的AR/VR新闻汇总。关于2022&#xff0c;近期我们发布了2022年AR/VR行业融资报告、2022年AR硬件总结、2022年VR硬件总结。AR方面&#xff0c;最新消息称苹果AIGC曝光&#xff0c;或用Sir…

通信数据中心供电系统故障影响区域分析定位

&#xff08;华北石油通信有限公司&#xff09;摘要&#xff1a;供电系统对于通信机房而言至关重要&#xff0c;一旦供电系统发生严重故障&#xff0c;需要快速制定出应急预案&#xff0c;使故障影响可控。本文提供一种对机房供电系统故障影响区域快速定位方法。该方法的实现思…

可观察性和安全性融合的紧迫性越来越高

根据一份新报告&#xff0c;融合可观察性和安全性的紧迫性越来越大。 软件情报公司 Dynatrace 公布了一项针对大型组织的 1,300 名 CIO 和高级 DevOps 经理&#xff08;包括来自澳大利亚的 100 名&#xff09;进行的独立全球调查的结果。 调查结果表明&#xff0c;随着对连…

OS 学习笔记(3) 操作系统的发展与分类

OS 学习笔记(3) 操作系统的发展与分类 这篇笔记对应的王道考研 1.2 操作系统的发展与分类&#xff0c;同时参考了 《Operating System Concepts, Ninth Edition》和 《 Operating Systems: Three Easy Pieces》&#xff08;俗称ostep&#xff09; 文章目录OS 学习笔记(3) 操作系…

【数据结构】堆的应用——TOP-K问题详解

目录 &#x1f34e;前言&#x1f34e;&#xff1a; &#x1f95d;一、TOP-K 问题概述&#x1f95d;&#xff1a; &#x1f349;二、不同解决思路实现&#x1f349;&#xff1a; ①排序法&#xff1a; ②直接建堆法&#xff1a; ③K 堆法&#xff08;最优解&#xff09;&a…

Redis简单入门

Redis简介 Redis是一个开源的使用C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value的NoSQL数据库。特点如下: 读写速度快&#xff1a;Redis官网测试读写能到10万左右每秒。速度快的原因这里简单说一下&#xff0c;第一是因为数据存储在内存中&#xff0c;我们知…

标准有效的项目开发流程

代码版本管理在项目中&#xff0c;代码的版本管理非常重要。每个需求版本的代码开发在版本控制里都应该经过以下几个步骤。在master分支中拉取该需求版本的两个分支&#xff0c;一个feature分支&#xff0c;一个release分支&#xff1b;feature分支用于接受个人分支merge过来的…

二叉树DFS、BFS

目录 1&#xff0c;DFS遍历 2&#xff0c;DFS遍历OJ实战 力扣 144. 二叉树的前序遍历 力扣 94. 二叉树的中序遍历 力扣 145. 二叉树的后序遍历 力扣 105. 从前序与中序遍历序列构造二叉树 力扣 106. 从中序与后序遍历序列构造二叉树 力扣 889. 根据前序和后序遍历构造二…

C++中的new、operator new与placement new

new operator 当我们使用了new关键字去创建一个对象时&#xff0c;你知道背后做了哪些事情吗&#xff1f; A* a new A;实际上这样简单的一行语句&#xff0c; 背后做了以下三件事情&#xff1a; 分配内存,如果类A重载了operator new&#xff0c;那么将调用A::operator new(…

TencentOS安装并运行多版本php

TencentOS版本3.1安装并运行php7&#xff0c;现在需要同时运行一个php8. php选择使用了php v8.0.27 采用编译安装的方式&#xff0c;编译命令如下&#xff1a; ./configure --prefix/application/php8 --with-config-file-path/application/php8/etc --with-mhash --with-o…

51单片机学习笔记-4矩阵键盘

4 矩阵键盘 [toc] 注&#xff1a;笔记主要参考B站江科大自化协教学视频“51单片机入门教程-2020版 程序全程纯手打 从零开始入门”。 注&#xff1a;工程及代码文件放在了本人的Github仓库。 4.1 矩阵键盘介绍 在键盘中按键数量较多时&#xff0c;为了减少I/O口的占用&#…

vuex中 this.$store.dispatch() 与 this.$store.commit()

一、理解 this.$store.dispatch 分发 actions-> 调用 mutations->改变 states 二、思考 1、为什么不直接分发 mutation mutation 有必须同步执行的限制&#xff0c;而 Action 不受约束&#xff0c;可以在 action 内部执行异步操作2、Action 通常是异步的&#xff0c;…

配置日志输出到指定位置的文件,单独报错error级别以上的日志,按日志类别打印日志

目录1.配置文件2.测试程序&#xff1a;工具&#xff1a;log4j的jar包、配置文件log4j.properties(文件名自定义)、eclipse或IDEA 更多参考&#xff1a;https://www.cnblogs.com/ITtangtang/p/3926665.html、 1.配置文件 新建一个配置文件log4j.properties&#xff08;我把它放…

区块链游戏走出一地鸡毛,元宇宙3D国风链游或成最大受益者

曾推出过《Cytus》《Deemo》《聚爆》等知名游戏的雷亚&#xff0c;其CEO游名扬在接受采访时曾谈到&#xff0c;游戏产业是文化产业加上科技产业的组合体&#xff0c;这两者是组成游戏产业的主要部分。看游戏的趋势&#xff0c;就要针对文化和科技的趋势上来看。 这话没错。 20…

flutter StreamController,ValueListenableBuilder,NotificationListener

FutureBuilder &#xff08;异步数据更新&#xff09; StreamBuilder &#xff08;异步数据更新&#xff09; 构造函数 特点 接收多个异步操作的结果class StreamBuilder<T> extends StreamBuilderBase<T, AsyncSnapshot<T>>{}单订阅&#xff1a;StreamCo…

在Linux中进行Hbase搭建

在公网IP为x.x.x.x、y.y.y.y和z.z.z.z并装有Centos8的服务器上进行hadoop集群搭建、zookeeper集群搭建和hbase搭建&#xff0c;都安装hadoop-3.1.3、server-jre-8u202-linux-x64、apache-zookeeper-3.6.4-bin和hbase-2.5.0-bin。 环境准备&#xff08;三台服务器都一样&#x…