数据结构 - 线段树的运用

数据结构 - 线段树的运用

  • 前言
  • 一. 线段树的运用
    • 1.1 区间和 - 线段树节点的成员变量
    • 1.2 线段树的构建
    • 1.3 线段树的区间和查询
    • 1.4 线段树的区间和更新
    • 1.5 完整代码
  • 二. 线段树的动态扩建
    • 2.1 向下递推
    • 2.2 向上递推
    • 2.3 更新操作
    • 2.4 查询操作
    • 2.5 完整代码
  • 三. 线段树的使用案例
    • 3.1 定长线段树的区间和计算
    • 3.2 动态线段树的区间和计算

前言

想要精通算法和SQL的成长之路 - 系列导航

一. 线段树的运用

我们先来看下线段树的含义:

  1. 线段树(Segment Tree):是一种解决 区间查询问题 的数据结构。
  2. 它将一个区间划分成多个较小的子区间,并对每个子区间计算出一些有用的信息,通常是该子区间的统计值(例如最大值、最小值、总和等)
  3. 通过将这些子区间的信息进行合并,线段树可以高效地回答各种区间查询问题。

线段树通常用于解决以下类型的问题:

  • 区间最值查询:找到给定区间内的最大值、最小值等。
  • 区间更新:对给定区间内的元素进行更新。
  • 区间求和:计算给定区间内元素的总和。

那么线段树的构建过程,通常是一个 分治递归 的过程。线段树的时间复杂度为O(logN)

例如我们有个数组:[1,2,3,4,5],它的区间和用线段树表示就是:
在这里插入图片描述

1.1 区间和 - 线段树节点的成员变量

首先我们思考一下,从上图中,我们一个节点需要包括哪些数据:

  • 左右节点。
  • 当前节点的左右区间。
  • 当前区间和。

那么不难得出,代码结构如下:

public class SegmentTreeNode {public SegmentTreeNode left;public SegmentTreeNode right;public int start;public int end;public int val;public SegmentTreeNode(int start, int end) {this.start = start;this.end = end;}
}

1.2 线段树的构建

我们往往针对的是一个数组,去构建它的线段树:

public class SegmentTree {// 线段树的根节点private SegmentTreeNode root;public SegmentTree(int[] nums) {root = buildTree(nums, 0, nums.length - 1);}private SegmentTreeNode buildTree(int[] nums, int start, int end) {if (start > end) {return null;}// 构建当前节点SegmentTreeNode node = new SegmentTreeNode(start, end);// 如果区间长度为1,那么当前节点的sum就是区间内的值if (start == end) {node.val = nums[start];} else {// 开始递归构建左右子树int mid = (start + end) >> 1;node.left = buildTree(nums, start, mid);node.right = buildTree(nums, mid + 1, end);// 当前节点的sum等于左右子树的sum之和node.val = node.left.val + node.right.val;}return node;}
}

1.3 线段树的区间和查询

构建完之后,我们需要计算区间和了:传入指定的区间queryStartqueryEnd,返回 [queryStart,queryEnd] 区间内的总和。

public int query(int queryStart, int queryEnd) {return queryHelper(root, queryStart, queryEnd);
}private int queryHelper(SegmentTreeNode node, int queryStart, int queryEnd) {// 如果当前节点为空,或者当前节点的区间和不在查询区间内,那么返回0if (node == null || queryStart > node.end || queryStart < node.start) {return 0;}// 如果当前节点的区间完全在查询区间内,那么直接返回当前节点的sum。// 例如我们要查询[2,4] 的区间和,[2, 4] = [2, 2] + [3, 4]. 当前节点的区间是 [2,2] 或者 [3,4],所以直接返回当前节点的sum即可。if (node.start >= queryStart && node.end <= queryEnd) {return node.val;}// 否则,我们需要递归查询左右子树int mid = (node.start + node.end) >> 1;// 注意这里的Math.min和Math.max,因为我们的查询区间是[queryStart, queryEnd],而当前节点的区间是[node.start, node.end],所以我们需要取交集int leftSum = queryHelper(node.left, queryStart, Math.min(queryEnd, mid));int rightSum = queryHelper(node.right, Math.max(queryStart, mid + 1), queryEnd);return leftSum
}

