您的位置:首页 > 其它

线段树解析

2015-09-27 23:01 453 查看
概念:

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

线段树分为构造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陈小旭
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: