数据结构——树状数组

文章目录

  • 前言
  • 问题引入
  • 问题分析
  • 树状数组
    • `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这类题应该没有多大问题

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

RobotFramework之用例执行时添加命令行参数(十三)

学习目录 引言 标签tag 设置变量 随机执行顺序 设置监听器 输出日志目录和文件 引言 Robot Framework 提供了许多命令行选项&#xff0c;可用于控制测试用例的执行方式以及生成的输出。本节介绍一些常用的选项语法。 标签tag 之前文章我们介绍过&#xff0c;在测试套件…

基于STC12C5A60S2系列1T 8051单片的模数芯片ADC0809实现模数转换应用

基于STC12C5A60S2系列1T 8051单片的模数芯片ADC0809实现模数转换应用 STC12C5A60S2系列1T 8051单片机管脚图STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式及配置STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式介绍模数芯片ADC0809介绍通过模数芯片ADC0809把电压模…

基于灰色神经网络的预测算法——订单需求预测

大家好&#xff0c;我是带我去滑雪&#xff01; 灰色系统理论的不确定性处理与神经网络的非线性建模相结合&#xff0c;有望更好地处理实际问题中的不确定性和复杂性。本期使用灰色神经网络实现预测冰箱订单需求。 一、问题背景与模型建立 &#xff08;1&#xff09;灰色理论…

中国制库:创新引领,效率突破,塑造行业新标准

制库是一家专注于企业知识应用的在线SAAS平台,主要构成部分包括制度、表单、流程、制问和集合。作为集合了各种管理制度的平台,制库不仅提供了丰富的制度资源,还通过SAAS版实现了知识集成、修订和应用的全流程。目标是打造中国全面的企业制度库,帮助企业快速建立核心管理系统,并…

ON1 Photo RAW MAX 2024 v18.0.4.14758

ON1 Photo RAW MAX 2024 for mac是一款专业的raw照片编辑软件&#xff0c;提供了各种各样的编辑工具&#xff0c;包括调整曝光、对比度、色彩、锐化、裁剪、旋转和去除红眼等功能&#xff0c;用户可以根据具体需求对照片进行精确的调整。ON1 Photo RAW MAX 2024还提供了智能修复…

98.qt qml-使用曲线图综合示例、支持多种鼠标交互、支持百万数据显示(已适配黑白风格)

在上章我们只是简单实现了曲线图和折线图的显示: 79.qt qml-如何在QML中使用QCustomPlot之曲线/折线示例(已适配黑白风格)_qml 折线图_诺谦的博客-CSDN博客 所以本章实现综合示例、并添加多种功能如下所示: 详细显示:鼠标任意移动显示具体值内容鼠标右击: 弹出菜单栏,支持…

【C++】:继承

朋友们、伙计们&#xff0c;我们又见面了&#xff0c;本期来给大家解读一下有关C继承的知识点&#xff0c;如果看完之后对你有一定的启发&#xff0c;那么请留下你的三连&#xff0c;祝大家心想事成&#xff01; C 语 言 专 栏&#xff1a;C语言&#xff1a;从入门到精通 数据结…

洛谷 P1064 [NOIP2006 提高组] 金明的预算方案 python解析

P1064 [NOIP2006 提高组] 金明的预算方案 时间&#xff1a;2023.11.19 题目地址&#xff1a;[NOIP2006 提高组] 金明的预算方案 题目分析 动态规划的0-1背包&#xff0c;采用动态数组。如果不了解的话&#xff0c;可以先看看这个背包DP。 这个是0-1背包的标准状态转移方程 f…

域名的理解

域名的分类 见下图 这里引用的阿里云对域名的定义&#xff0c;个人理解是有两种叫法&#xff0c;一种是传统的叫法&#xff0c;也就是将sample.org.cn划分成了三级域名&#xff0c;还有一种叫法是基于用户注册的域名来说的&#xff0c;将用户注册的整体域名称作一级域名&…

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

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

Java Swing实现简单的文本编辑器

内容要求 1) 本次程序设计是专门针对 Java 课程的,要求使用 Java 语言进行具有一定代码量的程序开发。程序的设计要结合一定的算法&#xff0c;在进行代码编写前要能够设计好自己的算法。 本次程序设计涉及到 Java 的基本语法&#xff0c;即课堂上所介绍的变量、条件语句、循…

Jmeter配置脚本录制进行抓包并快速分析、定位接口问题

对于测试人员、开发人员来说&#xff0c;善用抓包工具确实是快速分析和定位问题的一大必备神技&#xff0c;现将配置过程记录如下: 1、打开jmeter后&#xff0c;首先添加—个线程组: 2、线程组可以重新命名按项目名称分类: 如果你想学习自动化测试&#xff0c;我这边给你推荐一…

Leetcode经典题目之“双指针交换元素“类题目

1 LC 27. 移除元素 class Solution {public int removeElement(int[] nums, int val) {int nnums.length;int s0;for(int i0;i<n;i){// 只有不等于目标值的时候才会进行交换&#xff0c;然后移动s指针if(nums[i]!val){swap(nums,i,s);}}return s;}void swap(int[]nums, int…

22. 深度学习 - 自动求导

Hi&#xff0c;你好。我是茶桁。 咱们接着上节课内容继续讲&#xff0c;我们上节课已经了解了拓朴排序的原理&#xff0c;并且简单的模拟实现了。我们这节课就来开始将其中的内容变成具体的计算过程。 linear, sigmoid和loss这三个函数的值具体该如何计算呢&#xff1f; 我们…

『力扣刷题本』:环形链表(判断链表是否有环)

一、题目 给你一个链表的头节点 head &#xff0c;判断链表中是否有环。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环&#xff0c;评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置&am…

苹果iOS系统开发APP应用启动几种速度优化技巧与实践

在移动应用开发过程中&#xff0c;启动速度是影响用户体验的关键因素之一。一个应用如果启动迅速&#xff0c;会给用户留下良好的第一印象&#xff0c;相反&#xff0c;如果启动缓慢&#xff0c;用户的耐心和满意度可能会大打折扣。对于iOS开发者而言&#xff0c;优化启动速度不…

uart_printf自定义串口printf输出

暂时只格式化了%s和%c&#xff0c;需要其他格式化的可自行添加&#xff0c;后续也可能更新 标准库 #include <stdarg.h> //需要包含的头文件--->任意参数功能需要void UART_printf(USART_TypeDef *USARTx, const char *fmt, ...) {va_list args;va_start(args, fmt)…

安装第三方包报错 error: Microsoft Visual C++ 14.0 or greater is required——解决办法

1、问题描述 手动安装第三方软件时&#xff0c;可以使用setup.py&#xff0c;来安装已经下载的第三方包。一般文件下会存在setup&#xff0c;在所要安装库的目录下的cmd执行&#xff1a;python setup.py install报错&#xff1a;error: Microsoft Visual C 14.0 or greater i…

详解ssh远程登录服务

华子目录 简介概念功能 分类文字接口图形接口 文字接口ssh连接服务器浅浅介绍一下加密技术凯撒加密加密分类对称加密非对称加密非对称加密方法&#xff08;也叫公钥加密&#xff09; ssh两大类认证方式&#xff1a;连接加密技术简介密钥解析 ssh工作过程版本协商阶段密钥和算法…

程序员如何做事更细致?

最近在工作中老是犯一些小错误&#xff0c;哦&#xff0c;当然也不是最近了&#xff0c;其实我一直是个马虎的人&#xff0c;我很讨厌做一些细活&#xff0c;因为这会让我反复改动多次在会成功&#xff0c;而平时的代码由于有debug&#xff0c;即便出错了&#xff0c;再改回来即…