基于C#实现线段树

一、线段树

线段树又称"区间树”,在每个节点上保存一个区间,当然区间的划分采用折半的思想,叶子节点只保存一个值,也叫单元节点,所以最终的构造就是一个平衡的二叉树,拥有 CURD 的 O(lgN)的时间。
image.png
从图中我们可以清楚的看到[0-10]被划分成线段的在树中的分布情况,针对区间[0-N],最多有 2N 个节点,由于是平衡二叉树的形式也可以像堆那样用数组来玩,不过更加耗费空间,为最多 4N 个节点,在针对 RMQ 的问题上,我们常常在每个节点上增加一些 sum,max,min 等变量来记录求得的累加值,当然你可以理解成动态规划的思想,由于拥有 logN 的时间,所以在 RMQ 问题上比数组更加优美。

二、代码

1、在节点中定义一些附加值,方便我们处理 RMQ 问题。

 #region 线段树的节点/// <summary>/// 线段树的节点/// </summary>public class Node{/// <summary>/// 区间左端点/// </summary>public int left;/// <summary>/// 区间右端点/// </summary>public int right;/// <summary>/// 左孩子/// </summary>public Node leftchild;/// <summary>/// 右孩子/// </summary>public Node rightchild;/// <summary>/// 节点的sum值/// </summary>public int Sum;/// <summary>/// 节点的Min值/// </summary>public int Min;/// <summary>/// 节点的Max值/// </summary>public int Max;}#endregion

2、构建(Build)
前面我也说了,构建有两种方法,数组的形式或者链的形式,各有特点,我就采用后者,时间为 O(N)。

  #region 根据数组构建“线段树"/// <summary>/// 根据数组构建“线段树"/// </summary>/// <param name="length"></param>public Node Build(int[] nums){this.nums = nums;return Build(nodeTree, 0, nums.Length - 1);}#endregion#region 根据数组构建“线段树"/// <summary>/// 根据数组构建“线段树"/// </summary>/// <param name="left"></param>/// <param name="right"></param>public Node Build(Node node, int left, int right){//说明已经到根了,当前当前节点的max,sum,min值(回溯时统计上一层节点区间的值)if (left == right){return new Node{left = left,right = right,Max = nums[left],Min = nums[left],Sum = nums[left]};}if (node == null)node = new Node();node.left = left;node.right = right;node.leftchild = Build(node.leftchild, left, (left + right) / 2);node.rightchild = Build(node.rightchild, (left + right) / 2 + 1, right);//统计左右子树的值(min,max,sum)node.Min = Math.Min(node.leftchild.Min, node.rightchild.Min);node.Max = Math.Max(node.leftchild.Max, node.rightchild.Max);node.Sum = node.leftchild.Sum + node.rightchild.Sum;return node;}#endregion

3、区间查询
在线段树中,区间查询还是有点小麻烦的,存在三种情况。
① 完全包含:也就是节点的线段范围完全在查询区间的范围内,这说明我们要么到了“单元节点",要么到了一个子区间,这种情况就是我找到了查询区间的某一个子区间,直接累积该区间值就可以了。
② 左交集: 这种情况我们需要到左子树去遍历。
③ 右交集: 这种情况我们需要到右子树去遍历。
比如说:我要查询 Sum[4-8]的值,最终会成为:Sum 总=Sum[4-4]+Sum[5-5]+Sum[6-8],时间为 log(N)。

 #region 区间查询/// <summary>/// 区间查询(分解)/// </summary>/// <returns></returns>public int Query(int left, int right){int sum = 0;Query(nodeTree, left, right, ref sum);return sum;}/// <summary>/// 区间查询/// </summary>/// <param name="left">查询左边界</param>/// <param name="right">查询右边界</param>/// <param name="node">查询的节点</param>/// <returns></returns>public void Query(Node node, int left, int right, ref int sum){//说明当前节点完全包含在查询范围内,两点:要么是单元节点,要么是子区间if (left <= node.left && right >= node.right){//获取当前节点的sum值sum += node.Sum;return;}else{//如果当前的left和right 和node的left和right无交集,此时可返回if (node.left > right || node.right < left)return;//找到中间线var middle = (node.left + node.right) / 2;//左孩子有交集if (left <= middle){Query(node.leftchild, left, right, ref sum);}//右孩子有交集if (right >= middle){Query(node.rightchild, left, right, ref sum);}}}#endregion

