数组区域检索的优化 --- 分块,线段树,树状数组

思考


首先让我们来思考一个问题,给定一个数组,和left与right的值,让你求这个数组中left到right之间元素的和,你会怎么计算?最简单的当然是遍历。如果有人问你这个问题的时候,他决对是会让你优化的,起码时间复杂度一定要小于O(n),那你打算怎么做呢?


很明确的一点是,如果要优化时间复杂度,就必须要提高空间复杂度,这是算法的局限,当然也是自然界的能量守恒定律。这是不可避免的,


所以接下来你可以思考下,有什么结构可以实现了。

分块处理

这种就真的很基础了,就是开辟一个额外的数组来存储区间和。

设数组大小为 n,我们将数组 nums分成多个块,每个块大小 size,最后一个块的大小为剩余的不超过 size 的元素数目,那么块的总数为 n/size,用一个数组 sum 保存每个块的元素和。

那么究竟要分成多少个数组,才能在数量和速度上都是最优解那? 这个关键在于size的取值。在求和时:设left位于b1个块内的第i1个元素,right位于b2个块内的第i2个元素。如果b1=b2,那么直接返回第b1个块位于区间[i1,i2]的元素的和;否则计算第b1个块位于区间[i,size)的元素之和sum1,第b2个块位于区间[0,i2]的元素之和sum2,第b1+1个块到第b2-1个块的元素的总和sum3,返回sum1+sum2+sum3。这个求和的时间复杂度为O(size+\frac{n}{size})。因为size+\frac{n}{size} \geqslant 2\sqrt[]{n},当且仅当size = {\sqrt[]{n}}时等号成立。因此size的取值为\sqrt[]{n}。此时的时间复杂度为O(\sqrt[]{n})

var NumArray = function(nums) {
    this.nums = nums;
    const n = nums.length;
    size = Math.floor(Math.sqrt(n));
    this.sum = new Array(Math.floor((n + size - 1) / size)).fill(0); // n/size 向上取整
    for (let i = 0; i < n; i++) {
        this.sum[Math.floor(i / size)] += nums[i];
    }
};

NumArray.prototype.update = function(index, val) {
    this.sum[Math.floor(index / size)] += val - this.nums[index];
    this.nums[index] = val;
};


NumArray.prototype.sumRange = function(left, right) {
    const b1 = Math.floor(left / size), i1 = left % size, b2 = Math.floor(right / size), i2 = right % size;
    if (b1 === b2) { // 区间 [left, right] 在同一块中
        let sum = 0;
        for (let j = i1; j <= i2; j++) {
            sum += this.nums[b1 * size + j];
        }
        return sum;
    }
    let sum1 = 0;
    for (let j = i1; j < size; j++) {
        sum1 += this.nums[b1 * size + j];
    }
    let sum2 = 0;
    for (let j = 0; j <= i2; j++) {
        sum2 += this.nums[b2 * size + j];
    }
    let sum3 = 0;
    for (let j = b1 + 1; j < b2; j++) {
        sum3 += this.sum[j];
    }
    return sum1 + sum2 + sum3;
};

线段树

线段树是一种二叉树,也就是对于一个线段,我们会用一个二叉树来表示。比如说一个长度为4的线段,我们可以表示成这样:

这是什么意思呢? 如果你要表示线段的和,那么最上面的根节点的权值表示的是这个线段 1 ∼ 4  的和。根的两个儿子分别表示这个线段中 1 ∼ 2 的和,与 3 ∼ 4 的和。以此类推。

然后我们还可以的到一个性质:节点 i 的权值 = 她的左儿子权值 + 她的右儿子权值。因为 1 ∼ 4  的和就是等于 1 ∼ 2 的和与 2 ∼ 3 的和的和。

给定区间 [left,right]时,我们将区间 [left,right] 拆成多个结点对应的区间。

  • 如果结点 node 对应的区间与 [left,right] 相同,可以直接返回该结点的值,即当前区间和。
  • 如果结点 node 对应的区间与 [left,right]不同,设左子结点对应的区间的右端点为 m,那么将区间 [left,right] 沿点 m拆成两个区间,分别计算左子结点和右子结点。

