当前位置: 首页 > news >正文

树状数组简单介绍

树状数组简单介绍

  • 前言
  • 树状数组(Binary Indexed Tree)JavaScript 详细指南
    • 一、什么是树状数组?
    • 二、核心概念(前置知识):lowbit 函数
    • 三、树状数组的实现
      • 1. 初始化树状数组
      • 2. 使用示例
    • 四、详细原理解释
      • 1. 树状数组结构
      • 2. 更新操作流程
      • 3. 查询操作流程
    • 五、实际应用场景
      • 1. 计算逆序对
      • 2. 区间更新与单点查询(差分数组)
    • 六、与线段树的比较
    • 七、常见问题解答
    • 八、完整示例:动态排名系统


前言

树状数组简单介绍


树状数组(Binary Indexed Tree)JavaScript 详细指南

树状数组(也称为 Binary Indexed Tree 或 Fenwick Tree)是一种高效处理动态前缀和查询与更新的数据结构。
对于新手来说,理解它可能有些挑战,但我会用 JavaScript 示例和详细解释带你逐步掌握它。

补充:博主是看了b站这个视频理解树状数组的
链接: 【树状数组,就是这么简单!】

一、什么是树状数组?

树状数组是一种可以高效进行以下两种操作的数据结构:

  1. 单点更新:给数组中的某个元素加上一个值(时间复杂度 O(log n))
  2. 前缀查询:查询数组前 n 个元素的和(时间复杂度 O(log n))

相比普通数组:

  • 普通数组:单点更新 O(1),前缀查询 O(n)
  • 前缀和数组:单点更新 O(n),前缀查询 O(1)
  • 树状数组:两者都是 O(log n),在频繁更新和查询的场景下非常高效

二、核心概念(前置知识):lowbit 函数

树状数组的核心是一个神奇的 lowbit 函数,它获取数字二进制表示中最低位的 1 所代表的值。

function lowbit(x) {return x & -x;
}

例如:

  • lowbit(6) = 2,因为 6 的二进制是 110,最低位的 1 代表 2
  • lowbit(8) = 8,因为 8 的二进制是 1000
  • lowbit(7) = 1,因为 7 的二进制是 111

三、树状数组的实现

1. 初始化树状数组

无注释版

class FenwickTree {constructor(size) {this.size = size;this.tree = new Array(size + 1).fill(0); // 树状数组下标从1开始}// 更新操作:给位置i的元素加上deltaupdate(i, delta) {while (i <= this.size) {this.tree[i] += delta;i += lowbit(i); // 向上更新父节点}}// 查询操作:求前i个元素的和query(i) {let sum = 0;while (i > 0) {sum += this.tree[i];i -= lowbit(i); // 向左查询前驱节点}return sum;}// 区间查询:[i, j]的和rangeQuery(i, j) {return this.query(j) - this.query(i - 1);}
}

带注释版:

// 树状数组的核心是一个神奇的 `lowbit` 函数,它获取数字二进制表示中最低位的 1 所代表的值。
function lowBit(i) {return (-i) & i
}// 树状数组类class fenwickTree {// 构造函数constructor(size) {// 方便记忆,原数组记为  a[1, 2, 3……]this.size = size // 原数组的长度 this.tree = new Array(size + 1).fill(0) // 树状数组的下标从1开始!!!,并且全初始化为0}// lowBit函数 获取数字二进制表示中最低位的 1 所代表的值lowBit(i) {return (-i) & i}// 点更新  在树状数组的当前元素以及后继(也称 祖宗)上 添加新值// locate: 在树的哪个索引  num:要添加的数值update(locate, num) {// 写法一:for(locate; locate <= this.size; locate += lowBit(locate)) this.tree[locate] += num// 写法二:// while(locate <= this.size) {//   this.tree[locate] += num//   locate += lowBit(locate)// }}// 区间求和(前缀和)// n:原数组前n项的和,复杂度为  O(log n)    一般方法求和为O(n)getSum(n) {// 求前缀和:定义一个 sum 存放求和后的总值,每次都加上 tree[n]以及所有前驱的值let sum = 0// 写法一:for(n; n > 0; n -= lowBit(n)) sum += this.tree[n]// 写法二:// while(n > 0) {//   sum += this.tree[n]//   n -= lowBit(n)// }return sum}// JS居然不支持重载函数!!!getBlockSum(start, end) {return this.getSum(end) - this.getSum(start - 1)}
}const arr = [1, 2, 3, 4, 5]
const fenwick = new fenwickTree(arr.length) // 创建一个树状数组实例for(let i = 0; i < arr.length; i++) fenwick.update(i + 1, arr[i]) // 更新树状数组console.log(fenwick.getSum(5)) // 15
console.log(fenwick.getBlockSum(3, 5)) // 3 + 4 + 5 = 12