4、更新操作
这个操作跟树状数组中的更新操作一样,当递归的找到待修改的节点后,改完其值然后在当前节点一路回溯,并且在回溯的过程中一路修改父节点的附加值直到根节点,至此我们的操作就完成了,复杂度同样为 logN。

 #region 更新操作/// <summary>/// 更新操作/// </summary>/// <param name="index"></param>/// <param name="key"></param>public void Update(int index, int key){Update(nodeTree, index, key);}/// <summary>/// 更新操作/// </summary>/// <param name="index"></param>/// <param name="key"></param>public void Update(Node node, int index, int key){if (node == null)return;//取中间值var middle = (node.left + node.right) / 2;//遍历左子树if (index >= node.left && index <= middle)Update(node.leftchild, index, key);//遍历右子树if (index <= node.right && index >= middle + 1)Update(node.rightchild, index, key);//在回溯的路上一路更改,复杂度为lgNif (index >= node.left && index <= node.right){//说明找到了节点if (node.left == node.right){nums[index] = key;node.Sum = node.Max = node.Min = key;}else{//回溯时统计左右子树的值(min,max,sum)node.Min = Math.Min(node.leftchild.Min, node.rightchild.Min);node.Max = Math.Max(node.leftchild.Max, node.rightchild.Max);node.Sum = node.leftchild.Sum + node.rightchild.Sum;}}}#endregion