我们从根结点开始递归地拆分区间 [left,right]。

var NumArray = function(nums) {
    n = nums.length;
    this.segmentTree = new Array(nums.length * 4).fill(0);
    this.build(0, 0, n - 1, nums);
};

NumArray.prototype.update = function(index, val) {
    this.change(index, val, 0, 0, n - 1);
};

NumArray.prototype.sumRange = function(left, right) {
    return this.range(left, right, 0, 0, n - 1);
};

NumArray.prototype.build = function(node, s, e, nums) {
    if (s === e) {
        this.segmentTree[node] = nums[s];
        return;
    }
    const m = s + Math.floor((e - s) / 2);
    this.build(node * 2 + 1, s, m, nums);
    this.build(node * 2 + 2, m + 1, e, nums);
    this.segmentTree[node] = this.segmentTree[node * 2 + 1] + this.segmentTree[node * 2 + 2];
}

NumArray.prototype.change = function(index, val, node, s, e) {
    if (s === e) {
        this.segmentTree[node] = val;
        return;
    }
    const m = s + Math.floor((e - s) / 2);
    if (index <= m) {
        this.change(index, val, node * 2 + 1, s, m);
    } else {
        this.change(index, val, node * 2 + 2, m + 1, e);
    }
    this.segmentTree[node] = this.segmentTree[node * 2 + 1] + this.segmentTree[node * 2 + 2];
}

NumArray.prototype.range = function(left, right, node, s, e) {
    if (left === s && right === e) {
        return this.segmentTree[node];
    }
    const m = s + Math.floor((e - s) / 2);
    if (right <= m) {
        return this.range(left, right, node * 2 + 1, s, m);
    } else if (left > m) {
        return this.range(left, right, node * 2 + 2, m + 1, e);
    } else {
        return this.range(left, m, node * 2 + 1, s, m) + this.range(m + 1, right, node * 2 + 2, m + 1, e);
    }
}

树状数组

树状数组是一种可以动态维护序列前缀和的数据结构(序列下标从 1 开始)

如图,A是基本数组,C是求和数组,

其中,C[1]=A[1], C[2]=C[1]+A[2], C[3]=A[3], C[4]=C[2]+C[3]+A[4]......C[8]=C[4]+C[6]+C[7]+A[8]......

树状数组最简单最经典的使用场景,就是单点更新区间查询:

  • 单点修改 add(index,val):把序列第 index 个数增加 val;
  • 区间查询 prefixSum(index):查询前 index个元素的前缀和。

前置知识—lowbit(x)运算
如何计算一个非负整数n在二进制下的最低为1及其后面的0构成的数?
例如:44 的二进制表示为 (101100),最低为1和后面的0构成的数是( 100 ) 是 4 。

44的二进制=(101100),我们对44的二进制数取反+1,也即~44+1,得到-44

-44的二进制=(010100),然后我们把44和-44的二进制进行按位与运算,也即按位&得到,二进制000100,也就是十进制的4

所以lowbit(x) = x&(-x)

var NumArray = function(nums) {
    this.tree = new Array(nums.length + 1).fill(0);
    this.nums = nums;
    for (let i = 0; i < nums.length; i++) {
        this.add(i + 1, nums[i]);
    }
};

NumArray.prototype.update = function(index, val) {
    this.add(index + 1, val - this.nums[index]);
    this.nums[index] = val;
};

NumArray.prototype.sumRange = function(left, right) {
    return this.prefixSum(right + 1) - this.prefixSum(left);
};

NumArray.prototype.lowBit = function(x) {
    return x & -x;
}

NumArray.prototype.add = function(index, val) {
    while (index < this.tree.length) {
        this.tree[index] += val;
        index += this.lowBit(index);
    }
}

NumArray.prototype.prefixSum = function(index) {
    let sum = 0;
    while (index > 0) {
        sum += this.tree[index];
        index -= this.lowBit(index);
    }
    return sum;
}

树状数组和线段树的区别在哪


