
什么是线段树,它能解决什么样的问题?
🌷 仰望天空,妳我亦是行人.✨
🦄 个人主页——微风撞见云的博客🎐
🐳 数据结构与算法专栏的文章图文并茂🦕生动形象🦖简单易学!欢迎大家来踩踩~🌺
🪁 希望本文能够给读者带来一定的帮助🌸文章粗浅,敬请批评指正!🐥
文章目录
🍹假设我们现在有一个非常大的数组,而对于数组里面的数字我们要反反复复不断地做两个操作:
随机选定一块区间,求出这块区间里面的所有数字的和。我们暂且称之为query。修改这个数组中的某个元素的值,即array[i] = num 。我们暂且称之为update。🍹好的,那么现在我们来看看上面的①、②操作时间复杂度分别为多少:
🍏情况一(可以保证update是高效的):
O(n);O(1); 🍏情况二(可以保证query是高效的):
O(1);O(n);
我们会发现,不论如何改变,query和update都无法同时达到O(1)的时间复杂度,总是在O(1)和O(n)之间反复横跳。那么当我们频繁的调用这两个方法时,算法的总体速度都不会特别快。对于这种在线的区间操作,我们有没有什么更好的办法呢?答案当然是有的,也就是用到了咱们的线段树——SegmentTree,它可以把两者的时间复杂度稍微平均一下,使得总体的时间复杂度降为O(log n),让我们一起来看看,线段树究竟是什么,又该如何实现并使用它吧!
🥬线段树是一种特殊的二叉树,它是一种数据结构,更是一种工具。它能把在线的区间修改、维护,从O(N)的时间复杂度变成O(log n)。
我们还是用刚才的arr数组作为原数组,下标从0开始,数值分别为0,1,3,5,7,9,11。

我们如何将这个数组变为线段树呢?
首先我们需要知道线段树的结构,它的每一个结点存储的是一段区间值的总和。根节点存放所有元素的和,然后我们把区间劈成两半,(int)(0 +(5 - 0) / 2) = 2,那么左边为[0,2] 右边为 [3,5]。

下面的以此类推,直到范围不再是一个区间,而是具体的数值,那么我们就不再对它进行分解了(也没办法再分了对吧),构造出来线段树就如图所示:

然后我们说它是一棵线段树对吧,那么它就应该具有线段树的特点,每个结点储存的值应该是区间的总和,那么我们将数值给他从叶子结点往上给它填充起来。

那线段树我们就算是构造出来了,但是这么做有什么好处呢?
假设我们要计算的是这些数字的和:

那么我们可以先把[2,5]这个范围转移到线段树里面,从根节点做搜索:

但是我们会发现根节点并不是[2,5],那么我们就可以将[2,5]劈成两半:[2]和[3,5],左边的往左子树找,右边的往右子树找:

我们可以发现[3,5]是可以在右子树上直接找到的,那么我们就不必再继续“砍”它了;而[2]需要寻找两次,然后找到对应的值:

那么最后[2,5]的和就应该是[2] + [3,5] = 5 + 27 = 32,那么时间复杂度也从O(n) 降到了O(log n)。
query是知道了,那么update又该怎么做呢?
假如我们现在想把arr[4] 的值改为6,那么我们只需要顺藤摸瓜,找到叶子结点为4的这个元素,然后顺着父节点一路改回去,直到改掉根节点(感觉特像回溯有没有!好吧,其实就是回溯,嘿嘿O(∩_∩)O ~),由于我们修改的时候,其他路径的结点没有受到任何的影响,所以我们此处update的时间复杂度依旧为O(log n)。(左下角的结点值是1,我给写漏了,在后面的图中补上了)

🍐那么下面我们就来想想如何用代码来实现 biuld 以及 query 和 update操作。
首先我们需要思考应该怎样来保存这棵树?根据观察,我们可以发现,这样的树并非一棵完全二叉树,那么我们能不能够给它插两个枝干让它变为完全二叉树呢?当然可以!

