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

【数据结构】线段树

目录

  • 1.概述
  • 2.代码实现
    • 2.1.聚合操作——求和
    • 2.2.聚合操作——求和、求最小值、求最大值
  • 3.应用
  • 4.与前缀和之间的区别

更多数据结构与算法的相关知识可以查看数据结构与算法这一专栏。

1.概述

(1)线段树 (Segment Tree) 是一种二叉树形数据结构,经常用于高效地处理一维区间的各种查询和修改问题。

(2)一个线段树通常对应于一个区间,每个节点表示一个区间,具体如下图所示。

  • 对于线段树中的每个节点,它有一个区间范围和一个值。
  • 叶节点表示区间中的单个元素,而非叶子节点表示区间中的所有元素。
  • 线段树的每个节点表示区间的一部分,其左子树表示左半部分区间,右子树表示右半部分区间。因此,线段树的叶节点数总是等于数据元素的个数,而线段树的高度为 ⌈logn⌉ + 1,其中 n 为元素总个数。

在这里插入图片描述

① 上图来自线段树_百度百科。
② 一般来说,在代码中会用数组来存储某个区间内的元素,该数组内的元素可以是无序或者有序的,例如,nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 或者 nums = [2, 4, -1, 0, 9] 等。上图中线段树中的区间正好是前一个数组。

(3)线段树的主要优势是能够在 O(logn) 时间复杂度内执行区间查询(如最大值、最小值、区间和等)和区间修改操作(如区间加、区间减等),因此它非常适合解决那些需要频繁区间查询和修改的问题。

2.代码实现

(1)在线段树中,区间的聚合值是指该区间内元素的某种聚合操作的结果。这个聚合操作可以是求和求最小值求最大值等。聚合值的具体含义取决于所解决的问题,本节中分别给出以下两种情况。

(2)线段树的构建过程与 108.将有序数组转换为二叉搜索树这题类似,具体如下:

  • 定义线段树节点:线段树是一种二叉树,每个节点代表一个区间。每个节点包含了该区间的起始点start、结束点end,以及其他你可能需要的附加信息。
  • 定义递归构建函数:创建一个递归函数来构建线段树。该函数接收输入参数为当前节点、当前区间的起始点和结束点。
  • 基本情况处理:对于当前节点,如果起始点和结束点相等,表示当前节点为叶子节点,直接返回。
  • 划分区间:计算当前区间的中点 mid,将区间分割成两个子区间。通常是将区间一分为二,可以选择将 mid 设置为 (start+end)/2。
  • 递归构建左子树和右子树:调用递归函数,传入左子树和右子树的起始点和中点以构建左右子树。
  • 合并信息:在递归回溯时,将左右子树的信息合并到当前节点。这通常取决于你的问题需求,可以是求和、求最大值、求最小值等。
  • 返回根节点:递归构建完成后,返回根节点。

2.1.聚合操作——求和

(1)实现区间求和操作(包括修改区间的某个元素)的代码实现如下:

class SegmentTree {//线段树数组,segmentTree[i] 表示线段树的第 i 个节点(区间)的聚合值,本代码中是区间和int[] segmentTree;//原始数组int[] nums;public SegmentTree(int[] nums) {this.nums = nums;int n = nums.length;//确定树的高度int height = (int) (Math.ceil(Math.log(n) / Math.log(2))) + 1;//根据树的高度计算需要的线段树数组大小int maxSize = (int) Math.pow(2, height) - 1;//创建线段树数组segmentTree = new int[maxSize];//构建线段树buildTree(0, 0, n - 1);}//构建线段树private int buildTree(int index, int start, int end) {//叶子节点if (start == end) {//叶子节点存储对应的原始数组值segmentTree[index] = nums[start];return segmentTree[index];}int mid = start + (end - start) / 2; // 计算中间位置//分别递归构建左子树和右子树segmentTree[index] = buildTree(2 * index + 1, start, mid) +buildTree(2 * index + 2, mid + 1, end);return segmentTree[index];}//更新原始数组中的某个元素,并同时更新线段树public void update(int i, int val) {//计算变化的差值int diff = val - nums[i];//更新原始数组中的值nums[i] = val;//更新线段树updateTree(0, 0, nums.length - 1, i, diff);}//更新线段树private void updateTree(int index, int start, int end, int i, int diff) {if (i < start || i > end) {//该节点不包含要更新的元素,直接返回return;}//更新当前节点的值segmentTree[index] += diff;if (start != end) {//计算中间位置int mid = start + (end - start) / 2;//递归更新左子树updateTree(2 * index + 1, start, mid, i, diff);//递归更新右子树updateTree(2 * index + 2, mid + 1, end, i, diff);}}//查询线段树中某个区间的和public int querySum(int left, int right) {return queryTree(0, 0, nums.length - 1, left, right);}// 查询线段树private int queryTree(int index, int start, int end, int left, int right) {if (left > end || right < start) {//区间不相交,返回 0return 0;}if (left <= start && right >= end) {//当前节点表示的区间完全被查询区间包含,直接返回当前节点的值return segmentTree[index];}//计算中间位置int mid = start + (end - start) / 2;//分别递归查询左子树和右子树return queryTree(2 * index + 1, start, mid, left, right) +queryTree(2 * index + 2, mid + 1, end, left, right);}
}

(2)测试代码如下:

