适合入门的线段树基础

线段树其实不难,如果讲解时能用到生动形象的例子,那么谁都可以快速入门线段树,这里我费劲心血查遍了网上大部分关于线段树的博客,通过自己感悟总结,呕心沥血写下这篇博客~

来年的今天我就可以轻松让我的学弟学妹快速线段树了~哈哈



1.综述

什么是线段树?

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

为什么要用到这种数据结构?

请你考虑一下这个问题
有一个数组 arr[10000000],我需要实现下面两个操作
1.区间查询:求下标从L–R区间内的元素的和是多少。
2.单点更新:实现更新某一下标对应元素的值。
以上两个步骤会交叉进行多次。

那么以我们现有的知识,会对上面问题进行这样的操作

  1. 区间查询:用循环计算L–R区间内的元素累加和。时间复杂度O(n)。
  2. 单点更新:只需要对应arr[idx] = val(要修改的值)即可。时间复杂度O(1)

那么有什么办法可以降低区间查询的时间复杂度呢?

思考一下
我们可以新建一个前缀和数组sum[n],来记录arr[0]-arr[n]的和,当然这个操作很简单

这样的话,区间查询操作就变成了sum[R] - sum[L-1]了,表示的是L到R区间内的元素和,看起来是变成了O(1)。

但是这个时候如果我们需要更新某个下标对应的值,其后面所有的前缀和也需要发生改变

我们发现,单点更新时间复杂度又变成了O(n)!!!

看来,鱼和熊掌不可兼得,不能将两者都维持到一个很低的复杂度上。那么,我想知道有没有一种方法,可以将整体的时间复杂度维持到一个比线性更快的水平上呢?
当然,答案就是线段树~
线段树查询和更新操作可以将复杂度降到O(logn),是不是感觉快多了呢,下面就让我好好说一下“线段树”为何这么香!

使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN)。而未优化的空间复杂度为2N,实际应用时一般还要开4N的数组以免越界,因此有时需要离散化让空间压缩。

2.基本原理

线段树基本思想:二分

我们已经知道了,线段是是一颗二叉搜索树。二叉树我们知道,从根节点开始下面每个节点都会分出来两个儿子节点,也就是说根节点是它下面所有节点的老大,而下面每个节点也是从他分出来所有节点的老大。

线段树的每个节点都管理者一段区间,也就是他下面的儿子,而每个节点又存储着他两个儿子节点的和,这下你是不是明白了呢?如果要查询arr数组一段区间的和,我们可以通过查询构建的线段树,找到管理这段区间的父节点,他存储的值就是我们需要求的和。
线段树的结构应该是这样的
假设我们有一个arr[]数组,从a[1]开始
在这里插入图片描述

图中的二叉树根节点从1开始

观察这棵线段树我们发现,arr数组是按照下标顺序存进这颗树的叶节点的,

根节点管理所有节点所以管理区间是 1-13 ,他存储的是所有叶节点也就是arr数组下标1 - 13的和。

根节点的左儿子管理左半部分的节点所以管理区间是1 - 7,他存储的是数组下标从1 - 7的和

根节点的右儿子管理右半部分的节点,管理区间是8-13,他存储的是下标8-13的和。

每个节点分为左右两个儿子节点,他们把父亲管理的节点分成两半,一人一半分别管理,互不干扰很是和谐。直到叶节点,叶节点管理的左右区间左边界和右边界相等,所以到他为止,他没有儿子了。没办法生了

说道这里你有没有明白建线段树的原理了呢?

2.1构建线段树

根据二叉树的结构特性,若每个父亲节点的编号i,他的左右孩子的编号分别是2i和2i+1

根节点从1开始管理的是数组arr 1-n这个区间的和,有了儿子之后,把这个区间分成两半,左半边给左儿子管,右半边给右儿子管,这样一直分下去,直到分不动了,到达叶子节点,他管理的是一个点的值,也存储的就是arr里这个点下标的值,到这里为止开始向上延伸,因为每个节点存储的都是他两个儿子节点的和,每个节点都递归下去,直到叶子节点,开始返回

const int  maxn = 13;
int arr[maxn];
struct p
{
   
   
//l、r表示管理的左右区间边界,mid是区间的中点,w是管理区间的宽度,
//ans是这段区间的综合,flag作为标记用,下面会讲为什么需要标记
   int l,r,mid,w,ans,flag;
}tree[4*maxn];  
//通常情况下arr数组有n个元素时,我们建的树数组会在2n-4n之间,保险起见会开四倍原数据空间       
void push_up(int node)//向上更新,父亲节点存的值是两个儿子节点存的值的和
{
   
   
	tree[node].ans = tree[node<<1].ans + tree[node<<1|1].ans;
}
void bulid_tree(int l,int r,int node) //l区间左边界r是右边界,node树到达的这个点
{
   
   
	tree[node] = {
   
   l,r,(r+l)>>1,r-l+1,0,0};//先初始化一下
	if(l==r)//如果到达了叶子节点,我们赋一下值就可以返回了
	{
   
   
		tree[node].ans = arr[l];
		return;
	}
	bulid_tree(l,tree[node].mid,node<<1);//建左儿子树
	bulid_tree(tree[node].mid+1,r,node<<1|1);//建右儿子树
	push_up(node);//每建一对左右儿子树就可以把值向上延伸,
}
int main()
{
   
   
	for (int i=1;i<=13;i++)
		arr[i] = i;
	bulid_tree(1,13,1);
	for(int i=1;i<=4*13;i++)
	cout<<tree[i].ans<<' ';
	cout<<endl;
	return 0;
}

建树结果为

91 28 63 10 18 27 36 3 7 11 7 17 10 23 13 1 2 3 4 5 6 0 0 8 9 0 0 11 12 0 0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 0 0

一般情况下空节点我们会用0进行填充,我早在初始化的时候就已经填充了
这里大家可以查一下不用结构体数组,如何写线段树,思考一下结构体有什么好处呢?

在这里插入图片描述
我没有算完,但是已经可以观察出这个树的样子了,你学会了吗?

3.实现和基本操作

现在,我们已经把线段树构建好了,就存放在一个结构体数组中
那么,我们现在想要实现一个操作——单点更新,我要把arr[idx]的值赋为val

3.1单点更新

假设我们要把arr[5]=7,在这颗线段树中,每层只有一个节点包含[5]这个点,我们可以通过判断,每次都选择包含[5]这个点的分支,最后我们肯定可以走到[5]这个叶节点

修改叶节点的值之后,在回溯的过程中,把修改的值向上延伸,也就是上面的push_up操作,就可以把更新过的值传递上去

搜索路径是先到底,底部更新,然后返回时每层更新。

通过这个思路,我们写出来代码

void update(int idx,int val,int node)  //idx为下标,val为修改后的值,node是当前节点
{
   
   
	//如果当前节点管理的左边界和右边界相等,说明到达了目的叶子节点
	if(tree[node].l==tree[node].r)
	{
   
   
		tree[node].ans = val;
		arr[tree[node].l] = val;
		return;
	}
	//这一步判断只进入包含目标的节点
	if(idx<=tree[node].mid)  update(idx,val,node<<1);
	else                     update(idx,val,node<<1|1);
	//回溯向上更新,因为一直递归到叶子节点,才会更新,然后回溯的时候会从下面一层一层的更新上来
	push_up(node);
}

我利用之前建好的树,又添加了如下几行

	update(5,7,
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值