线段树解析

线段树是一种数据结构,用于高效处理区间查询和修改。它通过空间换时间,将区间操作的时间复杂度降低到对数级别。文章介绍了线段树的构造、插入和查询操作,并通过实例解释了其工作原理。线段树可以解决如计算一维空间中建筑受雨量等问题,提供O(n + m) * log(len)的解决方案,优于传统方法。

概念:

线段树是一种特殊的结构,它每个节点记录着一个区间和这个区间的一个计数,表示此区间出现的次数。

线段树分为构造build部分,插入insert部分,以及查询query部分。其主要的思想就是用空间换时间,来使一些特殊的问题的时间复杂度减少。比如对于一段空间或者一个数字的出现次数,以线段树来查询可以使时间复杂度从乘法减少到log,具体的复杂度分析可以看参考资料1:http://blog.csdn.net/bochuan007/article/details/6713971

举个栗子:

根节点为1-7区间的线段树如下所示:


它build的规则是:根节点部分表示的区间是所有数据的[min,max]部分,令mid = (min + max) / 2,左孩子代表的区间是[min,mid],右孩子代表的区间是[mid + 1, max]。

叶子节点代表的是单个数字,即left == right。

那么我们首先定义一个数据结构,表示线段数中节点元素:

struct Element{//元素结构体维护一个计数、一个左边界和右边界
	int count = 0;
	int left;
	int right;
	Element(int vx, int vy) :left(vx), right(vy){}//构造函数
};
然后,我们再对二叉树本身写一个结构,其中包含了上面所述的元素:

struct Node{//线段树的结点,里面有一个元素结构体以及孩子指针
	Element *e;
	Node* lchild;
	Node* rchild;
	Node(Element *ve) : e(ve), lchild(NULL), rchild(NULL){}
};
那么,线段数的build部分的函数如下:

Node* build(int n, int m){//构造树的方法,输入左、右边界和父结点指针
	Element *te = new Element(n, m);//构造元素结构体
	Node *ret = new Node(te);//构造返回结点
	if (n == m){//说明是叶子节点
		return ret;
	}
	else{//递归构造左右孩子节点
		int mid = (n + m) / 2;
		ret->lchild = build(n, mid);
		ret->rchild = build(mid + 1, m);
	}
	return ret;
}
接下来:线段数的insert插入规则如下:对于插入的区间[n,m],如果线段数节点当前表示区间[l,r]刚好覆盖了[n,m],那么当前节点的count添加上insert的计数部分。

否则,让子节点去接收。

注意,只有当当前节点恰好符合时才会接收,否则不会接收。

举个栗子,比如上面的1-7的线段树,插入[1,4]计数为5的元素,先找根节点[1,7],比较后不时恰好符合,所以看是否需要对[1,4]分割,因为[1,4]都是在左孩子的结点部分,所以递归让左孩子处理,左孩子[1,4]碰到[1,4]刚好符合,所以左孩子的count从0增加为5。

处理后线段树如下:(其中红色部分表示各结点的count)



如果再插入一个[2,6],计数为4的结点:

0层递归:根节点[1,7]依旧处理不了,而[2,6]因为横跨了根节点mid=4部分,所以把[2,6]分割成[2,4]和[5,6]两部分分别交于左右孩子处理,

    1层递归:左孩子[1,4]碰到[2,4]部分还是处理不了,它根据自己的mid=3把[2,4]分割成[2,2]和[3,4]部分,交于左右孩子处理

        2层递归:左孩子[1,2]碰到[2,2]还是处理不了,它交于自己的右孩子处理

            3层递归:右孩子[2,2]碰到[2,2]恰好,自己的count更新为4。完毕

        2层递归:右孩子[3,4]碰到[3,4],更好,自己的count更新为4。完毕

    1层递归:右孩子[5,7]碰到[5,6]处理不了,它交于自己的左孩子处理

        2层递归:左孩子[5,6]碰到[5,6]恰好,自己的count更新为4。

所以再经过此步,当前的线段树情况如下:


把代码部分呈上:

void insert(int n, int m,int count, Node *root){//更新,添加记录进去
	if (n <= root->e->left)//规整左边界
		n = root->e->left;
	if (m >= root->e->right)//规整右边界
		m = root->e->right;
	if (n == root->e->left && m == root->e->right){//如果刚好对应左右边界
		root->e->count += count;//当前count更新
		return;
	}
	int mid = (root->e->left + root->e->right) / 2;
	if (n <= mid){//添加到左孩子处
		if (m > mid){
			insert(n, mid, count, root->lchild);
			insert(mid + 1, m, count, root->rchild);
		}
		else if (m <= mid)
			insert(n, m, count, root->lchild);
		return;
	}
	if (m >= mid + 1){//添加到右孩子处
		if (n <= mid){
			insert(n, mid, count, root->lchild);
			insert(mid + 1, m, count, root->rchild);
		}
		else if (n > mid)
			insert(n, m, count, root->rchild);
		return;
	}
}
接下来是查询的部分:

它的规则如下:

对于一个查询的值value,如果不在根节点的区间范围内,返回0。

如果在,则从根节点开始一直找到和此值相同的叶子节点处,返回途径的各个节点的count的和。

如果查询的是一个范围[vstart,vend],如果不在根节点的范围内,返回0。

如果在,则从根节点开始一直找到和此区间相同的非叶子节点处,返回途径的各个节点的count的和。

(其实,查询一个值也相当于一个区间,不过前者是要找到叶子节点处,后者是找到非叶子节点处)

