数据结构——树状数组

news2024/11/17 13:49:55

文章目录

  • 前言
  • 问题引入
  • 问题分析
  • 树状数组
    • `lowbit`
    • 树状数组特性
    • 初始化一个树状数组
    • 更新操作
    • 前缀和计算
    • 区间查询
  • 总结


前言

原题的连接
最近刷leetcode的每日一题的时候,遇到了一个区间查询的问题,使用了一种特殊的数据结构树状数组,学习完之后我不禁感叹这个数据结构设计的巧妙,下面是我的学习笔记。

问题引入

给你一个数组 nums ,请你完成两类查询。

其中一类查询要求 更新 数组 nums 下标对应的值
另一类查询要求返回数组 nums 中索引 left 和索引 right 之间( 包含 )的nums元素的 和 ,其中 left <= right
实现 NumArray 类:

  • NumArray(int[] nums) 用整数数组 nums 初始化对象
  • void update(int index, int val) 将 nums[index] 的值 更新 为 val
  • int sumRange(int left, int right) 返回数组 nums 中索引 left 和索引 right 之间( 包含 )的nums元素的 和 (即,nums[left] + nums[left + 1], …, nums[right])

问题分析

这个问题我们可以总结为 :

  • 单点修改,就是一次只修改一个数(与之相对的是区间修改)
  • 区间查询,查询某一个区间的和

我们如果用暴力的求解,在每次单点修改之后都需要重新计算一下区间的和,显然效率低下。
那我们如果使用前缀和数组进行求解,这样所有的区间和问题都可以转换成区间边界前缀和相减,但是每次单点修改之后,修改位之后的所有前缀和都需要修改。
通过上面的思考,我们发现每次单点修改,之后重新计算前缀和的成本太高了。那有没有一种方法能缩小这种由于单点更新导致的前缀和重新计算?

树状数组

我们先看一下树状数组长什么样子:
假设我们现在有一个数组a[]={1,5,7,4,3,2,1,6,7,3,0,4,9}
在这里插入图片描述

然后上图展示的就是一个树状数组,树状数组实际上一个数组,但是在逻辑结构上看做一个树形结构。其实和堆的底层结构是一样的。
数组的每个元素实际上代表的是某一个区间和,而树状数组设计巧妙的就是这个区间覆盖的长度的设计!
然后我们来逐一分析如何得到这么一颗树形结构

lowbit

什么一个数的lowbit ?
lowbit是最截取一个数二进制最低位的二进制1 到最末尾
例如数字6,他的二进制位为0110,取出它的lowbit位就变成了10转换成十进制就是2!
在这里插入图片描述
例如数字8,他的二进制为1000,取出它的lowbit位就变成了1000转换成十进制就是8
在这里插入图片描述
那么如何计算数字a的lowbit呢?
这个只需要 a&(-a),就是a与上a的补码加1,补码加一使得最右边的1右边的所有位与a的二进制位均相同,而左边的位全部与a的二进制相反,这样一个操作直接能取出。

所以lowbit的函数就为:

	// 计算lowbit数值 
	int lowbit(int x) {
		return x & (-x);
	}

树状数组特性

理解了lowbit的概念之后我们在看这个树状数组:
我们对树状数组t[i]的定义是:原数组a在区间[i-lowbit(i),i]区间上的和(i的下标从1开始)
在这里插入图片描述

我们发现了树状数组实际上遵循以下规律:

  1. t[i] 实际上代表的是一个a数组的某个区间的和,而这个区间长度正好是lowbit(i)
  2. t[i]的父节点为t[i+lowbit(i)],而父区间一定是包含子区间的,所以子区间发生更新之后,一定需要修改父区间
  3. t[i]实际上就是数组在[i-lowbit(i)+1,i]这个区间上面的和

初始化一个树状数组

如果要初始化一个树状数组,我们需要定义两个数组,一个是用来保存原始的数组,一个用来保存树状数组:
初始化树状数组其实很简单:我们只需要牢记规律1,计算t[i]时,而为右边界通过规律1反推出左边界,就能计算出区间和

