【原创】一维线段树的建树、插入、遍历和删除

一、定义:

线段树,说白了,就是把线段储存在一棵树里。

举一个例子:

数轴上,有一条线段[1,9]:12345678

将其二分:

         12345678

       1234    5678

    12 34       56 78

1 2 3 4           5 6 7 8

这就是一棵线段树。

把数字换成线段,更清楚:


用严谨一点的语言,是一棵二叉树,树中的每一个结点表示了一个区间[a,b]。每一个叶子节点表示了一个单位区间。根节点表示的是所有的区间。

对于每一个非叶结点所表示的区间[a,b],左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b] 。

二、树的表示与建树

我们可以发现,线段树是一颗完全二叉树,(即每个结点都有0或2个子节点)。
完全二叉树的结点有这么一个性质,tree[i]的父节点是tree[i/2],左儿子是tree[i*2],右儿子是tree[i*2+1]。
所以我们可以写出这样的代码:
struct Epic
{
	int l,r;//l,r表示区间的左端点和右端点,闭区间。
	int C;//C为权值
}tree[4*MAXN];

tree的大小至少要开为4*MAXN。
要建一棵树,也就是初始化一颗树,只需遍历这棵树,把所有结点都附上初值即可。
详见代码:
void built(int i,int l,int r)
{
	tree[i].l=l,tree[i].r=r,tree[i].C=0;//附上需要的值
	if(l==r) return ;
	int mid=(l+r)/2;
	built(i*2,l,mid);
	built(i*2+1,mid+1,r);
}

当然你也可以用指针储存这棵树。
struct node 
{
    int l, r;	
    int C;
    node *lch, *rch;	//左右子区间指针
};
node *root;	

三、插入与删除
在线段树中插入一条[l,r],权值为D的线段。
对于每一个结点所对应的区间,有三种情况。

可能被完全覆盖,可能只被覆盖了一部分,可能压根儿没被覆盖。
那么我们就可以写出这样的程序:
void insert(int i,int l,int r,int D)
{
	int mid=(tree[i].l+tree[i].r)/2;//没有覆盖
	if(tree[i].r<l or r<tree[i].l) return ;
	else if(l<=tree[i].l and tree[i].r<=r) tree[i].C=D; //被当前区间覆盖
	else if(r<=mid) insert(i*2,l,r,D);//仅在左子区间
	else if(l>=mid+1)  insert(i*2+1,l,r,D); //仅在右子区间
	else//分在左右区间
	{
		insert(i*2,l,mid,D);
		insert(i*2+1,mid+1,r,D);
    }
}


删除程序与插入程序大同小异,只不过是insert一条权值为0的线段罢了。
但是,假设有一颗以线段[1,15]为根的线段树,其中的所有结点之前都已经插入过,即我们曾经这样附过值:[1,2],[2,3],……,[14,15],[1,3],[3,5],……,[13,15],[1,5],…………,[1,15]。然后删去[1,15]。
那么整个线段树中的所有结点的状态就都与实际不符了,全都需要修改。修改的复杂度就是线段树的结点数。如果线段很长,复杂度就很高了。有可能比直接模拟的复杂度还要高!
怎么办呢?
为了解决这个问题,我们为每个结点增加一个标记,bj。
Step 1:在擦去线段[a,b]之后,给它的左儿子和右儿子都做上标记,令它们的bj=-1。而不需要对整棵树进行修改。
Step 2:以后每次访问某条线段,首先检查它是否被标记,若被标记,则进行如下操作:
① 将该线段的状态改为未被覆盖,并把该线段设为未被标记,bj=0。
② 把该线段的左右儿子都设为被标记,bj=-1。
这样做的原理很简单,以下图为例:

把线段[1,5]擦去后,给[1,3],[3,5]加上标记。

若以后我们需要用到线段[3,4],就必须先访问[3,5],因为[3,5]被标记,我们访问它之后标记就会传递给[3,4]和[4,5]。[3,4]就给标记上了。也就是说,标记会顺着访问[3,4]的路径一直传递下去。
所以当我们需要用到某条线段时,标记就会传到它那里去,使它得到更新,避免错误的发生。而对于那些不用的线段,就没有更新的必要了,因此我们也不会访问到它和更新它,这样就避免了无用功的产生,提高了程序效率。
一句话,如果要删掉一条线段,打上标记,不管它。后来用到它的时候,再删。

传标记的代码:
void clear(int i) //清除结点i的懒标记,并往下传递
{
	tree[i].C=0;
	tree[i].bj=0;//表示本结点已处理,恢复bj的状态
	tree[i*2].bj=-1,tree[i*2+1].bj=-1;	//给左右儿子打上懒标记
}


如果tree[i].bj==-1时调用这个函数。


四、数的遍历
数的遍历就是把每个点都走一次。实现很简单,详见代码:
void travel(int i)
{
	//do something
	if(l==r) return ;
	travel(i*2);
	travel(i*2+1);
}










  • 2
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值