有人会问了既然线段树的问题能够用树状数组解决而且线段树还比树状数组扩展性强,那为什么不直接用线段树呢?问的很好,树状数组的作用就是为了简化线段树,举个例子:一个问题可以用线段树解决写代码半个小时,但是用树状数组只需要10分钟,那么你会选择哪一个算法呢?没错,基于某些简单的问题,我们没必要用到功能性强但实现复杂的线段树(杀鸡焉用宰牛刀)。当并不是所有能用线段树解决的问题,用树状数组都可以解决。
 

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

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

相关文章

vue项目路由使用history模式,nginx配置,刷新页面显示404

需要在配置项中添加 try_files $uri $uri/ /index.html;

削峰填谷:居民小区电动汽车有序充电策略研究

摘 要&#xff1a;针对电动汽车在居民小区无序充电对电网系统产生严重隐患及充电间时过长问题&#xff0c;提出一种采用延迟充电的电动汽车有序充电控制策略&#xff0c;并在分析国内外电动汽车有序充电的研究现状后&#xff0c;设计了居民小区电动汽车有序充电策略的总体框架。…

【源码复现】图神经网络之PPNP/APPNH

目录 1、论文简介2、论文核心介绍2.1、现有方法局限2.2、PageRank&Personalized PageRank2.3、PPNP&APPNP 3、源码复现3.1、模型总体框架3.2、PPNP3.3、APPNP3.4、MLP(两层) 1、论文简介 论文题目——《PREDICT THEN PROPAGATE: GRAPH NEURAL NETWORKS MEET PERSONALI…

转本考前4个月,手把手教你逆袭上岸

现在离转本考试的时间还剩下4个月&#xff0c;绝大多数同学会在之后的寒假期间全力学习&#xff0c;谁在这段时期懈怠&#xff0c;谁就丢掉了一半的分数。 不管是复习了很长一段时间&#xff0c;还是刚起步的同学&#xff0c;都有必要重新规划后面的复习。下面给大家讲讲&…

【中间件篇-Redis缓存数据库07】Redis缓存使用问题及互联网运用

Redis缓存使用问题 数据一致性 只要使用到缓存&#xff0c;无论是本地内存做缓存还是使用 redis 做缓存&#xff0c;那么就会存在数据同步的问题。 我以 Tomcat 向 MySQL 中写入和删改数据为例&#xff0c;来给你解释一下&#xff0c;数据的增删改操作具体是如何进行的。 我…

个推「数据驱动运营增长」城市巡回沙龙·上海专场:哈啰出行的自动化营销之路

近日&#xff0c;以“数据增能&#xff0c;高效提升用户运营价值”为主题的个推「数据驱动运营增长」城市巡回沙龙上海专场圆满举行。活动现场&#xff0c;哈啰出行基础算法负责人贾立从APP的营销场景切入&#xff0c;将“智能圈人->智能创意->智能活动->智能补贴->…

什么是指纹浏览器?——社媒营销多账号的管理神器

对于跨境卖家来说&#xff0c;通过海外社媒平台进行引流推广是不错的选择&#xff0c;但在实际操作中我们总会遇到很多问题。比如老手们肯定都经历过多个账号被封禁的情况&#xff0c;如果你也跟以前的东哥一样困扰怎么在一台电脑登录同平台多个账号&#xff0c;那今天这篇文章…

Go利用反射实现一个ini文件的解析器程序

package mainimport ("bufio" // 逐行读取配置文件"fmt""log""os""reflect""strconv""strings" )type Config struct { // 定义配置结构体Section1 Section1 ini:"section1" // 嵌套结构体1…

SAP系统供应商预付款请求和预付账款业务

最近搞清帐&#xff01; 在SAP中处理客户或供应商的预收/预付款相关业务流程操作说明, 首先由业务部门(销售或采购)下达销售/采购订单,同时基于订单提交预收/预付申请,客户/供应商款项到账时,由财务部门在SAP中勾选申请单来收付款;最后在财务转应收/应付转发票时自动核销。预付…

立体库堆垛机水平电机输出控制程序功能

###############水平电机输出控制程序################# #############水平变频器输出控制程序################# *******************水平速度曲线建立*********************** 列距离差值&#xff0c;建立与速度的关系式&#xff1a;VX/k MW220为K系数 水平速度控制K系数 列…

RK3568驱动指南|第七期-设备树-第66章of操作函数实验:获取设备树节点

瑞芯微RK3568芯片是一款定位中高端的通用型SOC&#xff0c;采用22nm制程工艺&#xff0c;搭载一颗四核Cortex-A55处理器和Mali G52 2EE 图形处理器。RK3568 支持4K 解码和 1080P 编码&#xff0c;支持SATA/PCIE/USB3.0 外围接口。RK3568内置独立NPU&#xff0c;可用于轻量级人工…

圆形承重钢管用在线直线度测量仪实时检测品质!

关于直线度尺寸的检测&#xff0c;相信你听说过很多诸如直线法、直尺法、激光准直法等的离线检测方式&#xff0c;那你听说过在线直线度测量仪吗&#xff1f;它是可安装在产线上进行实时在线检测的设备。本文就跟随小编一起来简单的了解一下。 在线直线度测量仪的测量方法 直…

Notepad++,搜索窗口独立后,恢复

双击一下find result框&#xff0c;恢复到原来的模式。

使用cmd运行控制面板工具

如何通过键入命令运行“控制面板”工具 - Microsoft 支持 windows自带管理工具&#xff08;exe/cpl/msc&#xff09;-CSDN博客 CMD下打开系统各面板_cmd打开轻松使用面板-CSDN博客 示例&#xff1a; rundll32.exe shell32.dll,Control_RunDLL powercfg.cpl 替换powercfg.…

光计算1周2篇Nature,英伟达的时代彻底结束!

近期&#xff0c;光计算领域连续发出重量级文章&#xff0c;刊登在学术界的顶级期刊上。一时间&#xff0c;各大媒体纷纷转发&#xff0c;读者们也纷纷感叹&#xff1a;中国芯片取代英伟达的机会来了&#xff01;今天&#xff0c;光子盒用这篇万字长文为大家梳理光计算的背景、…

FL Studio最新版本号21.2发行更新啦

Image Line宣布发布FL Studio 21.2。更新带来了许多改进&#xff0c;但主要功能是引入了新的词干分离功能和FL Cloud&#xff0c;这是一个新的在线平台&#xff0c;直接与DAW集成&#xff0c;为用户提供从循环和样本到母带和发行功能的一切。 词干分离与FL云 随着最新更新的发…

记一次线上问题引发的对 Mysql 锁机制分析

背景 最近双十一开门红期间组内出现了一次因 Mysql 死锁导致的线上问题&#xff0c;当时从监控可以看到数据库活跃连接数飙升&#xff0c;导致应用层数据库连接池被打满&#xff0c;后续所有请求都因获取不到连接而失败 整体业务代码精简逻辑如下&#xff1a; Transaction p…

DP4301-M无线模块一款SUB-1G无线收发模块

DP4301-M无线模块是一款低成本高效率工作于1GHz以内的收发模块&#xff0c;支持中国智能电无线 集抄标准470MHz~ 510MHz&#xff0c;兼容433MHz ISM/SRD频段均可使用。 此模块且前已经超大量应用于国标智能无线抄表及物联网自组网等双向数据传输系统方案&#xff0c;模 块具备的…

ADFS 高可用配置 + NLB配置(Windows网络负载均衡)

ADFS 高可用配置 NLB配置&#xff08;Windows网络负载均衡&#xff09; ADFS安装配置NLB配置节点 TEST-ADFS-01 网络负载平衡配置节点 TEST-ADFS-02 网络负载平衡修改CRM配置 ADFS实现高可用负载均衡有两种&#xff0c;主要是在数据库的选择方式&#xff1a; windows自带的内…

力扣第647题 回文子串 c++ 动态规划 双指针 附Java代码 注释解释版

题目 647. 回文子串 中等 相关标签 字符串 动态规划 给你一个字符串 s &#xff0c;请你统计并返回这个字符串中 回文子串 的数目。 回文字符串 是正着读和倒过来读一样的字符串。 子字符串 是字符串中的由连续字符组成的一个序列。 具有不同开始位置或结束位置的子串…