最后我们做个例子,在 2000000 的数组空间中,寻找 200-3000 区间段的 sum 值,看看他的表现如何。

 using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Diagnostics;using System.Threading;using System.IO;namespace ConsoleApplication2{public class Program{public static void Main(){int[] nums = new int[200 * 10000];for (int i = 0; i < 10000 * 200; i++){nums[i] = i;}Tree tree = new Tree();//将当前数组构建成 “线段树”tree.Build(nums);var watch = Stopwatch.StartNew();var sum = tree.Query(200, 3000);watch.Stop();Console.WriteLine("耗费时间:{0}ms,  当前数组有:{1}个数字, 求出Sum=:{2}", watch.ElapsedMilliseconds, nums.Length, sum);Console.Read();}}public class Tree{#region 线段树的节点/// <summary>/// 线段树的节点/// </summary>public class Node{/// <summary>/// 区间左端点/// </summary>public int left;/// <summary>/// 区间右端点/// </summary>public int right;/// <summary>/// 左孩子/// </summary>public Node leftchild;/// <summary>/// 右孩子/// </summary>public Node rightchild;/// <summary>/// 节点的sum值/// </summary>public int Sum;/// <summary>/// 节点的Min值/// </summary>public int Min;/// <summary>/// 节点的Max值/// </summary>public int Max;}#endregionNode nodeTree = new Node();int[] nums;#region 根据数组构建“线段树"/// <summary>/// 根据数组构建“线段树"/// </summary>/// <param name="length"></param>public Node Build(int[] nums){this.nums = nums;return Build(nodeTree, 0, nums.Length - 1);}#endregion#region 根据数组构建“线段树"/// <summary>/// 根据数组构建“线段树"/// </summary>/// <param name="left"></param>/// <param name="right"></param>public Node Build(Node node, int left, int right){//说明已经到根了,当前当前节点的max,sum,min值(回溯时统计上一层节点区间的值)if (left == right){return new Node{left = left,right = right,Max = nums[left],Min = nums[left],Sum = nums[left]};}if (node == null)node = new Node();node.left = left;node.right = right;node.leftchild = Build(node.leftchild, left, (left + right) / 2);node.rightchild = Build(node.rightchild, (left + right) / 2 + 1, right);//统计左右子树的值(min,max,sum)node.Min = Math.Min(node.leftchild.Min, node.rightchild.Min);node.Max = Math.Max(node.leftchild.Max, node.rightchild.Max);node.Sum = node.leftchild.Sum + node.rightchild.Sum;return node;}#endregion#region 区间查询/// <summary>/// 区间查询(分解)/// </summary>/// <returns></returns>public int Query(int left, int right){int sum = 0;Query(nodeTree, left, right, ref sum);return sum;}/// <summary>/// 区间查询/// </summary>/// <param name="left">查询左边界</param>/// <param name="right">查询右边界</param>/// <param name="node">查询的节点</param>/// <returns></returns>public void Query(Node node, int left, int right, ref int sum){//说明当前节点完全包含在查询范围内,两点:要么是单元节点,要么是子区间if (left <= node.left && right >= node.right){//获取当前节点的sum值sum += node.Sum;return;}else{//如果当前的left和right 和node的left和right无交集,此时可返回if (node.left > right || node.right < left)return;//找到中间线var middle = (node.left + node.right) / 2;//左孩子有交集if (left <= middle){Query(node.leftchild, left, right, ref sum);}//右孩子有交集if (right >= middle){Query(node.rightchild, left, right, ref sum);}}}#endregion#region 更新操作/// <summary>/// 更新操作/// </summary>/// <param name="index"></param>/// <param name="key"></param>public void Update(int index, int key){Update(nodeTree, index, key);}/// <summary>/// 更新操作/// </summary>/// <param name="index"></param>/// <param name="key"></param>public void Update(Node node, int index, int key){if (node == null)return;//取中间值var middle = (node.left + node.right) / 2;//遍历左子树if (index >= node.left && index <= middle)Update(node.leftchild, index, key);//遍历右子树if (index <= node.right && index >= middle + 1)Update(node.rightchild, index, key);//在回溯的路上一路更改,复杂度为lgNif (index >= node.left && index <= node.right){//说明找到了节点if (node.left == node.right){nums[index] = key;node.Sum = node.Max = node.Min = key;}else{//回溯时统计左右子树的值(min,max,sum)node.Min = Math.Min(node.leftchild.Min, node.rightchild.Min);node.Max = Math.Max(node.leftchild.Max, node.rightchild.Max);node.Sum = node.leftchild.Sum + node.rightchild.Sum;}}}#endregion}}

image.png

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

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

相关文章

【miniQMT实盘量化5】获取财务报表数据

前言 上面文章&#xff0c;我们介绍了如何获取实时数据&#xff0c;这篇文章&#xff0c;我们继续往下探讨&#xff0c;介绍关于财务报表数据的获取。 财务报表数据 财务报表数据&#xff0c;也就是常说的基本面数据&#xff0c;是除了行情数据之外&#xff0c;辅助我们投资…

WordPress站点屏蔽过滤垃圾评论教程(Akismet反垃圾评论插件)

前段时间我的WordPress站点经常收到垃圾评论的轰炸&#xff0c;严重时一天会收到几十条垃圾评论。我这个小破站一没啥流量&#xff0c;二又不盈利&#xff0c;实在是不太理解为啥有人要这么执着地浪费资源在上面。 Akismet反垃圾评论插件 其实用了 Akismet 反垃圾评论插件后&a…

Proteus下仿真AT89C51报“串行口通信失败,请检查电平适配是否正确。”解决办法

在Proteus下进行AT89C51串行口仿真时&#xff0c;如果遇到“串行口通信失败&#xff0c;请检查电平适配是否正确”的错误提示&#xff0c;以下是一些解决办法&#xff1a; 1. 了解AT89C51和外部设备的电平要求&#xff1a; 首先&#xff0c;了解AT89C51和外部设备之间的电平…

node版本管理工具-nvm

1、 下载地址 https://github.com/coreybutler/nvm-windows/releases/tag/1.1.11 2、 选择安装地址不能有空格&#xff0c;中文 3、 使用命令

视频网关简介

在数字化时代&#xff0c;视频通信已经成为了人们日常生活和工作中的重要部分。为了满足不同设备和平台之间的视频通信需求&#xff0c;各种视频协议应运而生。然而&#xff0c;这些协议之间的差异使得相互通信变得复杂。因此&#xff0c;视频网关作为一种重要的网络设备&#…

前端已死?看看我的秋招上岸历程

背景 求职方向&#xff1a;web前端 技术栈&#xff1a;vue2、springboot&#xff08;学校开过课&#xff0c;简单的学习过&#xff09; 实习经历&#xff1a;两段&#xff0c;但都是实训类的&#xff0c;说白了就是类似培训&#xff0c;每次面试官问起时我也会坦诚交代&…

实例讲解:在3dMax中如何使用python脚本?

如果你是Python或Maxscript的新手&#xff0c;你现在可以跟着这篇文章开始做一些代码了&#xff0c;本文将让我们从非常基本的东西开始学习。 如何在3dmax中获取选定的节点并打印出它们的名称&#xff1f;所有场景对象如何&#xff1f;我们直接看代码&#xff1a; import MaxP…

CentOS虚拟机重置账号密码

虚拟机忘记密码了 一般来说&#xff0c;虚拟机的账号密码&#xff0c;工作中都会有文档记录&#xff0c;如果忘记了可以查看文档。但是也有特例&#xff0c;虚拟机的密码没有记录到文档中&#xff0c;尝试了很多次依然登录失败&#xff0c;这时候就只能重置账号密码了。 1.重…

OpenCV快速入门:图像分析——傅里叶变换、积分图像

文章目录 前言一、傅里叶变换1.1 离散傅里叶变换1.1.1 离散傅里叶变换原理1.1.2 离散傅里叶变换公式1.1.3 代码实现1.1.4 cv2.dft 函数解析 1.2 傅里叶变换进行卷积1.2.1 傅里叶变换卷积原理1.2.2 傅里叶变换卷积公式1.2.3 代码实现1.2.4 cv2.mulSpectrums 函数解析 1.3 离散余…

【开源】基于Vue.js的天然气工程运维系统的设计和实现

项目编号&#xff1a; S 022 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S022&#xff0c;文末获取源码。} 项目编号&#xff1a;S022&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 系统角色分类2.2 核心功能2.2.1 流程…

多级缓存快速上手

哈喽~大家好&#xff0c;这篇来看看多级缓存。 &#x1f947;个人主页&#xff1a;个人主页​​​​​ &#x1f948; 系列专栏&#xff1a;【微服务】 &#x1f949;与这篇相关的文章&#xff1a; JAVA进程和线程JAVA进程和线程-CSDN博客Http…

最小二乘线性回归

​ 线性回归&#xff08;linear regression&#xff09;&#xff1a;试图学得一个线性模型以尽可能准确地预测实际值的输出。 以一个例子来说明线性回归&#xff0c;假设银行贷款会根据 年龄 和 工资 来评估可放款的额度。即&#xff1a; ​ 数据&#xff1a;工资和年龄&…