这几个叶子结点也太小了,哈哈哈,大家别介意啊。我们加入了两个叶子结点,保证这个树能够构造为完全二叉树,这样的话,咱们就可以去思考,该如何保存这棵完全二叉树。
经思考,我们可以用数组来保存这棵树的每个结点,根据我们构造的树来填充一下数组tree[],填完tree[]之后,图片如下↓

对于一棵完全二叉树而言,我们应该知道几个基本知识点:
index,那它的左子结点的下标应该为2 * index + 1,右子结点的下标为2 * index + 2,现在我们给这个树的结点加上标记,一共是2的4次方(树的高度)减一个 = 15个结点。## 为什么可以确定数的结点为15个?树的高度又该如何计算?
> 树的高度其实是根据节点的个数来确定的,比如这道题里面一共有6的结点,我们要对它进行倍增的拆分,
> 由于倍增是根据2的指数次方来倍增的,所以我们同样应该以2分的方式对数组进行拆分,
> 拆分为[0,2]--[3,5] ,然后是[0,1]--[1,2] , [3,4]--[4,5],最后就是把每个分组拆分为单个数字,即为最后一层
> 所以,数的高度理论上应该为1(根节点)+ log以2为底 6的对数(向下取整)+1(单个数字) --> 4;
> 由完全二叉树的性质可知,一个高度为h的完全二叉树,子节点个数为2的h次方-1,即2的4次方-1 --> 15;
> <p>
> 在完全二叉树中:
> 左结点的下标 == 父节点下标 * 2 + 1;
> 右结点的下标 == 父节点下标 * 2 + 2;

我们该如何来构建这颗树呢?为了方便区分,我们对与树有关的变量加个后缀比如left_node,而与原数组有关的变量则不带有该后缀,例如start、end、L、R。
建立树的步骤:
开一个有一定容量的数组,容量大小可以自己根据完全二叉树的特点稍作计算得出。
编写一个方法:build_tree(int[] arr, int[] tree, int node, int start, int end)
参数分别代表 arr: 原数组;tree : 线段树;node : 根节点;start : 根节点的左边界;end : 根节点的右边界
举个例子——拿我们的根节点来说,node = 0,start = 0,end = 5

思路是这样的:我们从根节点出发,使用递归函数进行对二叉树的创建。既然是递归,那就应该有递归出口和递归体。
递归出口:当左边界start == 右边界end的时候,说明该叶子结点已经构建好了,此时我们只需要将数组中的值赋给这个叶子结点。
递归体:我们先找出左孩子和右孩子,接着还是对区间进行“劈砍”的操作,以便确定构建左右子树时候的边界。最后记得将回溯一下——将左右孩子的值相加赋值给父节点。
🍐Java代码如下:
/**
* @param arr: 原数组
* @param tree : 线段树
* @param node : 根节点
* @param start : 根节点的左边界
* @param end : 根节点的右边界
*/
static void build_tree(int[] arr, int[] tree, int node, int start, int end) {
if (start == end) {
tree[node] = arr[start];
} else {
int mid = (start + end) / 2;
int left_node = 2 * node + 1;
int right_node = 2 * node + 2;
build_tree(arr, tree, left_node, start, mid);
build_tree(arr, tree, right_node, mid + 1, end);
tree[node] = tree[left_node] + tree[right_node];
}
}
🍐测试:
public static void main(String[] args) {
arr = new int[]{1, 3, 5, 7, 9, 11};
int size = arr.length;
tree = new int[size * 4];
build_tree(arr, tree, 0, 0, size - 1);
//查看构建的树
for (int i = 0; i < 15; i++) {
System.out.printf("tree[%d] = %d\n", i, tree[i]);
}
}
/**测试结果如下:
tree[0] = 36
tree[1] = 9
tree[2] = 27
tree[3] = 4
tree[4] = 5
tree[5] = 16
tree[6] = 11
tree[7] = 1
tree[8] = 3
tree[9] = 0
tree[10] = 0
tree[11] = 7
tree[12] = 9
tree[13] = 0
tree[14] = 0
*/
update_tree(int[] arr, int[] tree, int node, int start, int end, int idx, int val)更新的步骤:
同样需要刚才的参数,新的参数idx 也就是目标结点的下标,val 是修改后的值。
参数分别代表 arr: 原数组;tree : 线段树;node : 根节点;start : 根节点的左边界;end : 根节点的右边界,idx : 目标结点的下标,val : 修改后的值。
同样使用递归来实现:
start == end,达到条件后,说明找到idx的结点,修改数组和树里面idx下标元素的值。二分的思想,同样先砍一刀,如果当前的idx在左半边,则向左子树递归,右子树同理;同样记得在回溯的时候把左右孩子结点加起来的值赋值给父节点完成更新。举个例子:将arr[4]的值改为6,黄色字体为修改后的值。和下面的测试代码对照之后,可以证明我们的操作是正确的。

