本人为在校非计算机专业学生,才疏学浅,如有错误,恳请斧正…
本博文适用于未接触过线段树的同学使用,只涉及简单的线段树使用,算法巨佬请忽略
综述
线段树(Segment Tree)通俗地说是一种可以对一段固定区间内的数据进行较大规模改动查询的数据结构,是一种对于RMQ ( 区间最值 ) 的实现方法。因为使用了二叉树的结构,而且属于完全二叉树,所以线段树具有较好的效率,现对线段树的使用做一个简短的描述。
线段树使用的场合
如上所述,线段树解决的问题发生在固定长的区间上,可以解决的问题属于对于固定区间内的一段进行修改、查询的问题。例如之前提到的RMQ(Range Minimum/Maximum Query),即在序列内找最值,或者是求和、求某些区间的覆盖问题,线段树都具有较好的解决能力。
线段树的基本图解
线段树的结构
如之前所说,线段树是一种快速解决区间覆盖查询问题的数据结构,所以,对于区间的查询来说,较大的区间(也就是根),然后逐渐缩小区间找到需要查询或者插入的区间(各个子节点)。
- 对于线段树,我们使用二叉树进行不同区间的描述,例如下图:
假设我们设置一段10个数字的区间,并且数值为
1、4、 2、 2、 8、 7、 3、 6、 9、 2
对于此我们可以这样构造一个线段树:
按照图片上的红字区间可以确认这个线段树的正确性。我们可以发现一个节点的子节点,实际上就是这个节点表示区间的一个部分(一半),左右两个区间,于是就组成了一个线段树。
*ps:*如果无法理解,可以从下往上看,可以更好地理解线段树是如何通过节点表示一段数据的某一项值的
线段树如何建立和插入
线段树建立时,对于 N 规模的数据,一般使用 4N 规模的内存用以构建线段树。
- 第一步建立数组,大小为4N
- 进行插入,如果区间属于子节点的一个,那就只对于那一个区间进行插入,例如图例的右边。
- 如果插入时出现两个子区间全都有涉及,那就要把全部区间全部进行插入,例如根位置和左边的位置那样。
如图就是在区间(3,6)插入5时的搜索情况情况
4. 搜索完毕以后需要对值进行覆盖,一般是自下而上进行的。
具体情况如图所示
从上到下,就可以完成整体的更新,所以,一般在一个节点把子节点的值更新完成后,进行父节点值的更新。
线段树如何进行查询
线段树的操作主要为覆盖和读取两个部分
- 依然类似于插入,我们需要找到相关的区间,并且不断比较区间得到相关的值,运气好的话甚至可以不比较,例如期间正好就是某一节点位置。
- 文字怎么说都不如画图,现提供两个用例:
用例1:查找(0,5)区间的最大值
图中当找到一个正好的区间以后,到了(0,5)的节点就会停止搜索,返回最大值8,于是9这个节点得到了一个最大值8,此处的函数也返回8,结束查询。
用例2:查找(3,8)区间的最大值
- 对于这个查找,根节点就会存在下方返回的两个最大值,对于这两个最大值,选取最大的返回即可
- 对于左右区间来说,就是重复之前根节点的那种查找,找到对应区间,如果期间无法完全契合那就继续向下,直到找到对应的极值进行返回。
线段树的代码实现
为了方便理解和查看,这里是一个已经实现的 SegTree 类,
实际应用的时候可以根据情况进行改动
此处使用求区间最值的代码进行实现说明
// 此版本的代码书写较为仓促,且缺少对于健壮性的考虑,理解思想即可,线段树的思想理解以后,书写起来也不会太过于难受
#pragma once
#include<iostream>
#include<string>
using namespace std;
class SegTree
{
public:
SegTree(int num);
~SegTree();
void insert(int left, int right,int pos, int value);
int search(int min, int max, int left, int right, int pos);
//参数表含义:左边界,有边界,需要查询的左边界,需要查询的右边界,数组开始的位置(默认写0)
private:
int *tree;
int len;
void insert(int min, int max, int left, int right, int pos, int value);
//参数表含义:左边界,有边界,需要查询的左边界,需要查询的右边界,数组开始的位置(默认写0),插入的值
int max_(int a, int b) { return a > b ? a : b;}
};
SegTree::~SegTree()
{
delete[]tree;
len = 0;
}
SegTree::SegTree(int num)
{
tree = new int[num * 4];
//初始化操作,使用memset()更好
for(int i = 0; i < num*4; i++)
{
tree[i] = 0;
}
//memset(tree, 0, sizeof(tree));
len = num;
}
//初次进入插入操作时的函数,接下来的调用为私有,仅允许此函数调用
void SegTree::insert(int left, int right,int pos, int value)
{
if (left <= 0 && right >= len - 1)
{
tree[pos] = value;
return;//发现到了尽头,那就直接返回
}
else
{
if (left > len / 2)
//二分,如果左半边的区间不需要查询,左边界在右边,那就直接跳到右边
{
insert(len / 2 + 1, len - 1, left, right, pos * 2 + 2, value);
}
else if (right <= len / 2)
//和上面结果相反时的操作
{
insert(0, len / 2, left, right, pos * 2 + 1, value);
}
else
//如果不幸卡在两个区间之间,那就分别插入,进行递归
{
insert(0, len / 2, left, len / 2, pos * 2 + 1, value);
insert(len / 2 + 1, len - 1, len / 2 + 1, right, pos*2+2, value);
}
}
//对于最大之进行必要的更新
tree[pos] = max_(tree[pos*2+1], tree[pos*2+2]);
}
//私有的插入函数
void SegTree::insert(int min, int max, int left, int right,int pos, int value)
{
if (min == left && max == right)
{
tree[pos] = value;
return;
}
else
{
if (left > min + (max - min) / 2)
{
insert(min + (max - min) / 2 + 1, max, left, right, pos * 2 + 2, value);
}
else if(right <= min + (max - min) / 2)
{
insert(min, min + (max - min) / 2, left, right, pos * 2 + 1, value);
}
else
{
insert(min, min + (max - min) / 2, left, min + (max - min) / 2, pos * 2 + 1, value);
insert(min + (max - min) / 2 + 1, max, min + (max - min) / 2 + 1, right, pos * 2 + 2, value);
}
}
tree[pos] = max_(tree[pos*2+1], tree[pos*2+2]);
}
//查询函数
int SegTree::search(int min, int max, int left, int right, int pos)
{
if (min == left && max == right) //返回标志,找到固定区间内的最大值
{
return tree[pos];
}
else //和insert相仿的结构
{
if (left > min + (max - min) / 2)
{
return search(min + (max - min) / 2 + 1, max, left, right, pos * 2 + 2);
}
else if (right <= min + (max - min) / 2)
{
return search(min, min + (max - min) / 2, left, right, pos * 2 + 1);
}
else
{
//此处使用变量,主要考虑到可读性,否则函数太长不利于观察修改,这里的插入完全可以写进max_()函数
int a = search(min, min + (max - min) / 2, left, min + (max - min) / 2, pos * 2 + 1);
int b = search(min + (max - min) / 2 + 1, max, min + (max - min) / 2 + 1, right, pos * 2 + 2);
return max_(a, b);
}
}
}
小结
对于线段树在各类程序设计竞赛中多有出现,由于其较好的效率也得到了较多的使用,本文只介绍了基本的使用,对于线段树来说,较大的内存空间就需要用到压缩,凡此种种都属于较为其较深的内容,本文不做过多讨论,毕竟我也是小白
[\哭]