class TreeArray {

private:
	vector<int> t;   // 树状数组
	vector<int> v;    // 原始数组
		// 计算lowbit数值 
	int lowbit(int x) {
		return x & (-x);
	}
public:
	// 初始化树状数组
	void Init(const vector<int>& x) {
		// 注意这里的v和t 下标都不是从0开始的,而是从1开始的
		v.resize(x.size() + 1);
		copy(x.begin(), x.end(), (++v.begin()));
		t.resize(x.size() + 1, 0);
		for (int i = 1; i <= x.size(); i++) {
			for (int j = i - lowbit(i) + 1; j <= i; j++) {
				t[i] += v[j];
			}
		}
	}
};

更新操作

更新操作也十分简单,由于更新之后需要修改某些区间和,这里牢记规律2,从子区间向上更新父区间,然后更新v和t数组

例如:下面我们需要修改下标为3的值,将它的值修改为4,那么区间和就需要增加1,然后将所有包含下标为3的区间都进行修改
在这里插入图片描述


class TreeArray {

private:
	vector<int> t;   // 树状数组
	vector<int> v;    // 原始数组

	// 计算lowbit数值 
	int lowbit(int x) {
		return x & (-x);
	}
public:
	// 初始化树状数组
	void Init(const vector<int>& x) {
		v.resize(x.size() + 1);
		copy(x.begin(), x.end(), (++v.begin()));
		t.resize(x.size() + 1, 0);
		for (int i = 1; i <= x.size(); i++) {
			for (int j = i - lowbit(i) + 1; j <= i; j++) {
				t[i] += v[j];
			}
		}
	}
	void Update(int index, int val) {
		// 传入的index是从0开始的,但是我们这里的树状数组下标是从1开始
		int dif = val - v[index + 1];  // 这里我们计算一个差值
		v[index + 1] = val;
		// 修改index的所有的父节点
		for (int i = index + 1; i < t.size(); i += lowbit(i)) {
			t[i] += dif;
		}
	}
};

前缀和计算

前缀和这个就更简单了,更具树状数组的定义

我们对树状数组t[i]的定义是:原数组a在区间[i-lowbit(i)+1,i]区间上的和(i的下标从1开始)

比方说我们要计算下标为7的前缀和,我们可以拆成t[7]+t[6]+t[4],而我们发现7减去区间长度(lowbit(7))就是t[6],而t[6]减去区间长度t[4]…
这就是线段树的设计巧妙之处,把前缀和转换成多个树状数组元素相加!

在这里插入图片描述

class TreeArray {
private:
	vector<int> t;   // 树状数组
	vector<int> v;    // 原始数组
	// 计算lowbit数值 
	int lowbit(int x) {
		return x & (-x);
	}
public:
	// 初始化树状数组
	void Init(const vector<int>& x) {
		v.resize(x.size() + 1);
		copy(x.begin(), x.end(), (++v.begin()));
		t.resize(x.size() + 1, 0);
		for (int i = 1; i <= x.size(); i++) {
			for (int j = i - lowbit(i) + 1; j <= i; j++) {
				t[i] += v[j];
			}
		}
	}
	void Update(int index, int val) {
		// 传入的index是从0开始的,但是我们这里的树状数组下标是从1开始
		int dif = val - v[index + 1];
		v[index + 1] = val;
		// 修改index的所有的父节点
		for (int i = index + 1; i < t.size(); i += lowbit(i)) {
			t[i] += dif;
		}
	}
	// 算出index的前缀和
	int GetPrefixSum(int index) {
		int sum = 0;
		for (int i = index + 1; i > 0; i -= lowbit(i)) {
			sum += t[i];
		}
		return sum;
	}
};

区间查询

我们直接把区间查询转换成 前缀和相减!

class TreeArray {

private:
	vector<int> t;   // 树状数组
	vector<int> v;    // 原始数组

	// 计算lowbit数值 
	int lowbit(int x) {
		return x & (-x);
	}
public:
	// 初始化树状数组
	void Init(const vector<int>& x) {
		v.resize(x.size() + 1);
		copy(x.begin(), x.end(), (++v.begin()));
		t.resize(x.size() + 1, 0);
		for (int i = 1; i <= x.size(); i++) {
			for (int j = i - lowbit(i) + 1; j <= i; j++) {
				t[i] += v[j];
			}
		}
	}
	void Update(int index, int val) {
		// 传入的index是从0开始的,但是我们这里的树状数组下标是从1开始
		int dif = val - v[index + 1];
		v[index + 1] = val;
		// 修改index的所有的父节点
		for (int i = index + 1; i < t.size(); i += lowbit(i)) {
			t[i] += dif;
		}
	}