🍐Java代码如下:
/**
* @param arr: 原数组
* @param tree : 线段树
* @param node : 根节点
* @param start : 查找范围的左边界
* @param end : 查找范围的右边界
* @param idx : 目标结点的下标
* @param val : 修改后的值
*/
static void update_tree(int[] arr, int[] tree, int node, int start, int end, int idx, int val) {
if (start == end) {
arr[idx] = val;
tree[node] = val;
} else {
int mid = (start + end) / 2;
int left_node = 2 * node + 1;
int right_node = 2 * node + 2;
if (idx >= start && idx <= mid) {
update_tree(arr, tree, left_node, start, mid, idx, val);
} else {
update_tree(arr, tree, right_node, mid + 1, end, idx, val);
}
tree[node] = tree[left_node] + tree[right_node];
}
}
🍐测试:
public static void main(String[] args) {
arr = new int[]{1, 3, 5, 7, 9, 11};
int size = arr.length;
tree = new int[size * 4];
build_tree(arr, tree, 0, 0, size - 1);
/*//查看构建的树
for (int i = 0; i < 15; i++) {
System.out.printf("tree[%d] = %d\n", i, tree[i]);
}
System.out.println();*/
//将arr[4]的值改为6
update_tree(arr, tree, 0, 0, size - 1, 4, 6);
for (int i = 0; i < 15; i++) {
System.out.printf("tree[%d] = %d\n", i, tree[i]);
}
System.out.println();
}
/**
tree[0] = 33
tree[1] = 9
tree[2] = 24
tree[3] = 4
tree[4] = 5
tree[5] = 13
tree[6] = 11
tree[7] = 1
tree[8] = 3
tree[9] = 0
tree[10] = 0
tree[11] = 7
tree[12] = 6
tree[13] = 0
tree[14] = 0
*/
接下来我们来实现查询操作query_tree(int[] tree, int node, int start, int end, int L, int R)
参数分别代表 arr: 原数组;tree : 线段树;node : 根节点;start : 根节点的左边界;end : 根节点的右边界,L : 查询框定的左边界,R : 查询框定的右边界。
查询的步骤同样使用递归来实现:
举个例子:查询[2,5]这个范围所有数的和。
我们从根节点出发,那么[2,5]是在根节点的范围[0,5]之内的,我们还是劈成两半分开找

