您的位置:首页 > 理论基础 > 数据结构算法

线段树

2015-10-17 22:09 309 查看

线段树

本文总结自己学习线段树的一些知识点。我最初是通过区间最值查询问题学习到线段树,查询一个区间的最值可以使用RMQ离线算法,该离线算法需要O(nlgn)的预处理时间和O(1)的查询时间。但是一个区间的某个值修改后,又需要重新计算,对于区间的值频繁的修改的情况,RMQ离线算法并不合适。

线段树确是可以针对区间的值频繁的修改的情况作出应对。线段树是用O(lgn)的时间处理修改,用O(lgn)的时间进行区间最值查询,相对RMQ离线算法来讲是一种折中。

定义

先来看一下百度百科的定义…:

线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。

对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。因此线段树是平衡二叉树,最后的子节点数目为N,即整个线段区间的长度。

下面就是一个简单的线段树的结构(摘自网络)



给定10个元素的值,按照其顺序最终构成了线段树的10个叶子节点。根节点表示区间[1,10]的最值;假设我们要初始化确定区间[1,5]的最小值,(1+5)/2 =3 那么分为以下几步:

确定其左半边区间[1,3]的最小值

确定其右半边的[4,5]的最小值

取[1,3]区间和[4,5]区间的之中的最小值

区间[1.3]和区间[4,5]可以递归的操作下去,那么构建一个线段树的时间复杂度为O(N)(可自行计算)

此外,对于每个查询操作都可以在O(lgN)(树的高度)时间内完成;同理修改某个值后影响的区间数目也是在O(lgN)量级。

线段树的操作

线段树构建

线段树的构建需要O(N)的时间,构建之后未优化的树的空间复杂度是2N. 构建的过程用递归实现较容易理解,对于一个区间节点,构建好左区间儿子和右区间儿子之后,将该区间的最值设定为min(tree[lson].s,tree[rson].s)。代码如下:

struct TreeNode{//线段树节点
int l, r;
int s;//区间最值
}tree[max*2];

int a[max];//对应区间里面每个元素的值

void bulidSegTree(int i,int left ,int right)
{
tree[i].l =left;
tree[i].r = right;
if(left == right)
{
tree[i].s = a[left];
return;
}
int mid = left+(right-left)/2;
bulidSegTree(lson,left,mid);
bulidSegTree(rson,mid+1,right);

tree[i].s = min(tree[lson].s,tree[rson].s);
}


查询

要查询一个区间[a,b]的最值,我们首先要从线段树的根节点开始查询,若根节点的表示的区间为[l,r](一开始l<=a<=b<=r)。那么需要判断[a,b]是落在[l,r]的哪个区间当中,mid= l+(l-r)/2,有三种情况:

[a,b]落在了[l,r]左边,那么我们只需要在该节点的左子树中查找对应[a,b]区间的最值

[a,b]落在了[l,r]右边,那么我们只需要在该节点的右子树中查找对应[a,b]区间的最值

[a,b]横跨了[l,r]的左右两半边,因此要取区间[a,mid]和[mid+1,b]两个最小值中的最小值

同时根据平衡二叉树的性质,我们可以通过节点的下标i,迅速定位到其左孩子和右孩子的下标:

lson = i*2(等价于i<<1)

rson = i*2+1(等价于i<<1|1)

代码如下:

#define lson (i<<1)
#define rson (i<<1|1)

//查询区间的最值
int Query(int l, int r, int i)
{
if( l==tree[i].l && r== tree[i].r ) return tree[i].s;

int mid = tree[i].l +(tree[i].r-tree[i].l)/2;

if(mid<l)
return Query(l, r, rson);//在右半边
else if(mid>=r)
return Query(l,r,lson);//在左半边
else
return min(Query(l, mid, lson), query(mid+1, r, rson));
}


线段树的查询区间总是在减小的,并且查询的深度等同于树的高度,因此线段树的查询时间复杂度为O(lgN).

修改

假设我们修改了某个叶子节点的值,我们只需要把该的节点的所有祖先节点的应对最值进行修改,时间复杂度是O(lgN),这事相对于RMQ离线算法的一大优势。思路大致和查询一致,判断要改的点区间是落在当前的左半边还是右半边,递归的修改完子树节点的值之后,再更新当前节点最值。

修改代码如下:

/**
*l代表要修改元素的下标(对应于某一个叶子节点)
*num是要被设定的新的值
*i表示当前的线段树节点的下标
*/
void modify(int l, int num, int i)
{
if(l == tree[i].l && l == tree[i].r) {
tree[i].s = num;
return;
}
int mid = tree[i].l +(tree[i].r-tree[i].l)/2;
if(mid < l) {
modify(l, r, num, rson);
}
else {
modify(l, r, num, lson);
}

tree[i].s = min(tree[lson].s,tree[rson].s);
}


总结

本文简单介绍了线段树以及线段树的基本操作,希望自己以后常回顾,不断巩固线段树的知识,并期待加以应用。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息