	// 算出index的前缀和
	int GetPrefixSum(int index) {
		int sum = 0;
		for (int i = index + 1; i > 0; i -= lowbit(i)) {
			sum += t[i];
		}
		return sum;
	}
	// 区间查询
	int Select(int begin, int end) {
		return GetPrefixSum(end) - GetPrefixSum(begin - 1);
	}
};

总结

树状数组 实际上是对前缀和的优化,前缀和计算的是[0,i]的和,如果一个修改就要对所有的区间和修改,但是树状数组将区间的长度通过lowbit的巧妙构造,使得每次单点修改所要更新的区间和始终不超过O(logN)

树状数组的使用条件(遇到这种情况直接默写模版):

  • 单点更新
  • 区间查询

然后默写树状数组的时候牢记三个规律,AC这类题应该没有多大问题

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

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

相关文章

智慧工地APP全套源码,智慧工地云平台

智慧工地平台 &#xff0c;智慧工地源码&#xff0c;智慧工地APP全套源码 智慧工地以施工现场风险预知和联动预控为目标&#xff0c;将智能AI、传感技术、人像识别、监控、虚拟现实、物联网、5G、大数据、互联网等新一代科技信息技术植入到建筑、机械、人员穿戴设施、场地进出关…

SOME/IP 协议介绍(五)指南

指南&#xff08;信息性&#xff09; 选择传输协议 SOME/IP直接支持互联网上使用最广泛的两种传输协议&#xff1a;用户数据报协议&#xff08;UDP&#xff09;和传输控制协议&#xff08;TCP&#xff09;。UDP是一种非常简洁的传输协议&#xff0c;仅支持最重要的功能&#…

03. Python中的语句

1、前言 在《Python基础数据类型》一文中&#xff0c;我们了解了Python中的基础数据类型&#xff0c;今天我们继续了解下Python中的语句和函数。 2、语句 在Python中常用的语句可以大致分为两类&#xff1a;条件语句、循环语句。 2.1、条件语句 条件语句就是我们编码时常见…

C语言ASCII码排序(1086: ASCII码排序(多实例测试))

题目描述 输入三个字符后&#xff0c;按各字符的ASCII码从小到大的顺序输出这三个字符。 输入&#xff1a;输入数据有多组&#xff0c;每组占一行&#xff0c;由三个字符组成&#xff0c;之间无空格。chu 输出&#xff1a;对于每组输入数据&#xff0c;输出一行&#xff0c;字符…

Codeforces Round 909 (Div. 3)(A~G)(启发式合并 , DSU ON TREE)

1899A - Game with Integers 题意&#xff1a;给定一个数 , 两个人玩游戏&#xff0c;每人能够执行 操作&#xff0c;若操作完是3的倍数则获胜&#xff0c;问先手的人能否获胜&#xff08;若无限循环则先手的人输&#xff09;。 思路&#xff1a;假如一个数模3余1或者2&#…

【具身智能评估2】具身视觉语言规划(EVLP)数据集基准汇总

参考论文&#xff1a;Core Challenges in Embodied Vision-Language Planning 论文作者&#xff1a;Jonathan Francis, Nariaki Kitamura, Felix Labelle, Xiaopeng Lu, Ingrid Navarro, Jean Oh 论文原文&#xff1a;https://arxiv.org/abs/2106.13948 论文出处&#xff1a;Jo…

Python PyQt 程序设置图标

源码运行时图标 第一步&#xff1a;阿里巴巴是两图标库下载喜欢的图标 iconfont-阿里巴巴矢量图标库 第二步&#xff1a;转化png为ico https://www.aconvert.com/cn/icon/png-to-ico/ 256x256为大图标 默认的32x32很小&#xff08;不建议用) 转化后右键点击文件链接&…

基于金鹰算法优化概率神经网络PNN的分类预测 - 附代码

基于金鹰算法优化概率神经网络PNN的分类预测 - 附代码 文章目录 基于金鹰算法优化概率神经网络PNN的分类预测 - 附代码1.PNN网络概述2.变压器故障诊街系统相关背景2.1 模型建立 3.基于金鹰优化的PNN网络5.测试结果6.参考文献7.Matlab代码 摘要&#xff1a;针对PNN神经网络的光滑…