递归出口:
砍成两半开始递归的时候,我们会发现一种情况,我们左子树结点的范围有可能根本就不在我们要找的范围[L,R]里面,这个时候我们就return掉。那什么情况会不在范围内呢?第一种就如下图所示,R在start的左边,或者L在end右边。if (R < start || L > end) return 0;
·
递归体:
在那个区间内,这里请大家思考一个问题,我们是选择和之前一样(start==end结束然后返回结点值)呢?还是说我们的区间在大区间内部就返回结点值?答案显然是后者! 为什么呢?因为如果是第一种情况,我们每次都需要直接搜到底再回溯上来,这样很低效,因为线段树的每个结点都记录了对应区间的总和,那么我们直接返回这个区间总和就行,相当于剪枝了。🍐Java代码如下:
/**
* @param tree : 线段树
* @param node : 根节点
* @param start : 查找范围的左边界
* @param end : 查找范围的右边界
* @param L : 查询框定的左边界
* @param R : 查询框定的右边界
*/
static int query_tree(int[] tree, int node, int start, int end, int L, int R) {
/*System.out.printf("start = %d\n", start);
System.out.printf("end = %d\n", end);
System.out.println();*/
if (R < start || L > end) {
return 0;
} else if (L <= start && end <= R) {
return tree[node];
} else {
int mid = (start + end) / 2;
int left_node = 2 * node + 1;
int right_node = 2 * node + 2;
int sum_left = query_tree(tree, left_node, start, mid, L, R);
int sum_right = query_tree(tree, right_node, mid + 1, end, L, R);
return sum_left + sum_right;
}
}
🍐测试一下区间[2,4]的和:

public static void main(String[] args) {
arr = new int[]{1, 3, 5, 7, 9, 11};
int size = arr.length;
tree = new int[size * 4];
build_tree(arr, tree, 0, 0, size - 1);
/*//查看构建的树
for (int i = 0; i < 15; i++) {
System.out.printf("tree[%d] = %d\n", i, tree[i]);
}
System.out.println();*/
//将arr[4]的值改为6
update_tree(arr, tree, 0, 0, size - 1, 4, 6);
for (int i = 0; i < 15; i++) {
System.out.printf("tree[%d] = %d\n", i, tree[i]);
}
System.out.println();
//计算[2,5]区间类所有数字加起来等于多少
int s = query_tree(tree, 0, 0, size - 1, 2, 4);
System.out.println("s = " + s);
}
/**
s = 18
*/
🍐可以看到,5 + 13 =18,结果正确。
/**
* @Auther: LiangXinRui
* @Date: 2023/3/1 17:13
* @Description: 查询指定区间内, 所有数的和(可修改). RMQ by SegmentTree
*/
public class SegmentTree {
static int[] arr;
static int[] tree;
/**
* @param arr: 原数组
* @param tree : 线段树
* @param node : 根节点
* @param start : 根节点的左边界
* @param end : 根节点的右边界
*/
static void build_tree(int[] arr, int[] tree, int node, int start, int end) {
if (start == end) {
tree[node] = arr[start];
} else {
int mid = (start + end) / 2;
int left_node = 2 * node + 1;
int right_node = 2 * node + 2;
build_tree(arr, tree, left_node, start, mid);
build_tree(arr, tree, right_node, mid + 1, end);
tree[node] = tree[left_node] + tree[right_node];
}
}
/**
* @param arr: 原数组
* @param tree : 线段树
* @param node : 根节点
* @param start : 查找范围的左边界
* @param end : 查找范围的右边界
* @param idx : 目标结点的下标
* @param val : 修改后的值
*/
static void update_tree(int[] arr, int[] tree, int node, int start, int end, int idx, int val) {
if (start == end) {
arr[idx] = val;
tree[node] = val;
} else {
int mid = (start + end) / 2;
int left_node = 2 * node + 1;
int right_node = 2 * node + 2;
if (idx >= start && idx <= mid) {
update_tree(arr, tree, left_node, start, mid, idx, val);
} else {
update_tree(arr, tree, right_node, mid + 1, end, idx, val);
}
tree[node] = tree[left_node] + tree[right_node];
}
}
/**
* @param tree : 线段树
* @param node : 根节点
* @param start : 查找范围的左边界
* @param end : 查找范围的右边界
* @param L : 查询框定的左边界
* @param R : 查询框定的右边界
*/
static int query_tree(int[] tree, int node, int start, int end, int L, int R) {
/*System.out.printf("start = %d\n", start);
System.out.printf("end = %d\n", end);
System.out.println();*/
if (R < start || L > end) {
return 0;
} else if (L <= start && end <= R) {
return tree[node];
} else {
int mid = (start + end) / 2;
int left_node = 2 * node + 1;
int right_node = 2 * node + 2;
int sum_left = query_tree(tree, left_node, start, mid, L, R);
int sum_right = query_tree(tree, right_node, mid + 1, end, L, R);
return sum_left + sum_right;
}
}
//Finish:Tree's build、update、query test.
public static void main(String[] args) {
arr = new int[]{1, 3, 5, 7, 9, 11};
int size = arr.length;
tree = new int[size * 4];
build_tree(arr, tree, 0, 0, size - 1);
//查看构建的树
for (int i = 0; i < 15; i++) {
System.out.printf("tree[%d] = %d\n", i, tree[i]);
}
System.out.println();
//将arr[4]的值改为6
update_tree(arr, tree, 0, 0, size - 1, 4, 6);
for (int i = 0; i < 15; i++) {
System.out.printf("tree[%d] = %d\n", i, tree[i]);
}
System.out.println();
//计算[2,5]区间类所有数字加起来等于多少
int s = query_tree(tree, 0, 0, size - 1, 2, 4);
System.out.println("s = " + s);
}
}
🍭到此,线段树的简单应用就结束了,你学会了吗?码字实属不易,如果有帮助就点个赞吧~
🌺🌺🌺🌺🌺🌺🌺🌺🌺🌺🌺🌺🌺🌺🌺🌺完结撒花🌺🌺ヽ(°▽°)ノ🌺🌺🌺🌺🌺🌺🌺🌺🌺🌺🌺🌺🌺🌺🌺🌺

