线段树是一种树形的高级数据结构,因为noip不考这种数据结构,而且当时的代码实现能力比较差,编程水平比较低,所以高中的时候粗略的看了看,并没有真正的搞懂,虽然早已弃了算法竞赛的坑,但还是回过头去研究了一下这种高级数据结构并写下此教程,以纪念曾经奋斗在OI道路上的日子。
问题提出:
现有如下的问题,给定一个的序列,实现以下操作:
①更新序列的某个值。
②查询序列的某个区间的最小值(最大值、区间和)线段树常用于解决区间统计问题。求最值,区间和等操作均可使用该数据结构,本篇以求最小值为例。
③更新序列的某个区间内的所有值。
对于求最小值,我们很容易想到的算法就是。更新序列的某个值直接找到该值,更新,时间复杂度是O(1);区间查询直接遍历该区间,时间复杂度是O(n);区间修改的也是直接遍历该区间修改,时间复杂度是O(n),在数据量特别大,操作比较多的时候,效率是很低的。另一种解法是这样的。构建一个二维数组,a[i][j]表示区间[i,j]的最小值。这样查询操作的复杂度为O(1),但是这样的话,修改的复杂度也不低而且如果数据量特别大,O(n^2)的空间复杂度也是不容忽视的。这时候就需要我们是用线段树这种优秀的高级数据结构来解决了。
线段树:
我们以序列{5,9,7,4,6,1}为例子演示。这个序列构成的线段树是这样的。
从这颗树上我们可以了解线段树的这几个特点,线段树是一颗近似的完全二叉树,每个节点代表一个区间,节点的权值是该区间的最小值。根节点是整个区间。每个节点的左孩子是该节点所代表的的区间的左半部分,右孩子是右半部分。为方便起见,如果区间长度为奇数,则左孩子为较长的半部分。通过线段树,我们可以用O(logn)的时间复杂度完成查询和更新操作。当然,预处理的时间复杂度是O(n)。
线段树的构建:
我们将每一个节点封装到Node类中。
class Node//节点
{
int start;//区间的左端点
int end;//区间的右端点
int data;//该节点的值
int mark = 0;//延迟更新的标记
public Node(int start,int end)//构造方法中传入左端点和右端点
{
this.start = start;
this.end = end;
}
void addMark(int value)//做标记
{
this.mark+=value;
}
void clearMark()
{
this.mark = 0;
}
public String toString()
{
return start+"-"+end;
}
}
这个节点类有四个域,分别是区间左端点,区间右端点,节点的权值,延迟更新的标记(暂时用不到)构造方法中传入左右端点。构建线段树的时候,我们可以从根开始构建,我们注意到,这颗线段树的叶子节点区间中的元素只有一个,代表着序列的元素,所以n个元素的序列构成的线段树中的叶子节点有n个。这颗线段树虽然不是完全二叉树,但是可以通过移动变成一颗完全二叉树。n个叶子节点的完全二叉树的非叶子结点个数为n-1所以n个元素的序列构成的线段树的节点有2n-1个。这2n-1个只是有效的节点,因为我们用数组存储,所以需要给这颗近似的完全二叉树添加一些虚节点,