2. 使用示例

// 原始数组
const nums = [1, 3, 5, 7, 9, 11]; // 下标从0开始// 初始化树状数组
const fenwick = new FenwickTree(nums.length);// 构建树状数组
for (let i = 0; i < nums.length; i++) {fenwick.update(i + 1, nums[i]); // 注意树状数组下标从1开始
}console.log(fenwick.query(3)); // 前3个元素的和:1 + 3 + 5 = 9
console.log(fenwick.rangeQuery(2, 4)); // 第2到第4个元素的和:3 + 5 + 7 = 15// 更新第3个元素(原始数组的索引2)加2
fenwick.update(3, 2); // 5 → 7console.log(fenwick.query(3)); // 现在前3个元素的和:1 + 3 + 7 = 11

四、详细原理解释

1. 树状数组结构

树状数组的每个节点存储的是一段区间的和:

  • tree[1] = nums[0]
  • tree[2] = nums[0] + nums[1]
  • tree[3] = nums[2]
  • tree[4] = nums[0] + nums[1] + nums[2] + nums[3]
  • 以此类推

2. 更新操作流程

当更新 nums[i] 时,需要更新所有包含它的区间:(即更新 所有后继tree[i]的直接后继为tree[i + lowbit(i)

  1. i+1 开始(因为树状数组下标从1开始)
  2. 每次加上 lowbit(i),直到超过数组长度
  3. 沿途所有节点都加上变化值

例如更新 nums[2](即第3个元素):

  • 更新 tree[3]
  • 3 + lowbit(3)=4 → 更新 tree[4]
  • 4 + lowbit(4)=8 → 如果数组长度≥8则更新 tree[8]

3. 查询操作流程

查询前 i 个元素的和:(即每次要加上 所有前驱tree[i]的直接前驱为tree[i - lowbit(i)]

  1. i 开始
  2. 每次减去 lowbit(i),直到为0
  3. 累加沿途所有节点的值

例如查询前5个元素的和:

  • 加上 tree[5]
  • 5 - lowbit(5)=4 → 加上 tree[4]
  • 4 - lowbit(4)=0 → 结束
  • 总和 = tree[5] + tree[4]

五、实际应用场景

1. 计算逆序对

function countInversions(nums) {// 离散化处理const sorted = [...new Set(nums)].sort((a, b) => a - b);const rank = new Map(sorted.map((num, idx) => [num, idx + 1]));const fenwick = new FenwickTree(sorted.length);let inversions = 0;// 从后向前遍历for (let i = nums.length - 1; i >= 0; i--) {const r = rank.get(nums[i]);inversions += fenwick.query(r - 1); // 查询比当前数小的数的个数fenwick.update(r, 1); // 当前数出现次数+1}return inversions;
}console.log(countInversions([5, 3, 2, 4, 1])); // 输出逆序对数量

2. 区间更新与单点查询(差分数组)

class FenwickTreeRangeUpdate {constructor(size) {this.size = size;this.tree1 = new Array(size + 1).fill(0); // 维护差分数组this.tree2 = new Array(size + 1).fill(0); // 维护i*差分数组}// 区间[l,r]加上valrangeUpdate(l, r, val) {this._update(l, val);this._update(r + 1, -val);}_update(i, val) {const v1 = val;const v2 = i * val;while (i <= this.size) {this.tree1[i] += v1;this.tree2[i] += v2;i += lowbit(i);}}// 查询前i个元素的和query(i) {let sum = 0;let x = i;while (i > 0) {sum += (x + 1) * this.tree1[i] - this.tree2[i];i -= lowbit(i);}return sum;}// 单点查询get(i) {return this.query(i) - this.query(i - 1);}
}

六、与线段树的比较

特性树状数组线段树
代码复杂度简单复杂
时间复杂度两者相同两者相同
功能主要处理前缀和可以处理各种区间操作
空间O(n)O(4n)
适用场景前缀和、逆序对区间最值、复杂区间操作

七、常见问题解答

Q: 为什么树状数组下标从1开始?
A: 因为 lowbit(0) = 0 会导致无限循环,从1开始更方便计算

Q: 树状数组能处理最大值/最小值吗?
A: 可以但不推荐,实现复杂且效率不如线段树,建议用线段树处理最值问题

Q: 如何选择树状数组和线段树?
A: 如果只需要前缀和/单点更新,用树状数组;需要更复杂的区间操作,用线段树

八、完整示例:动态排名系统

class DynamicRanking {constructor(maxValue) {this.maxValue = maxValue;this.ft = new FenwickTree(maxValue);}// 插入一个数insert(num) {this.ft.update(num, 1);}// 删除一个数remove(num) {this.ft.update(num, -1);}// 查询小于num的数的个数rank(num) {return this.ft.query(num - 1);}// 查询第k小的数(k从1开始)select(k) {let left = 1, right = this.maxValue;while (left < right) {const mid = Math.floor((left + right) / 2);if (this.ft.query(mid) < k) {left = mid + 1;} else {right = mid;}}return left;}
}const dr = new DynamicRanking(100);
dr.insert(5);
dr.insert(3);
dr.insert(8);
dr.insert(3);
console.log(dr.rank(5)); // 输出2(有两个数小于5)
console.log(dr.select(2)); // 输出3(第2小的数是3)

通过这个指南,你应该已经掌握了树状数组的基本概念、实现方法和实际应用。记住,理解 lowbit 函数是关键,而多练习实际编码会帮助你更好地掌握这种数据结构。

相关文章:

  • 内釜底阀解析:V型球阀与C型球阀的应用对比-耀圣
  • 如何让 Rust + WebAssembly `.wasm` 更小更快?从构建配置到源码重构的全流程指南
  • 国产DPU芯片+防火墙,能否引领网络安全新跨越?
  • 使用 Java 8 Stream实现List重复数据判断
  • C# 类型、存储和变量(类型是一种模板)
  • SQL Server 2022 安装常见问题及解决方法
  • AI编程新纪元:GitHub Copilot、CodeGeeX与VS2022的联合开发实践
  • CobaltStrike
  • 工作记录4
  • Spring Boot 中的自动配置原理
  • Flutter使用flutter_driver进行自动化测试
  • Python刷题笔记1
  • Golang|KVBitcask
  • springboot3 cloud gateway 配置websocket代理转发教程
  • [dp14_回文串] 分割回文串 II | 最长回文子序列 | 让字符串成为回文串的最少插入次数
  • 【JavaEE】Spring AOP的注解实现
  • Java大模型MCP服务端开发-数据库查询(智能问数)
  • 基于PLC的停车场车位控制系统的设计
  • Ubuntu 安装 NVIDIA显卡驱动、CUDA 以及 CuDNN工具
  • [ElasticSearch]Suggest查询建议(自动补全纠错)
  • 做网站前怎么写文档/网站页面优化方案
  • 网站建设和维护实训/线上推广营销
  • 长春教做网站带维护的培训机构/网络营销方法有哪些
  • 网站有什么/百度推广开户费用
  • 百货店怎么做网站送货/免费发帖推广的平台
  • 网站开发学费/泰州seo平台