🐬初学一门技术时,总有些许的疑惑,别怕,它们是我们学习路上的点点繁星,帮助我们不断成长。
🐟文章粗浅,希望对大家有帮助!
🥛参考B栈大佬“正月点灯笼”的视频:【数据结构】线段树(Segment Tree)
编辑:更改了标题。我对两个部分是否相同不太感兴趣,而是如果它们在一定的公差范围内彼此共线。如果是这样,那么这些线应该聚集在一起作为一个单独的线段。编辑:我想有一个简短的说法:我试图以一种有效的方式将相似的线段聚集在一起。假设我有线段f(fx0,fy0)和(fx1,fy1)和g(gx0,gy0)和(gx1,gy1)这些来自计算机视觉算法边缘检测器之类的东西,在某些情况下,两条线基本相同,但由于像素容差而被视为两条不同的线。有几种情况f和g共享完全相同的端点,例如:f=(0,0),(10,10)g=(0,0),(10,10)f和g共享大致相同的端点和大致相同的长度,例如:f=(0,0.01
我想画一个点,大约1秒后我想画下一个点。这是否可能:我已经试过了:functionsimulate(i){setTimeout(function(){drawPoint(vis,i,i);},1000);}for(vari=1;i不幸的是,这是行不通的。它只是立即绘制整条线。 最佳答案 这是行不通的,因为for循环将立即运行到结束,setTimeouts将被同时调度,所有函数将同时触发。取而代之的是,这样做:vari=1;(functionloop(){if(i++>200)return;setTimeout(function(){
我正在尝试创建类似于UDK或MayaMaterial编辑器的东西http://www.google.com/search?q=udk+material+editor&oe=utf-8&rls=org.mozilla:en-US:official&client=firefox-a&um=1&ie=UTF-8&hl=en&tbm=isch&source=og&sa=N&tab=wi&biw=1144&bih=929通过单击一个连接并将其拖动到另一个连接,可以连接两个节点。WPF可以执行此操作,但我不知道如何以编程方式(使用C#,而不是XAML)绑定(bind)贝塞尔曲线的端点和控制点以跟随
我使用Fortune算法找到一组点的Voronoi图。我得到的是一个线段列表,但我需要知道哪些线段形成闭合多边形,并将它们放在一个由它们围绕的原始点散列的对象中。找到这些内容的最快方法是什么??我应该从算法中保存一些重要信息吗?如果是什么?这是我在Java中从C++实现移植的fortune算法的实现hereclassVoronoi{//ThesetofpointsthatcontrolthecentersofthecellsprivateLinkedListpts;//Alistoflinesegmentsthatdefineswherethecellsaredividedprivat
见上图;基本上,我想要一个简单的测试来检查一个点是否在线段的范围内。我拥有的信息(或输入,如果您愿意)是点的坐标和线段终点的坐标。我想要的输出是一个简单的boolean值。我怎样才能以简单的方式检查它? 最佳答案 使用内积可以简单统一的检查。两个vector之间的内积可以在几何上可视化为两个vector的长度乘以两者夹角的余弦的乘积,或者是其中一个vector的长度与(正交)投影长度的乘积另一个到由该vector确定的线上。在您的情况下,如果您将vectorv从线段的一个端点投影到所考虑的点,则该点位于允许区域内当且仅当投影落在段s
以下是http://www.spoj.pl/problems/LITE/的实现使用具有惰性传播的线段树。我是分割树的新手,我不明白为什么我会得到TLE。有人可以看看它并帮助我纠正我的错误吗?#include#include#include#include#defineMAX100000usingnamespacestd;intM[2*MAX+1];intflag[2*MAX+1];intcount;voidrefresh(intbegin,intend,intn){M[n]=end-begin+1-M[n];flag[n]=0;flag[n*2]=!flag[n*2];flag[n*2
是否可以有效地计算与数轴上的单个点P重叠的线段的数量?所有线段都位于一条数字线上(它是一个1-D世界,而不是一个3-D世界)。每条线段都有一个起始坐标X1和一个结束坐标X2。例子:LinesegmentAspansfromX1==1toX2==3LinesegmentBspansfromX1==2toX2==4LinesegmentCspansfromX1==3toX2==5LinesegmentDspansfromX1==1toX2==4----------------------------------------Ex1:LinesegmentsthatoverlappointP=
我正在从一个数据数组中实现线段树,我还想在更新一系列数据时保持树的最大/最小值。这是我遵循本教程的初步方法http://p--np.blogspot.com/2011/07/segment-tree.html.不幸的是它根本不起作用,逻辑对我来说很有意义,但我对b和e有点困惑,我想知道这是数据数组?或者它是树的实际范围?据我了解,max_segment_tree[1]应该包含[1,MAX_RANGE]范围内的max而min_segment_tree[1]应该包含范围[1,MAX_RANGE]的min。intdata[MAX_RANGE];intmax_segment_tree[3*MA
我正在处理计算机图形学。我想表示一条有两个端点的线,然后我想要我的Line2d类有一个返回Vector2d的方法对象。假设,我有以下类(class):structPoint2d{intx;inty;};然后,我可以很容易地用两点表示一条线段:classLineSegment2d{private:Point2dstart;Point2dend;public:......};根据定义,vector由大小和方向组成。classVector2d{private:Point2dp;public:doubleMagnitude(void);PointComponent(void);Vector2d
我有一条射线,我需要找到它命中的最近线段。我认为如果我先对线段进行排序,可以在O(logn)时间内完成此操作,但我不记得如何对它们进行排序......我认为某种树最有效,但我该如何排序他们的起点和终点?如果可能的话,我还想快速插入到这个数据结构中。一条射线与一条线段有很多代码,但我需要一些关于一条射线与多条线段的代码...我不知道要用谷歌搜索什么术语。适当文章的链接很好,C++代码更好。谢谢!:)PS:线段实际上是非自相交多边形的边,按CCW顺序排序...但我认为以不同的方式排序它们可能有一些优势?这都是二维的。再三考虑,我不完全确定这是否可能。某种空间划分可能会有所帮助,但除此之