线段树解析
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。
那么我们首先定义一个数据结构,表示线段数中节点元素:
否则,让子节点去接收。
注意,只有当当前节点恰好符合时才会接收,否则不会接收。
举个栗子,比如上面的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。
所以再经过此步,当前的线段树情况如下:
把代码部分呈上:
它的规则如下:
对于一个查询的值value,如果不在根节点的区间范围内,返回0。
如果在,则从根节点开始一直找到和此值相同的叶子节点处,返回途径的各个节点的count的和。
如果查询的是一个范围[vstart,vend],如果不在根节点的范围内,返回0。
如果在,则从根节点开始一直找到和此区间相同的非叶子节点处,返回途径的各个节点的count的和。
(其实,查询一个值也相当于一个区间,不过前者是要找到叶子节点处,后者是找到非叶子节点处)
所以,如果是查找3出现的次数,我们需要途径[1,7],[1,4],[3,4],[3,3],把各个节点的count累加,3出现的次数就是9。
如下图:
代码如下:以下只有查找一个值的代码,查找一个区间的类似。
说了那么多,那么线段数可以解决什么问题呢?参考资料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以及图示过后一定很好理解)
参考资料:
[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陈小旭
线段树是一种特殊的结构,它每个节点记录着一个区间和这个区间的一个计数,表示此区间出现的次数。
线段树分为构造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陈小旭
相关文章推荐
- Android下Xml解析技术(三)、pull解析Xml文件
- iOS 静态库开发
- sqlplus回退键设置 rlwrap
- JDBC原理
- hdu 5491 The Next
- ACRush 楼天成回忆录
- Android下Xml解析技术(二)、DOM解析Xml文件
- CSS3
- VS2010调试之“编辑并继续”
- CSS background-position 属性
- String类的不可变性
- CButtonST类简介
- css基础2
- 总结几种应用于WPF的Chart插件
- Android下Xml解析技术(一)、SAX解析Xml文件
- css
- 利用viewpager实现页面的滑动切换
- guava总结
- 电脑稳定性检测软件
- 最新一键修改手机MAC地址和路由器wifi物理地址