1.4 线段树的区间和更新

上面的区间和计算,往往是基于数组的值不变的情况下进行的。那么假若数组中的某个元素被更新了,那么我们的区间和就不正确了,跟这个元素有关的所有链路,都要被更新,因此我们还需要准备一个更新的函数,它和查询非常类似,同样是递归操作。

public void update(int index, int newVal) {updateHelper(root, index, newVal);
}private void updateHelper(SegmentTreeNode node, int index, int newVal) {if (node == null || index < node.start || index > node.end) {return;}// 如果当前节点的区间就是要更新的区间,那么直接更新当前节点的sum即可if (node.start == node.end) {node.val = newVal;return;}// 否则,我们需要递归更新左右子树int mid = (node.start + node.end) >> 1;if (index <= mid) {updateHelper(node.left, index, newVal);} else {updateHelper(node.right, index, newVal);}// 更新完左右子树之后,需要更新当前节点的sumnode.val = node.left.val + node.right.val;
}

1.5 完整代码

public class SegmentTree {// 线段树的根节点private SegmentTreeNode root;public SegmentTree(int[] nums) {root = buildTree(nums, 0, nums.length - 1);}private SegmentTreeNode buildTree(int[] nums, int start, int end) {if (start > end) {return null;}// 构建当前节点SegmentTreeNode node = new SegmentTreeNode(start, end);// 如果区间长度为1,那么当前节点的sum就是区间内的值if (start == end) {node.val = nums[start];} else {// 开始递归构建左右子树int mid = (start + end) >> 1;node.left = buildTree(nums, start, mid);node.right = buildTree(nums, mid + 1, end);// 当前节点的sum等于左右子树的sum之和node.val = node.left.val + node.right.val;}return node;}public int query(int queryStart, int queryEnd) {return queryHelper(root, queryStart, queryEnd);}private int queryHelper(SegmentTreeNode node, int queryStart, int queryEnd) {// 如果当前节点为空,或者当前节点的区间和不在查询区间内,那么返回0if (node == null || queryStart > node.end || queryStart < node.start) {return 0;}// 如果当前节点的区间完全在查询区间内,那么直接返回当前节点的sum。// 例如我们要查询[2,4] 的区间和,[2, 4] = [2, 2] + [3, 4]. 当前节点的区间是 [2,2] 或者 [3,4],所以直接返回当前节点的sum即可。if (node.start >= queryStart && node.end <= queryEnd) {return node.val;}// 否则,我们需要递归查询左右子树int mid = (node.start + node.end) >> 1;// 注意这里的Math.min和Math.max,因为我们的查询区间是[queryStart, queryEnd],而当前节点的区间是[node.start, node.end],所以我们需要取交集int leftSum = queryHelper(node.left, queryStart, Math.min(queryEnd, mid));int rightSum = queryHelper(node.right, Math.max(queryStart, mid + 1), queryEnd);return leftSum + rightSum;}public void update(int index, int newVal) {updateHelper(root, index, newVal);}private void updateHelper(SegmentTreeNode node, int index, int newVal) {if (node == null || index < node.start || index > node.end) {return;}// 如果当前节点的区间就是要更新的区间,那么直接更新当前节点的sum即可if (node.start == node.end) {node.val = newVal;return;}// 否则,我们需要递归更新左右子树int mid = (node.start + node.end) >> 1;if (index <= mid) {updateHelper(node.left, index, newVal);} else {updateHelper(node.right, index, newVal);}// 更新完左右子树之后,需要更新当前节点的sumnode.val = node.left.val + node.right.val;}
}

二. 线段树的动态扩建

第一节的代码,它有一个前提:

  • 我们已知数组的长度和各个元素的值。

那如果我们不知道数组包含的元素个数以及各个元素值的时候,怎么去建立这颗线段树?如果可以,它必定是在不断地更新的基础上去动态扩建的。

还是以求区间和为例,我们试想一下,在动态扩建的过程中,每个新节点需要向其他节点传递什么信息?

  • 本次应该新增的值,我们用一个变量add来标识。
  • 同时,由于数组的范围不再是固定,因此数据结构中应该剔除startend属性。
public static class SegmentTreeNode {public SegmentTreeNode left;public SegmentTreeNode right;public int add;public int val;public SegmentTreeNode() {}
}

2.1 向下递推

我们定义一个pushDown函数,它的功能有这么几个:

  • 动态创建左右子节点。
  • 给左右子节点传递add值,以及计算他们的区间和val
  • 若传递结束,那么要将当前节点的add值置为0。

考虑到add值的传递,以及树中叶子节点的性质,除了当前节点node我们还需要两个变量来标识当前节点的左孩子数量和右孩子数量。

代码如下:

private void pushDown(SegmentTreeNode node, int leftNum, int rightNum) {// 动态开点if (node.left == null) {node.left = new SegmentTreeNode();}if (node.right == null) {node.right = new SegmentTreeNode();}// 如果当前节点的add值为0,那么我们不需要更新子节点的add值if (node.val == 0) {return;}// 否则,更新左右子节点的add值node.left.add += node.add * leftNum;node.right.add += node.add * rightNum;// 更新左右子节点的add值node.left.val += node.add;node.right.val += node.add;// 更新当前节点的add值node.add = 0;
}

2.2 向上递推

我们定义一个函数pushUp,主要用来计算当前节点的区间值:

  • 当前节点的区间值 = 左区间和 + 右区间和。
private void pushUp(SegmentTreeNode node) {node.val = node.left.val + node.right.val;
}

2.3 更新操作

/**
* @param node  线段树的根节点* @param start 线段树的起始位置* @param end   线段树的结束位置* @param left  查询区间的左边界* @param right 查询区间的右边界* @param addValue   比原本的值多加的值*/
public void update(SegmentTreeNode node, int start, int end, int left, int right, int addValue) {// 如果线段树的区间完全在查询区间内,那么直接更新当前节点的add值即可if (start >= left && end <= right) {// 该区间内,所有叶子节点都要加上valnode.val += (end - start + 1) * addValue;// 该区间内,所有非叶子节点都要加上val,传递给后面的新节点node.add += addValue;return;}// 如果不在查询区间内,那么我们需要递归更新左右子树int mid = (start + end) >> 1;// 向下传递标记pushDown(node, mid - start + 1, end - mid);if (left <= mid) {update(node.left, start, mid, left, right, addValue);}// [mid + 1, end] 和 [l, r] 可能有交集,遍历右孩子区间if (right > mid) {update(node.right, mid + 1, end, left, right, addValue);}// 计算当前节点的val值pushUp(node);
}

2.4 查询操作

public int query(SegmentTreeNode node, int start, int end, int left, int right) {if (left <= start && end <= right) {return node.val;}// 把当前区间 [start, end] 均分得到左右孩子的区间范围int mid = (start + end) >> 1, ans = 0;// 下推标记pushDown(node, mid - start + 1, end - mid);// [start, mid] 和 [l, r] 可能有交集,遍历左孩子区间if (left <= mid) {ans += query(node.left, start, mid, left, right);}// [mid + 1, end] 和 [l, r] 可能有交集,遍历右孩子区间if (right > mid) {ans += query(node.right, mid + 1, end, left, right);}return ans;
}

2.5 完整代码

public class SegmentTreeDynamic {public static class SegmentTreeNode {public SegmentTreeNode left;public SegmentTreeNode right;public int add;public int val;public SegmentTreeNode() {}}/*** @param node     线段树的根节点* @param leftNum  左节点的叶子数量* @param rightNum 右节点的叶子数量*/private void pushDown(SegmentTreeNode node, int leftNum, int rightNum) {// 动态开点if (node.left == null) {node.left = new SegmentTreeNode();}if (node.right == null) {node.right = new SegmentTreeNode();}// 如果当前节点的add值为0,那么我们不需要更新子节点的add值if (node.val == 0) {return;}// 否则,更新左右子节点的add值node.left.add += node.add * leftNum;node.right.add += node.add * rightNum;// 更新左右子节点的add值node.left.val += node.add;node.right.val += node.add;// 更新当前节点的add值node.add = 0;}private void pushUp(SegmentTreeNode node) {node.val = node.left.val + node.right.val;}/*** @param node  线段树的根节点* @param start 线段树的起始位置* @param end   线段树的结束位置* @param left  查询区间的左边界* @param right 查询区间的右边界* @param addValue   比原本的值多加的值*/public void update(SegmentTreeNode node, int start, int end, int left, int right, int addValue) {// 如果线段树的区间完全在查询区间内,那么直接更新当前节点的add值即可if (start >= left && end <= right) {// 该区间内,所有叶子节点都要加上valnode.val += (end - start + 1) * addValue;// 该区间内,所有非叶子节点都要加上val,传递给后面的新节点node.add += addValue;return;}// 如果不在查询区间内,那么我们需要递归更新左右子树int mid = (start + end) >> 1;// 向下传递标记pushDown(node, mid - start + 1, end - mid);if (left <= mid) {update(node.left, start, mid, left, right, addValue);}// [mid + 1, end] 和 [l, r] 可能有交集,遍历右孩子区间if (right > mid) {update(node.right, mid + 1, end, left, right, addValue);}// 计算当前节点的val值pushUp(node);}public int query(SegmentTreeNode node, int start, int end, int left, int right) {if (left <= start && end <= right) {return node.val;}// 把当前区间 [start, end] 均分得到左右孩子的区间范围int mid = (start + end) >> 1, ans = 0;// 下推标记pushDown(node, mid - start + 1, end - mid);// [start, mid] 和 [l, r] 可能有交集,遍历左孩子区间if (left <= mid) {ans += query(node.left, start, mid, left, right);}// [mid + 1, end] 和 [l, r] 可能有交集,遍历右孩子区间if (right > mid) {ans += query(node.right, mid + 1, end, left, right);}return ans;}
}

三. 线段树的使用案例

Demo如下:数组:[1, 3, 5, 7, 9, 11]