所以,如果是查找3出现的次数,我们需要途径[1,7],[1,4],[3,4],[3,3],把各个节点的count累加,3出现的次数就是9。

如下图:


代码如下:以下只有查找一个值的代码,查找一个区间的类似。

int query(int value, Node* root){//查询某个值出现的次数
	if (value < root->e->left || value > root->e->right){//如果出界,返回0
		return 0;
	}
	if (value == root->e->left && value == root->e->right)//如果到了叶子节点,返回当前值
		return root->e->count;
	int mid = (root->e->left + root->e->right) / 2;
	if (value <= mid){//返回当前节点值和递归左孩子的值的和
		return root->e->count + query(value, root->lchild);
	}
	else if (value > mid){//返回当前节点值和递归右孩子的值的和
		return root->e->count + query(value, root->rchild);
	}
}

说了那么多,那么线段数可以解决什么问题呢?参考资料3中有列出一些例子:http://dongxicheng.org/structure/segment-tree/

我也举个栗子:

有一座城市,经常下雨,我们找了几个标志性建筑,假设它们的位置是一维的,每次下雨都有一个范围和持续时间,

现在给你M个标志性建筑的位置,和N次下雨的范围以及持续时间,让你输出每次每个建筑的所承受的总下雨量。

这个问题当然可以用普通的方法和数据结构解决,但是时间复杂度会很高,为O(n*m)。

可以用线段树,我们用M个位置中的min和max来build一个线段树,然后用每次下雨的范围和持续时间来insert,最后对于标示性建筑的位置来进行query即可。

令len = max - min,空间复杂度是O(2*len),时间复杂度是O(n + m)*log(len)(包含n*log(len)的insert以及m*log(len)的query),当N很大时改进的效率提升还是很大的。

总体代码如下:(相信看过上面的build,insert和query以及图示过后一定很好理解)

#include<stdio.h>
#include<iostream>
#include<vector>
#include<algorithm>
#include<string>
#include<math.h>
#include<climits>
using namespace std;
//------------线段树----------------by-Apie陈小旭---------------
struct Element{//元素结构体维护一个计数、一个左边界和右边界
	int count = 0;
	int left;
	int right;
	Element(int vx, int vy) :left(vx), right(vy){}//构造函数
};
struct Node{//线段树的结点,里面有一个元素结构体以及孩子指针
	Element *e;
	Node* lchild;
	Node* rchild;
	Node(Element *ve) : e(ve), lchild(NULL), rchild(NULL){}
};
Node* build(int n, int m){//构造树的方法,输入左、右边界和父结点指针
	Element *te = new Element(n, m);//构造元素结构体
	Node *ret = new Node(te);//构造返回结点
	if (n == m){//说明是叶子节点
		return ret;
	}
	else{//递归构造左右孩子节点
		int mid = (n + m) / 2;
		ret->lchild = build(n, mid);
		ret->rchild = build(mid + 1, m);
	}
	return ret;
}
void insert(int n, int m,int count, Node *root){//更新,添加记录进去
	if (n <= root->e->left)//规整左边界
		n = root->e->left;
	if (m >= root->e->right)//规整右边界
		m = root->e->right;
	if (n == root->e->left && m == root->e->right){//如果刚好对应左右边界
		root->e->count += count;//当前count更新
		return;
	}
	int mid = (root->e->left + root->e->right) / 2;
	if (n <= mid){//添加到左孩子处
		if (m > mid){
			insert(n, mid, count, root->lchild);
			insert(mid, m, count, root->rchild);
		}
		else if (m <= mid)
			insert(n, m, count, root->lchild);
		return;
	}
	if (m >= mid + 1){//添加到右孩子处
		if (n <= mid){
			insert(n, mid, count, root->lchild);
			insert(mid, m, count, root->rchild);
		}
		else if (n > mid)
			insert(n, m, count, root->rchild);
		return;
	}
}
int query(int value, Node* root){//查询某个值出现的次数
	if (value < root->e->left || value > root->e->right){//如果出界,返回0
		return 0;
	}
	if (value == root->e->left && value == root->e->right)//如果到了叶子节点,返回当前值
		return root->e->count;
	int mid = (root->e->left + root->e->right) / 2;
	if (value <= mid){//返回当前节点值和递归左孩子的值的和
		return root->e->count + query(value, root->lchild);
	}
	else if (value > mid){//返回当前节点值和递归右孩子的值的和
		return root->e->count + query(value, root->rchild);
	}
}
int main(void){
	const int NUM = 2;//记录的次数
	int s = 1, t = 7;//开始和结尾的范围
	int start[NUM]{1, 2};//记录的开始点
	int end[NUM]{4, 6};//记录的结尾点
	int count[NUM]{5, 4};//记录的持续时间
	Node* root = build(s, t);
	for (int i = 0; i < NUM; ++i){
		insert(start[i], end[i], count[i], root);
	}
	vector<int>V{ 1, 3, 5, 7 };//查询的位置集合
	for (int i = 0; i < V.size(); ++i){
		cout << query(V[i], root) << endl;
	}
	return 0;
}



参考资料:

[1] http://blog.csdn.net/bochuan007/article/details/6713971

[2] http://blog.csdn.net/x314542916/article/details/7837276

[3] http://dongxicheng.org/structure/segment-tree/

其中[3]说线段树是完全二叉树,这种说法不对,比如root节点为[1-6]的时候,不是完全二叉树,如下图:


——Apie陈小旭

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值