【Android】Android Framework系列--CarUsbHandler源码分析

Android Framework系列–CarUsbHandler源码分析 本文基于Android12源码。 CarUsbHandler是Android Car提供的服务之一&#xff0c;其用车载USB连接的场景。 车载USB有其特殊应用场景&#xff0c;比如AndroidAuto、CarLife等。而Android的做法是在其原有的USB服务上&#xff0…

十七、Linux的组管理

1、Linux组基本介绍 在linux中的每个用户必须属于一个组&#xff0c;不能独立于组外。在linux中每个文件所有者、所在组、其它组的概念 1.所有者 2.所在组 3.其他组 4.改变用户所在的组 2、文件/目录 所有者 一般为文件的创建者&#xff0c;谁创建了该文件&#xff0c;就自…

【图解算法】- 快乐数还能这么解?

一 - 前言 介绍&#xff1a;大家好啊&#xff0c;我是hitzaki辰。 社区&#xff1a;&#xff08;完全免费、欢迎加入&#xff09;日常打卡、学习交流、资源共享的知识星球。 自媒体&#xff1a;我会在b站/抖音更新视频讲解 或 一些纯技术外的分享&#xff0c;账号同名&#xff…

MongoDB随记

MongoDB 1、简单介绍2、基本术语3、shard分片概述背景架构路由功能chunk&#xff08;数据分片&#xff09;shard key&#xff08;分片键值&#xff09; 4、常用命令 1、简单介绍 MongoDB是一个分布式文件存储的数据库&#xff0c;介于关系数据库和非关系数据库之间&#xff0c…

第 372 场 LeetCode 周赛题解

A 使三个字符串相等 求三个串的最长公共前缀 class Solution { public:int findMinimumOperations(string s1, string s2, string s3) {int n1 s1.size(), n2 s2.size(), n3 s3.size();int i 0;for (; i < min({n1, n2, n3}); i)if (!(s1[i] s2[i] && s2[i] s…

【智能家居】5、主流程设计以及外设框架编写与测试

目录 一、主流程设计 1、工厂模式结构体定义 &#xff08;1&#xff09;指令工厂 inputCmd.h &#xff08;2&#xff09;外设工厂 controlDevices.h 二、外设框架编写 1、创建外设工厂对象bathroomLight 2、编写相关函数框架 3、将浴室灯相关操作插入外设工厂链表等待被调…

内容运营工具:标签体系

一.分类和标签的区别 ■标签是扁平的&#xff0c;分类是层级的。 ■标签是精确的&#xff0c;分类是粗糙的。 ■标签是多维的&#xff0c;分类是一维的。 二.标签的本质&#xff1a;元数据 事实上&#xff0c;在数据领域&#xff0c;有一个鼎鼎大名的词汇与标签极其雷同&…

再高级的打工人也只是打工人!

再高级的打工人也只是打工人&#xff01; OpenAI CEO 奥特曼被罢免的事情人尽皆知「虽然&#xff0c;今天又复职了。。」&#xff0c;我们能从中学到什么呢&#xff1f; CEO 也能被裁&#xff0c;这应该是最近几年被裁名单里面&#xff0c;职级最高的一个人了吧。你再也不用担…

吐血整理,金融银行测试的“火“到底在哪里?银行测试真正实施...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 银行里的软件测试…

C#WPF中的实现读取和写入文件的几种方式

说明&#xff1a;C#中实现读取和写入的类根据需要来选择。 1、File类 File类是用于操作文件的工具类&#xff0c;提供了对文件进行创建、复制、删除、移动和打开单一文件的静态方法。但需要注意的是&#xff0c;WPF中使用File的类&#xff0c;需要先引用System.IO下的命名空间。…

数据结构【DS】图的遍历

BFS 要点 需要一个辅助队列visited数组&#xff0c;防止重复访问 复杂度 时间复杂度&#xff1a;访问结点的时间访问所有的边的时间 广度优先生成树 邻接表存储的图的表示方式不唯一&#xff0c;生成树也不唯一 DFS 复杂度 时间复杂度&#xff1a;访问结点的时间访问所有…

Java工具包Hutool框架

Hutool是一个Java基础工具类,对文件、流、加密解密、转码、正则、线程、XML 等 JDK 方法进行封装,组成各种 Util 工具类。官网地址:https://www.hutool.cn/。 添加依赖 <dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artif…