  1. 求得区间[1,4]的区间和。
  2. 如果索引为2的地方的值更新为19,求得区间[1,4]的区间和。

3.1 定长线段树的区间和计算

public static void main(String[] args) {int[] nums = {1, 3, 5, 7, 9, 11};SegmentTree segmentTree = new SegmentTree(nums);int sum = segmentTree.query(1, 4); // 查询区间[1, 4]的和 3+5+7+9=24System.out.println(sum); // 输出:24segmentTree.update(2, 19); // 将索引2处的值更新为19sum = segmentTree.query(1, 4); // 再次查询区间[1, 4]的和System.out.println(sum); // 输出:38
}

结果如下:
在这里插入图片描述

3.2 动态线段树的区间和计算

public static void main(String[] args) {int[] nums = {1, 3, 5, 7, 9, 11};SegmentTreeDynamic segmentTree = new SegmentTreeDynamic();SegmentTreeDynamic.SegmentTreeNode root = new SegmentTreeDynamic.SegmentTreeNode();int n = nums.length - 1;for (int i = 0; i < nums.length; i++) {segmentTree.update(root, 0, n, i, i, nums[i]);}System.out.println(segmentTree.query(root, 0, n, 1, 4));segmentTree.update(root, 0, n, 2, 2, 14);// 在原本值的基础上再多14,相当于定长计算中的5 + 14 = 19System.out.println(segmentTree.query(root, 0, n, 1, 4));
}

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

Unity之NetCode多人网络游戏联机对战教程(3)--NetworkObject组件讲解

文章目录 NetworkObjectAlways Replicate As RootSynchronization TransformActive Scene SynchronizationScene Migration SynchronizationSpawn With ObserversDont Destroy With OwnerAuto Object Parent Sync 后话 NetworkObject 为了复制任何Netcode感知属性或发送/接收R…

Python大数据之pandas快速入门(一)

文章目录 pandas快速入门学习目标1. DataFrame 和 Series 简介2. 加载数据集(csv和tsv)2.1 csv和tsv文件格式简介2.2 加载数据集(tsv和csv) pandas快速入门 学习目标 能够知道 DataFrame 和 Series 数据结构能够加载 csv 和 tsv 数据集能够区分 DataFrame 的行列标签和行列位…

FPGA project : uart232_ram_vga

重点学习&#xff1a; 本实验重点学习了双口ram解决多bit跨时钟域同步处理的问题。 其实signal port ram&#xff0c;它的输入口和输出口分别用不同的时钟&#xff0c;也可以解决这个问题。 让我意识到的比较重要的事情&#xff1a; 1&#xff0c;代码设计中&#xff0c;一…

经典题记录 字符串相加/相乘

1. LeetCode 415 字符串相加 代码一&#xff1a;代码简短&#xff0c;但需要借助额外的一个string来保存结果&#xff0c;更占用内存。 class Solution { public:string addStrings(string num1, string num2) {string ans"";int size1num1.size();int size2num2.si…

Qt_C++读写NFC标签Ntag支持windows国产linux操作系统

本示例使用的发卡器&#xff1a;Android Linux RFID读写器NFC发卡器WEB可编程NDEF文本/智能海报/-淘宝网 (taobao.com) ntag2标签存储结构说明 #include "mainwindow.h" #include "./ui_mainwindow.h" #include <QDebug> #include "QLibrary&…

Django REST Farmowork初探

1.简介 Django REST framework &#xff08;简称&#xff1a;DRF&#xff09;是一个强大而灵活的 Web API 工具。 遵循RESTFullAPI风格&#xff0c;功能完善&#xff0c;可快速开发API平台。 官网文档&#xff1a;https://www.django-rest-framework.org 2. framwork的安装 …

界面组件DevExpress WPF v23.2新功能预览 - 更轻量级的主题

本文主要描述了DevExpress WPF即将在几个月之后发布的v23.2中包含的新功能&#xff0c;持续关注我们获取更多最新资讯哦~ P.S&#xff1a;DevExpress WPF拥有120个控件和库&#xff0c;将帮助您交付满足甚至超出企业需求的高性能业务应用程序。通过DevExpress WPF能创建有着强…

2023蓝帽杯半决赛取证复现

1.检材数据开始提取是今年什么时候&#xff1f;&#xff08;答案格式&#xff1a;04-12 13:26&#xff09; 09-11 17:21 这题做错了 其实当时盘古石手机取证里面就有的&#xff0c;想多了去看了日志文件 是真的有点歧义&#xff0c;20分就开始提取任务了 2.嫌疑人手机SD卡存…

精通git,没用过git cherry-pick?

前言 git cherry-pick是git中非常有用的一个命令&#xff0c;cherry是樱桃的意思&#xff0c;cherry-pick就是挑樱桃&#xff0c;从一堆樱桃中挑选自己喜欢的樱桃&#xff0c;在git中就是多次commit中挑选一个或者几个commit出来&#xff0c;也可以理解为把特定的commit复制到…

【实验记录】AGW | Visible-Infrared Re-ID

【RT】Visible Thermal Re-IDDeep Learning for Person Re-identification: A Survey and Outlook中提出了一个针对单/跨模态行人重识别的baseline&#xff1a;AGW 做过两次&#xff0c;在测试阶段有问题&#xff0c;现在再重做一次&#x1f914;Code RTX3090 修改数据集路…

手机相机系统介绍

目录 一张照片是如何生成的? 相机的成像原理 相机硬件 颜色四要素 相机硬件三大块 模组结构 镜头 镜头光路 镜头常见参数 镜头-FOV&EFL 镜头-焦距 镜头-光圈 图像传感器 图像传感器-像素-底 RGB排布 图像传感器-Pattern & PDAF Sensor CMOS sensor …

计算机类软件方向适合参加的比赛

前言 博主是一名计算机专业的大三学生&#xff0c;在校时候参加了很多比赛和训练营&#xff0c;现在给大家博主参加过的几个的比赛&#xff0c;希望能给大一大二的学生提供一点建议。 正文 最近也有比赛的&#xff0c;我会从时间线上来给大家推荐一些比赛&#xff0c;并且给…

雷柏mv20鼠标使用体验

用了1年多&#xff0c;第一次用竖着的鼠标&#xff0c;现在已经很习惯了&#xff0c;感觉还不错。说说使用感受&#xff1a; 1、 仍然是长时间使用鼠标&#xff0c;但是很少出现手腕痛的情况&#xff0c;确实是有一定效果的。 2、使用场景是有限制的&#xff0c;我是配合笔记…

解决kali beef启动失败问题及实战

文章目录 一、解决方法二、靶场实战应用1.首先打开dvwa这个靶场&#xff0c;设置难度为low2.打开xss-stored3.准备payload4.提交payload5.利用 一、解决方法 首先需卸载 ruby apt remove ruby 卸载 beef apt remove beef-xss 重新安装ruby apt-get install ruby apt-get insta…

安卓修改ROM 修改固件中的一些基本常识 自己做rom注意事项

修改rom 制作rom 解包rom的一些问题解析 安卓系列机型如何内置app 如何选择so文件内置 修改设置里 添加选项 添加文字 修改图标 修改版本号等等 实例解析 最近有几个粉丝对修改rom有兴趣。今天主要给这些友友提供一些自己初学修改rom的一些建议和思路&#xff0c;可以供大家…

uni-app:多方法实现两个view在同一行展示

效果 方法一&#xff1a;flex 布局 使用 display: flex 后&#xff0c;默认的 flex-direction 值就是 row&#xff0c;即水平排列。 <template><view class"container"><view class"left-view">123</view><view class"r…

SpringBoot的excel模板导出

Word的模板导出(参考&#xff1a;https://easyexcel.opensource.alibaba.com/docs/current/quickstart/fill) 创建有两个sheet的excel文件模板 将模板文件放入resource\templates/doc下使用 public void exportUavInfoExcel(HttpServletResponse response, CaseExportRPO cas…

GB28181协议-SDP详解

SDP协议 SDP全称是Session Description Protocol&#xff0c;翻译过来就是描述会话的协议。主要用于两个会话实体之间的媒体协商。 SDP描述由许多文本行组成&#xff0c;文本行的格式为<类型><值>&#xff0c;表示为keyvalue; SIP负责建立和释放会话&#xff0c…

架构问题:技术选型

1. 几款数据库特性及如何选型 1.MySQL&#xff1a;一种常用的开源关系型数据库管理系统&#xff0c;可以快速访问大量数据&#xff0c;并支持多用户同时访问。其最大的优点在于成本低&#xff0c;易于安装和配置&#xff0c;因此被广泛应用于各种中小型企业和网站。支持读写分离…

C++ Primer----1.5类简介 章节练习

头文件 Sales_item.h #ifndef SALESITEM_H #define SALESITEM_H #include <iostream> #include <string>class Sales_item{ public:Sales_item(const std::string &book):isbn(book),units_sold(0),revenue(0.0){}Sales_item(std::istream &is){ is >&…