线段树解析

来源:互联网 发布:淘宝sma特卖什么意思 编辑:程序博客网 时间:2024/06/06 01:39

概念:

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

线段树分为构造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){//如果出界,返回0return 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){//如果出界,返回0return 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陈小旭

0 0
原创粉丝点击