在好不容易把树状数组大概看懂的情况下,现在又紧接着挑战线段树了。
这些相对高级的数据结构都是从基础的数据结构加上一些思想后创造出来,比如,线段树的亲属就有平衡二叉树。所以,在学习这些高级的数据结构时,多多汲取里面的思想,对我们编程有莫大的益处,相对来说,它们在实际的工作中直接使用到的机会反而不多了。
按百度百科的定义,线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。如图所示:
所以,对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。因此线段树是平衡二叉树,最后的子节点数目为N,即整个线段区间的长度。
由此可知,查找时的时间复杂度为O(logN)。100万长的树,也就20次就KO了。使用线段树可以快速的查找某一个节点在若干条线段中出现的次数 。但是,还是有不足,见上图中,叶子结点个数等于根线段的长度,总结点数为2 * N (具体证明,我也不清楚,等学好离散……大家自己证吧),也就是说如果线段中,最右端的端点很大的话,MAX很大,比如,到1000万,那就要1000万个叶结点,2000万个结点,然后,中间很多的线段根本用不上,就造成了巨大的浪费。
于是,还要进行离散化,在信号处理中的离散化是指把原来连续的信号从中抽若干个点,这里也类似,从连续长度的数组中,只保留输入数据相关的那些线段。网上找的一个简单的说明:http://blog.sina.com.cn/s/blog_72fc630a0100pbb3.html 。
上面说的离散化是把数据排个序,用数组记录它们的大小顺序,这样就可以把数值很大的线段也挤到一起去了,就可以得到压缩后的数据。
离散化是开始构建之前的准备,假设我们把所有的端点按序排好,得到数组point[],大小为N,这些就是我们到时候要用的线段树的叶子结点的值,所以整个线段树的空间大小也就是2 * N,下面就是万里长征第一步,建树!。
根据图示,我们知道该树的每个结点最起码要左端点和右端点的位置,所以,设计一个结构体:
struct segment { int left, right;}; //暂定
然后,从输入的数据中,找到最大值和最小值,就从根结点开始建树。根结点的位置在哪儿呢?这个一般都设置成1这个位置,大家不知道看过堆排序否,堆排序就是利用从根(设为1)然后逐层往下更新。在二叉树中,对于位置为i(不能为0)的结点来说,它的左结点就是2*i,右结点就是2 * i + 1,所以把根设为1,方便树的平衡和计算。
创建一棵N个结点的树,其实就是个数组啦,Segment node[2 * N]。嗯,我知道C语言不允许这样写……
把根结点创建好后,下面的问题就是怎么找它的左青龙右白虎呢?各位看客,还记得上面说结点[a,b]的左线段和右线段分别是什么吗?
恭喜你!答对了,[a, (a + b)/2] 和 [(a + b)/ 2+1, b],再算上它们对应的位置2 * pos和 2 * pos + 1,就可以这样层层往下构造。
咦?什么时候结束呢?如图,当线段就是一个点的时候,就没有儿子了,所以这时就结束递归生成。
于是总结建树的C语言版如下:
void build(int s, int t, int pos)
{
int mid = (s + t) / 2;
segtree[n].left = s;
segtree[n].right = t;
if (s == t)
return ;
build(s, mid, 2 * pos);
build(mid + 1, t, 2 * pos + 1);
}
假设我们输入了m条线段,之前是得到了2 * m个点(不相同的点有n多个),现在则把这m条线段在线段树上到此一游吧!记得排队(知道是第几条线段,线段没有排序)哦!
仍是从根结点开始,这个和树状数组不同,树状数组的更新是自底向上,这里总是自顶向下。
直接上代码是如此的不好……
一步步说明吧,现在来了一条线段[x, y],从根结点开始,无疑一般都会比根结点代表线段长度要短上一点,所以,会到子结点,我们已经知道左右结点的端点了,所以,我们要拿线段的两端和 (a + b)/2 比较,是在左边呢?右边呢?还是刘翔跨栏呢?然后再决定到下面的哪结点,也是个递归的过程呢!
好的,这条线段掉下分下的,总算遇到个结点对上它的胃口了,哦,端点相同了,那就不用再继续了。可是我们用什么来记录下状态了,看来那个结构体还要改下:
typedef struct Segment { int left, right; int n; }Segment;//仍暂定
于是,我们就可以更新这个结点的n值喽,n = n + 1,表示新来一条线段到此一游。于是这条线段分别递归到各点留念后,就返回了。
下一条线段来了,整个过程是那么的顺利,让它觉得毫无压力,遇到对上胃口的点,就对点进行更新,n = n + 1,又有一条边看上了。
所以这部分的C语言代码如下:
void insert(int s, int t, int step)
{
if (s == segtree[step].left && t == segtree[ste].right)
{
segtree[step].n++;
return ;
}
if (segtree[step].left == segtree[step].right)
return ;
int mid = (segtree[step].left + segtree[step].right) / 2;
if (mid >= t)
insert(s, t, step * 2);
else if (mid < s)
insert(s, t, step * 2 + 1);
else
{
insert(s, mid, step * 2);
insert(mid + 1, t, step * 2 + 1);
}
}
最后就是统计结点覆盖次数了。每次仍是从根结点开始,对每一个结点,如果它的n 值即表示几条线段覆盖到这部分,所以,sum += 覆盖的条数 * 线段的长度。然后,再继续查询子结点,递归查询这一部分和上面相同,我甚至考虑,这三块能不能把这部分抽出来……
下面是某段区间里面,点的覆盖次数的C代码:
void count(int s, int t, int step)
{
if (segtree[step].n != 0)
sum += segtree[step].n * (t - s + 1);
if (segtree[step].left == segtree[step].right)
return ;
int mid = (segtree[step].left + segtree[step].right) / 2;
if (mid >= t)
count(s, t, step * 2);
else if (mid < s)
count(s, t, step * 2 + 1);
else
{
count(s, mid, step * 2);
count(mid + 1, t, step * 2 + 1);
}
}
本来有DELETE删除的,不过也就是和INSERT相反而已,所以,就不写了……已经很长了。
真艰难啊!又大概明白了一个数据结构,不过花了好多时间,写日记都推后了!