数据结构--线段树

线段树

一.概念(构建)

线段树是一种基于二分思想的完全二叉树,用于维护一段数据或数组,并且使得所有操作的时间复杂度尽量维持在O(nlogn)。
对于未安排数据的节点都选择默认为0,且每一个节点存储的值都是左右孩子进行一些操作的结果(叶节点除外)。
在这里插入图片描述

1.代码实现

其实可以直接以数组实现,但为了实现再存储原数组左右区间的值,就使用结构体实现线段树

#include<iostream>
using namespace std;

const int N = 1e4 +5;
int arr[N];

struct segmentTree{
    
    
	int l,r;
	int val;
}tree[4*N]; 

void build(int p,int l,int r) {
    
    
	tree[p].l = l;
	tree[p].r = r;
	//终止条件
	if(l==r) {
    
    
		tree[p].val = arr[l];
	} 
	else {
    
    
		//本质上是递归的过程
		int mid = (l+r)/2;
		build(2*p+1,l,mid);
		build(2*p+2,mid+1,r);
		tree[p].val = tree[2*p+1].val + tree[2*p+2].val;
	}
}

int main( ) {
    
    
	for(int i=0;i<=5;i++) {
    
    
		cin>>arr[i];
	}
	build(0,0,5);
	for(int i=0;i<15;i++) {
    
    
		printf("tree[%d,%d]=%d\n",tree[i].l,tree[i].r,tree[i].val);
	}
	return 0;
}

2.运行结果

tree后面跟着的是区间值,一些未进入的结点,全部都初始化为0
在这里插入图片描述

二.基本操作

1.查询(单点 or 区间)

查询区间和线段树节点区间相交的不同情况:

  1. 查询区间完全包含线段树节点区间------------直接返回该节点tree[p].sum
  2. 查询区间和线段树节点左孩子有交集---------往下递归query(2*p+1,L,R)
  3. 查询区间和线段树节点右孩子有交集---------往下递归query(2*p+2,L,R)

单点查询就是区间查询的特殊情况,此时L = R,对以下代码稍微修改一下就可以了

int query(int p,int L,int R) {
    
    
	int sum = 0;
	//终止条件
	if(tree[p].l>R||tree[p].r<L)
		sum = 0;//不在范围内直接返回
	else if(tree[p].l>=L&&R>=tree[p].r)
		sum += tree[p].val;//包含其中直接返回这个节点的值
	else {
    
    	
	    //以上两种情况都不包含则是有交集的情况,对左右孩子都递归,不符合会直接返回
		int mid  = (L+R)/2;
		sum += query(2*p+1,L,R);
		sum += query(2*p+2,L,R);
	}
	return sum;
}

测试实例
在这里插入图片描述

2.单点修改

对单点进行修改就是对于单点查询做一点修改,在查询到修改节点值之后,再更新每个祖先节点的值。

//单点修改 
void update(int p,int ind,int val) {
    
    
	if(ind < tree[p].l||ind > tree[p].r ) 
		return;
	else if(tree[p].r == tree[p].l) {
    
    
		tree[p].val = val;
	}
	else {
    
    
		update(2*p+1,ind,val);
		update(2*p+2,ind,val);
		//这一步很重要,一定不能少
		tree[p].val = tree[2*p+1].val + tree[2*p+2].val;
	}
}

在这里插入图片描述

3.区间修改(带pushdown())

区间修改就是某一个区间的值都要进行修改,可能是将某一个区间的所有值都加一操作,如果把每一个参与进去的祖先和子孙节点都进行修改,会超时,这个时候线可以只改动祖先而子节点在要用到的时候再进行修改。用一个lazy标记(延迟标记),在要用到的时候再使用标记进行操作。

//区间修改,此处是将[L,R]所有元素全部加一 
void change(int p,int L,int R) {
    
    
	if(R < tree[p].l||L > tree[p].r ) 
		return;
	if(L <= tree[p].l && tree[p].r <= R) {
    
    
		//完全包含其中,直接改变标记并处理然后返回即可 
		tree[p].val += (tree[p].r - tree[p].l +1);
		tree[p].lazy += 1;
		return;
	}
	//处理延迟标记,因为要用到左右儿子节点了 
	pushdown(p);
	int mid = (tree[p].l + tree[p].r)/2;
	if(L <= mid)	change(2*p+1,L,R);
	if(R >  mid) 	change(2*p+2,L,R);
	tree[p].val = tree[2*p+1].val + tree[2*p+2].val;
}

接下来介绍pushdown()函数,这个函数的作用就是处理延迟标记,对于被标记了的节点,即lazy不为0,则先把标记传递给左右儿子,然后处理左右儿子的内容,最后再将这个标记清零,因为已经处理完成。

void pushdown(int p) {
    
    
	if(tree[p].lazy != 0){
    
    
		tree[2*p+1].lazy += tree[p].lazy;
		tree[2*p+2].lazy += tree[p].lazy;
		int mid = (tree[p].l + tree[p].r)/2;
		tree[2*p+1].val += mid - tree[p].l + 1;
		tree[2*p+2].val += tree[p].r - mid;
		tree[p].lazy = 0;//父节点的标记清零 
	}
}

最后提一点因为区间修改用的是懒标记,这个时候的查询已经不是上面那个查询了,同样是需要用到pushdown()函数的查询函数,但函数大体并没有发生改变。

//查询 
int _query(int p,int L,int R) {
    
    
	//终止条件
	if(tree[p].l>R||tree[p].r<L)
		return 0;
		
	int sum = 0;
	pushdown(p);
	if(tree[p].l>=L&&R>=tree[p].r) {
    
    
		sum += tree[p].val;
	} 		
	else {
    
    	
		int mid  = (L+R)/2;
		sum += _query(2*p+1,L,R);
		sum += _query(2*p+2,L,R);
	}
	return sum;
}

三.特殊的线段树

还有两个比较特特殊的乘法线段树和根号线段树,等以后再补吧。。。

猜你喜欢

转载自blog.csdn.net/weixin_45203704/article/details/107256918