class SegmentTreeTest {public static void main(String[] args) {//原始数组,可以是有序或者无序的int[] nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};SegmentTree segmentTree = new SegmentTree(nums);//查询区间 [1, 4] 的和,即 nums[1...4] 的和int sum = segmentTree.querySum(1, 4);System.out.println("Sum of range [1, 4]: " + sum);//将数组下标为 2 的元素更新为 6,即更新 nums[2] = 6,同时更新线段树segmentTree.update(2, 6);//再次查询区间 [1, 4] 的和sum = segmentTree.querySum(1, 4);System.out.println("Updated sum of range [1, 4]: " + sum);}
}

输出结果如下:

Sum of range [1, 4]: 14
Updated sum of range [1, 4]: 17

2.2.聚合操作——求和、求最小值、求最大值

(1)实现区间求和、求最小值、求最大值操作(包括修改区间的某个元素)的代码实现如下:

class SegmentTree {private Node root;//定义节点类,用于表示某个区间private class Node {int start;int end;int sum;int max;int min;Node left;Node right;Node(int start, int end) {this.start = start;this.end = end;this.sum = 0;this.max = Integer.MIN_VALUE;this.min = Integer.MAX_VALUE;}}public SegmentTree(int[] nums) {this.root = build(nums, 0, nums.length - 1);}//构建线段树private Node build(int[] nums, int start, int end) {if (start > end) {return null;}Node node = new Node(start, end);if (start == end) {node.sum = nums[start];node.max = nums[start];node.min = nums[start];} else {int mid = start + (end - start) / 2;node.left = build(nums, start, mid);node.right = build(nums, mid + 1, end);node.sum = node.left.sum + node.right.sum;node.max = Math.max(node.left.max, node.right.max);node.min = Math.min(node.left.min, node.right.min);}return node;}//查询线段树中某个区间的和public int queryRangeSum(int start, int end) {return queryRangeSum(root, start, end);}private int queryRangeSum(Node node, int start, int end) {if (node.start == start && node.end == end) {return node.sum;}int mid = node.start + (node.end - node.start) / 2;if (end <= mid) {return queryRangeSum(node.left, start, end);} else if (start > mid) {return queryRangeSum(node.right, start, end);} else {return queryRangeSum(node.left, start, mid) + queryRangeSum(node.right, mid + 1, end);}}//查询线段树中某个区间的最大值public int queryRangeMax(int start, int end) {return queryRangeMax(root, start, end);}private int queryRangeMax(Node node, int start, int end) {if (node.start == start && node.end == end) {return node.max;}int mid = node.start + (node.end - node.start) / 2;if (end <= mid) {return queryRangeMax(node.left, start, end);} else if (start > mid) {return queryRangeMax(node.right, start, end);} else {return Math.max(queryRangeMax(node.left, start, mid),queryRangeMax(node.right, mid + 1, end));}}//查询线段树中某个区间的最小值public int queryRangeMin(int start, int end) {return queryRangeMin(root, start, end);}private int queryRangeMin(Node node, int start, int end) {if (node.start == start && node.end == end) {return node.min;}int mid = node.start + (node.end - node.start) / 2;if (end <= mid) {return queryRangeMin(node.left, start, end);} else if (start > mid) {return queryRangeMin(node.right, start, end);} else {return Math.min(queryRangeMin(node.left, start, mid),queryRangeMin(node.right, mid + 1, end));}}//更新原始数组中的某个元素,并同时更新线段树public void update(int index, int value) {update(root, index, value);}private void update(Node node, int index, int value) {if (node.start == node.end) {node.sum = value;node.max = value;node.min = value;return;}int mid = node.start + (node.end - node.start) / 2;if (index <= mid) {update(node.left, index, value);} else {update(node.right, index, value);}node.sum = node.left.sum + node.right.sum;node.max = Math.max(node.left.max, node.right.max);node.min = Math.min(node.left.min, node.right.min);}
}

(2)测试代码如下:

class SegmentTreeTest {public static void main(String[] args) {//原始数组,可以是有序或者无序的int[] nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};SegmentTree segmentTree = new SegmentTree(nums);//查询区间 [1, 4] 的和,即 nums[1...4] 的和int sum = segmentTree.queryRangeSum(1, 4);System.out.println("Sum of range [1, 4]: " + sum);int max = segmentTree.queryRangeMax(1, 4);System.out.println("Max of range [1, 4]: " + max);int min = segmentTree.queryRangeMin(1, 4);System.out.println("Min of range [1, 4]: " + min);//将数组下标为 1 的元素更新为 0,即更新 nums[1] = 0,同时更新线段树segmentTree.update(1, 0);//将数组下标为 2 的元素更新为 6,即更新 nums[2] = 6,同时更新线段树segmentTree.update(2, 6);//再次查询区间 [1, 4] 的和sum = segmentTree.queryRangeSum(1, 4);System.out.println("Updated sum of range [1, 4]: " + sum);max = segmentTree.queryRangeMax(1, 4);System.out.println("Updated Sum of range [1, 4]: " + max);min = segmentTree.queryRangeMin(1, 4);System.out.println("Updated Min of range [1, 4]: " + min);}
}

输出结果如下:

Sum of range [1, 4]: 14
Max of range [1, 4]: 5
Min of range [1, 4]: 2
Updated sum of range [1, 4]: 15
Updated Sum of range [1, 4]: 6
Updated Min of range [1, 4]: 0

3.应用

(1)LeetCode 中的 307.区域和检索 - 数组可修改这题便是对线段树的具体应用,其题目如下。显然,使用上面的代码可以直接求解。

在这里插入图片描述

(2)大家可以去 LeetCode 上找相关的线段树的题目来练习,或者也可以直接查看 LeetCode 算法刷题目录 (Java) 这篇文章中的线段树章节。如果大家发现文章中的错误之处,可在评论区中指出。

4.与前缀和之间的区别

(1)线段树和前缀和是两种常见的用于解决区间查询问题的数据结构,它们有一些区别:

  • 数据结构
    • 线段树是一种二叉树结构,用于处理区间查询和更新操作。它将区间划分为不相交的子区间,并将每个子区间的信息存储在相应节点中。
    • 前缀和是一个数组,用于存储前缀和值。它通过计算数组元素累加和的方式存储数据。
  • 功能
    • 线段树可以支持多种区间查询操作,例如区间和、区间最大值、区间最小值等。它可以在 O(logN) 的时间复杂度内完成查询和更新操作。
    • 前缀和主要用于计算数组中特定区间的和。它可以在 O(1) 的时间内计算出给定区间的和,但只能处理区间和的查询。
  • 空间复杂度
    • 线段树的空间复杂度为 O(N),其中 N 是数组的大小。它需要存储整个线段树的节点。
    • 前缀和的空间复杂度为 O(N),其中 N 是数组的大小。它只需要存储一个与数组大小相等的前缀和数组。
  • 应用场景
    • 线段树通常用于解决需要频繁进行区间查询和更新操作的问题,比如计算数组的区间和、区间最大值和最小值等。
    • 前缀和通常用于解决需要频繁计算数组特定区间和的问题,比如计算子数组的和、快速判断数组中是否存在某个区间的和等。

(2)综上所述,线段树和前缀和在功能和应用场景上略有不同,选择使用哪种数据结构取决于具体的问题需求和效率要求。

有关前缀和的相关知识可以参考【数据结构】前缀和数组这篇文章。

相关文章:

【数据结构】线段树

目录 1.概述2.代码实现2.1.聚合操作——求和2.2.聚合操作——求和、求最小值、求最大值 3.应用4.与前缀和之间的区别 更多数据结构与算法的相关知识可以查看数据结构与算法这一专栏。 1.概述 &#xff08;1&#xff09;线段树 (Segment Tree) 是一种二叉树形数据结构&#xff…...

王道数据结构课后代码题p175 06.已知一棵树的层次序列及每个结点的度,编写算法构造此树的孩子-兄弟链表。(c语言代码实现)

/* 此树为 A B C D E F G 孩子-兄弟链表为 A B E C F G D */ 本题代码如下 void createtree(tree* t, char a[], int degree[], int n) {// 为B数组分配内存tree* B (tree*)malloc(sizeof(tree) * n);int i 0;i…...

filter过滤器

package com.it.filter;import javax.servlet.*; import javax.servlet.annotation.WebFilter;import java.io.IOException;WebFilter(urlPatterns"/*") public class DemoFilter implements Filter {Override // 初始化的方法 只要调用一次public void init(Filte…...

MES物料的动态批次管理漫谈

在制造企业中&#xff0c;原辅材料占产品制造总成本基本在60%以上&#xff0c;特殊材料加工企业可能达到80%以上&#xff0c;按“2/8管理原则”管理好物料就基本做好制造企业的成本管理&#xff0c;这也许是很多企业向“数字化转型”的一个主要原因&#xff0c;希望借助数字信息…...

【爬虫逆向分析实战】某笔登录算法分析——本地替换分析法

前言 作者最近在做一个收集粉币的项目&#xff0c;可以用来干嘛这里就不展开了&#x1f601;&#xff0c;需要进行登录换算token从而达到监控收集的作用&#xff0c;手机抓包发现他是通过APP进行计算之后再请求接口的&#xff0c;通过官网分析可能要比APP逆向方便多&#xff0…...

vue3使用动态component

使用场景&#xff1a; 多个组件通过component标签挂载在同一个组件中&#xff0c;通过触发时间进行动态切换。vue3与vue2用法不一样&#xff0c;这里有坑&#xff01; 使用方法&#xff1a; 1.通过vue的defineAsyncComponent实现挂载组件 2.component中的is属性 父组件&am…...

单机游戏推荐:巨击大乱斗 GIGABASH 中文安装版

在泰坦之中称霸天下吧&#xff01;《GigaBash 巨击大乱斗》是一款多人战斗擂台游戏&#xff0c;有着受特摄片启发的巨型怪兽&#xff0c;具有传奇色彩的英雄&#xff0c;震天动地的特别攻击&#xff0c;以及可以完全摧毁的擂台场景。 ​游戏特点 怪物大解放 多达10个独特的角…...

计算机系统启动过程

计算机系统启动过程 阅读笔记&#xff1a; 《计算机体系结构基础&#xff08;第三版&#xff09;》-- 胡伟武 第7章&#xff1a;计算机系统启动过程分析 系统启动的整个过程中, 计算机系统在软件的控制下由无序到有序, 所有的组成部分都由程序管理, 按照程序的执行发挥各自的功…...

DedeCms后台文章列表文档id吗?或者快速定位id编辑文章

我们在建站时有的时候发现之前的文章有错误了&#xff0c;要进行修改&#xff0c;但又不知道文章名&#xff0c;只知道大概的文章id&#xff0c;那么可以搜索到DedeCms后台文章列表文档id吗&#xff1f;或者快速定位文章id方便修改&#xff1f; 第一种方法&#xff1a;复制下面…...

【开发问题解决方法记录】03.dian

登录提示 ERR-1002 在应用程序 "304" 中未找到项 "ROLE_ID" 的项 ID。 一开始找错方向了&#xff0c;以为是代码错误&#xff0c;但是后来在蒋老师的提醒下在共享组件-应用程序项 中发现设的项不是ROLE_ID而是ROLEID&#xff0c;怪不得找不到ORZ 解决方法…...

QT之QString

QT之QString 添加容器 点击栅格布局 添加容器&#xff0c;进行栅格布局 布局总结&#xff1a;每一个模块放在一个Group中&#xff0c;排放完之后&#xff0c;进行栅格布局。多个Group进行并排时&#xff0c;先将各个模块进行栅格布局&#xff0c;然后都选中进行垂直布…...

常见的几种计算机编码格式

前言&#xff1a; 计算机编码是指将字符、数字和符号等信息转换为计算机可识别的二进制数的过程&#xff0c;正因如此&#xff0c;计算机才能识别中英文等各类字符。计算机中有多种编码格式用于表示和存储文本、字符和数据&#xff0c;实际走到最后都是二进制&#xff0c;本质一…...

3D旋转tab图

上图 代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>3D旋转tab图</title><style>* {margin: 0;padding: 0;}body {height: 100vh;background: linear-gradient(to top, #29323c, #…...

openGL 三:矩阵和向量

1.使用glm数学库进行矩阵和向量的计算 2.位置坐标可以看做一个向量 3.向量的移动&#xff0c;缩放&#xff0c;旋转&#xff0c;都是可以通过和矩阵的计算得出 4.向量的缩放乘一个44的矩阵 5.注意事项&#xff08;有些版本的glm::mat4 不是默认构建一个单位44的矩阵&#xff09…...

Socket和Http的通讯原理,遇到攻击会受到哪些影响以及如何解决攻击问题。

德迅云安全-领先云安全服务与解决方案提供商 Socket和HTTP通信原理&#xff1a; Socket通信原理&#xff1a; Socket是一种应用程序编程接口&#xff08;API&#xff09;&#xff0c;用于在单个进程或多个进程之间进行通信。它提供了一种灵活的、异步的通信方式&#xff0c;使…...

【springboot】整合redis

1.前提条件:docker安装好了redis确定redis可以访问 可选软件: 2.测试代码 (1)redis依赖 org.springframework.boot spring-boot-starter-data-redis (2)配置redis &#xff08;3&#xff09; 注入 Resource StringRedisTemplate stringRedisTemplate; 对键进行操作 –o…...

回溯和分支算法

状态空间图 “图”——状态空间图 例子&#xff1a;农夫过河问题——“图”状态操作例子&#xff1a;n后问题、0-1背包问题、货郎问题(TSP) 用向量表示解&#xff0c;“图”由解向量扩张得到的解空间树。 ——三种图&#xff1a;n叉树、子集树、排序树 ​ 剪枝 不满住条件的…...

深入理解:指针变量的解引用 与 加法运算

前言 指针变量的解引用和加法运算是非常高频的考点&#xff0c;也是难点&#xff0c;因为对初学者的不友好&#xff0c;这就导致了各大考试都很喜欢在这里出题&#xff0c;通常会伴随着强制类型转换、二维数组、数组指针等一起考查大家对指针的理解。但是不要怕&#xff0c;也许…...

Docker 镜像构建的最佳做法

一、镜像分层 使用docker image history命令&#xff0c;可以看到用于在镜像中创建每个层的命令。 1、 使用docker image history命令查看创建的入门镜像中的层。 docker image history getting-started 您应该得到如下所示的输出&#xff1a; IMAGE CREATED…...

工作上Redis安装及配置

下载redis软件 第一步&#xff1a;解压压缩包 tar -zxvf redis-7.0.14.tar.gz 第二步&#xff1a;移动redis存放目录&#xff08;结合个人需求而定&#xff01;&#xff09; redis-7.0.14&#xff1a;解压后的文件路径 /usr/local&#xff1a;移动后的文件路径 mv redis-7.0.…...

XML Group端口详解

在XML数据映射过程中&#xff0c;经常需要对数据进行分组聚合操作。例如&#xff0c;当处理包含多个物料明细的XML文件时&#xff0c;可能需要将相同物料号的明细归为一组&#xff0c;或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码&#xff0c;增加了开…...

linux之kylin系统nginx的安装

一、nginx的作用 1.可做高性能的web服务器 直接处理静态资源&#xff08;HTML/CSS/图片等&#xff09;&#xff0c;响应速度远超传统服务器类似apache支持高并发连接 2.反向代理服务器 隐藏后端服务器IP地址&#xff0c;提高安全性 3.负载均衡服务器 支持多种策略分发流量…...

以下是对华为 HarmonyOS NETX 5属性动画(ArkTS)文档的结构化整理,通过层级标题、表格和代码块提升可读性:

一、属性动画概述NETX 作用&#xff1a;实现组件通用属性的渐变过渡效果&#xff0c;提升用户体验。支持属性&#xff1a;width、height、backgroundColor、opacity、scale、rotate、translate等。注意事项&#xff1a; 布局类属性&#xff08;如宽高&#xff09;变化时&#…...

Caliper 配置文件解析:config.yaml

Caliper 是一个区块链性能基准测试工具,用于评估不同区块链平台的性能。下面我将详细解释你提供的 fisco-bcos.json 文件结构,并说明它与 config.yaml 文件的关系。 fisco-bcos.json 文件解析 这个文件是针对 FISCO-BCOS 区块链网络的 Caliper 配置文件,主要包含以下几个部…...

Spring是如何解决Bean的循环依赖:三级缓存机制

1、什么是 Bean 的循环依赖 在 Spring框架中,Bean 的循环依赖是指多个 Bean 之间‌互相持有对方引用‌,形成闭环依赖关系的现象。 多个 Bean 的依赖关系构成环形链路,例如: 双向依赖:Bean A 依赖 Bean B,同时 Bean B 也依赖 Bean A(A↔B)。链条循环: Bean A → Bean…...

探索Selenium:自动化测试的神奇钥匙

目录 一、Selenium 是什么1.1 定义与概念1.2 发展历程1.3 功能概述 二、Selenium 工作原理剖析2.1 架构组成2.2 工作流程2.3 通信机制 三、Selenium 的优势3.1 跨浏览器与平台支持3.2 丰富的语言支持3.3 强大的社区支持 四、Selenium 的应用场景4.1 Web 应用自动化测试4.2 数据…...

论文阅读:LLM4Drive: A Survey of Large Language Models for Autonomous Driving

地址&#xff1a;LLM4Drive: A Survey of Large Language Models for Autonomous Driving 摘要翻译 自动驾驶技术作为推动交通和城市出行变革的催化剂&#xff0c;正从基于规则的系统向数据驱动策略转变。传统的模块化系统受限于级联模块间的累积误差和缺乏灵活性的预设规则。…...

十九、【用户管理与权限 - 篇一】后端基础:用户列表与角色模型的初步构建

【用户管理与权限 - 篇一】后端基础:用户列表与角色模型的初步构建 前言准备工作第一部分:回顾 Django 内置的 `User` 模型第二部分:设计并创建 `Role` 和 `UserProfile` 模型第三部分:创建 Serializers第四部分:创建 ViewSets第五部分:注册 API 路由第六部分:后端初步测…...

vue3 daterange正则踩坑

<el-form-item label"空置时间" prop"vacantTime"> <el-date-picker v-model"form.vacantTime" type"daterange" start-placeholder"开始日期" end-placeholder"结束日期" clearable :editable"fal…...

Modbus RTU与Modbus TCP详解指南

目录 1. Modbus协议基础 1.1 什么是Modbus? 1.2 Modbus协议历史 1.3 Modbus协议族 1.4 Modbus通信模型 🎭 主从架构 🔄 请求响应模式 2. Modbus RTU详解 2.1 RTU是什么? 2.2 RTU物理层 🔌 连接方式 ⚡ 通信参数 2.3 RTU数据帧格式 📦 帧结构详解 🔍…...