【数据结构】线段树

目录

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

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

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

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

相关文章

linux 用户级别修改文件句柄数不生效

修改/etc/security/limits.conf 句柄数后发现无效需要执行以下操作。 1. vi /etc/ssh/sshd_config 2. StrictModes no/yes 3. UsePAM yes 4. service sshd restart 5. /etc/systemd/user.conf 新增DefaultLimitNOFILE65535 6. /etc/systemd/system.conf 新增DefaultLimitN…

logback-spring.xml的内容格式

目录 一、logback-spring.xml 二、Logback 中的三种日志文件类型 一、logback-spring.xml <?xml version"1.0" encoding"UTF-8"?> <configuration scan"true" scanPeriod"10 seconds" ><!-- <statusListener…

C#使用WebSocket进行链接

C#使用WebSocket进行网络链接&#xff0c;和服务端搭建一个长连接进行通信。 有两种方式&#xff1a;串口方式&#xff08;SerialPort&#xff09;和网口方式&#xff08;Socket 包括&#xff1a;TcpClient和UdpClient&#xff09; 准备&#xff1a; 1.C#使用WebSocket的一种…

《100 Java Mistakes and How to Avoid Them》笔记 1

这几日在阅读 Manning 出版社的 《100 Java Mistakes and How to Avoid Them》, 其中列举的确实是一些容易带入到代码中的错误&#xff0c;不少还是通过代码 Review 或单元测试很难发现的问题。也有些看似很弱智&#xff0c;却可能是隐匿许久的定时炸弹&#xff0c;只等某一特定…

pandas教程:Advanced GroupBy Use 高级GroupBy用法

文章目录 12.2 Advanced GroupBy Use&#xff08;高级GroupBy用法&#xff09;1 Group Transforms and “Unwrapped” GroupBys&#xff08;组变换和无包装的GroupBy&#xff09;2 Grouped Time Resampling&#xff08;分组时间重采样&#xff09; 12.2 Advanced GroupBy Use&a…

图形编辑器开发:快捷键的管理

大家好&#xff0c;我是前端西瓜哥。 快捷键操作在图形编辑器中是很高频的操作&#xff0c;能让用户快速高效地执行特定命令&#xff0c;今天讲讲图形编辑器如果管理快捷键。 编辑器 github 地址&#xff1a; https://github.com/F-star/suika 线上体验&#xff1a; https://b…

计算机网络:网络层

0 本节主要内容 问题描述 解决思路 1 问题描述 两大问题&#xff08;重点&#xff0c;也是难点&#xff09;&#xff1a; 地址管理&#xff1b;路由选择。 1.1 子问题1&#xff1a;地址管理 网络上的这些主机和节点都需要使用一种规则来区分&#xff0c;就相当于是一种身…

说说过量 tcp pure ack 的利弊

tcp 的 ack 实在太多了&#xff0c;如果互联网上 80% 报文是 tcp&#xff0c;那么其中 1/3 的报文都是 ack&#xff0c;此前写过几篇短文&#xff0c;比如 丢弃一些 pure ack 和 注入或利用 pure ack。 简单说&#xff0c;tcp 依靠 ack 提供 self-clock&#xff0c;发送 data …

ARM异常模型

快速链接: . 👉👉👉 个人博客笔记导读目录(全部) 👈👈👈 付费专栏-付费课程 【购买须知】:【精选】ARMv8/ARMv9架构入门到精通-[目录] 👈👈👈 — 适合小白入门【目录】ARMv8/ARMv9架构高级进阶-[目录]👈👈👈 — 高级进阶、小白勿买【加群】ARM/TEE…

Docker和Kubernetes:区别与优势对比

在现代软件开发和部署中&#xff0c;Docker和Kubernetes是两个备受关注的技术。本文将对Docker和Kubernetes进行比较&#xff0c;探讨它们的区别和各自的优势。 引言 在过去的几年中&#xff0c;容器技术得到了迅速的发展&#xff0c;并且在现代软件交付和部署中扮演着越来越重…

LVS-DR实验

实验前准备 DR服务器&#xff1a;192.168.188.11 192.168.188.15 NFS服务器&#xff1a;192.168.188.14 Web服务器1&#xff1a;192.168.188.12 Web服务器2&#xff1a;192.168.188.13 Vip&#xff1a;192.168.188.188 客户端&#xff1a;192.168.188.200 配置负载均衡调度…

leetCode 77.组合 + 回溯算法 (bactracking) + 剪枝 + 图解 + 笔记

77. 组合 - 力扣&#xff08;LeetCode&#xff09; 给定两个整数 n 和 k&#xff0c;返回范围 [1, n] 中所有可能的 k 个数的组合。可以按 任何顺序 返回答案。 示例 1&#xff1a; 输入&#xff1a;n 4, k 2 输出&#xff1a; [[2,4],[3,4],[2,3],[1,2],[1,3],[1,4], ] …

Unity EventSystem的一些理解和使用

Unity的EventSystem是用于处理用户输入和交互的系统。它是Unity UI系统的核心组件之一&#xff0c;可以用于捕捉和分发各种事件&#xff0c;例如点击、拖拽、按键、射线等。 常用的属性和方法有以下这些&#xff1a; 属性&#xff1a; current: 获取当前的EventSystem实例。…

HuggingFace学习笔记--datasets的使用

目录 1--datasets的使用 1-1--加载数据集 1-2--查看数据集 1-3--打乱和排序数据集 1-4--选择和筛选数据集 1-5--划分数据集 1-6--修改数据集 1-7--导出数据集 1--datasets的使用 1-1--加载数据集 代码&#xff1a; from datasets import load_dataset, load_from_di…

C#中的TaskFactory

在C#中&#xff0c;TaskFactory是一个用于创建异步任务的类。它提供了一种方便的方式来创建Task对象&#xff0c;这些对象表示可以异步执行的操作。TaskFactory类位于System.Threading.Tasks命名空间下。 要使用TaskFactory&#xff0c;您需要导入相应的命名空间&#xff1a; …

uni-app+vue3 封装全局函数(详细完整的方法)

在uni-app和vue3中&#xff0c;我们可以封装全局函数来复用代码。以下是详细的步骤&#xff1a; 首先&#xff0c;我们需要在main.js文件中引入我们封装的全局函数。这样我们就可以在整个项目中使用这些函数。 import globalFunctions from ./globalFunctionsVue.prototype.$g…

Vue简易的车牌输入键盘,可以根据需要修改

效果图如下&#xff1a; 代码如下&#xff1a; <template><div><div class"carNoBoxInput"><div style"padding: 6px;border: 2px solid #fff;border-radius: 6px;margin: 6px 3px 6px 6px;"><input class"inputBox"…

小航助学题库蓝桥杯题库stem选拔赛(23年8月)(含题库教师学生账号)

需要在线模拟训练的题库账号请点击 小航助学编程在线模拟试卷系统&#xff08;含题库答题软件账号&#xff09;_程序猿下山的博客-CSDN博客 需要在线模拟训练的题库账号请点击 小航助学编程在线模拟试卷系统&#xff08;含题库答题软件账号&#xff09;_程序猿下山的博客-CSD…

利用ambari搭建Hbase高可用

初始环境&#xff1a; 节点名称服务名ambari-hadoop1ambari-hadoop2region serverambari-hadoop3hmater、 region server 计划为ambari-hadoop1添加hmaster&#xff0c;以避免hmaster的单点故障、 step1&#xff1a;添加备用Hmaster step2&#xff1a;选择ambari-hadoop1作为…

【一周AI简讯】OpenAI奥特曼王者归来,马斯克AI模型Grok下周开放测试,ChatGPT语音对话功能向所有用户免费开放

OpenAI奥特曼王者归来&#xff0c;董事会改组 终于&#xff0c;经历大约5天的极限拉扯&#xff0c;年底AI界吃瓜大戏落下帷幕&#xff0c;奥特曼确认回归。 ChatGPT语音对话功能向所有用户免费开放 ChatGPT 语音输入最初于 9 月份推出&#xff0c;标题是“ChatGPT 现在可以看…