线段树(二)

转自:http://blog.csdn.net/liujian20150808/article/details/51137749node

 

1.线段树的定义:web

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

对于线段树中的每个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。所以线段树是平衡二叉树,最后的子节点数目为N,即整个线段区间的长度。this

 

举例描述:url

所以有了以上对线段树的定义,咱们能够将区间[1,10]的线段树结构描述出来:spa

图片来自于百度百科.net

有了线段[1,10]的线段树结构,咱们能够发现,每一个叶节点对应区间范围内的端点[a,a](1<=a<=10)orm

 

2.构造线段树blog

显然,当咱们将线段树定义清楚以后,那么咱们就要想要怎么去实现它。递归

咱们能够观察上图,对于线段树中的每个非叶子节点[a,b],它的左儿子表示的区间均为[a,(a+b)/2],右儿子表示的区间均为[(a+b)/2+1,b],

所以咱们利用线段树的这一特色,能够递归的将这棵线段树构造出来,递归的终止条件也就是咱们构造到了叶节点,即此时线段的左右区间相等。

有了以上的思路,咱们能够得出如下构造线段树的代码:

 

[cpp] view plain copy
  1. //由区间[left,right]创建一棵线段树  
  2.   
  3. Node *Build(Node *_root,int left,int right){  
  4.     _root = Init(_root,left,right);    //节点初始化  
  5.     if(left != right){            //递归终止条件,表示已经线段树创建到了叶节点  
  6.         _root -> lchild = Build(_root -> lchild,left,(left + right) / 2);  
  7.         _root -> rchild = Build(_root -> rchild,(left + right)/2+1,right);  
  8.     }  
  9.     return _root;        //回溯至最后一层时,则返回整棵树的根节点  
  10. }  


为了检验构造状况是否和上述图示一致,咱们能够利用树的中序遍历,查看每一个节点存储的线段,所以咱们得出如下完整代码:

 

 

[cpp] view plain copy
  1. #include<cstdio>  
  2. #include<cstdlib>  
  3.   
  4. typedef struct node Node;  
  5.   
  6. struct node{  
  7.     int leftvalue;  
  8.     int rightvalue;      //分别用来记录节点记录的区间范围  
  9.     Node *lchild;     //左孩子节点  
  10.     Node *rchild;    //右孩子节点  
  11. };  
  12.   
  13. int flag = 1;      //用于标记当前遍历到哪一个节点  
  14. //节点的初始化  
  15. Node *Init(Node *_node,int lvalue,int rvalue){  
  16.     _node = (Node *)malloc(sizeof(Node));  
  17.     _node -> lchild = NULL;  
  18.     _node -> rchild = NULL;  
  19.     _node -> leftvalue = lvalue;  
  20.     _node -> rightvalue = rvalue;  
  21.     return _node;  
  22. }  
  23.   
  24. //由区间[left,right]创建一棵线段树  
  25.   
  26. Node *Build(Node *_root,int left,int right){  
  27.     _root = Init(_root,left,right);    //节点初始化  
  28.     if(left != right){            //递归终止条件,表示已经线段树创建到了叶节点  
  29.         _root -> lchild = Build(_root -> lchild,left,(left + right) / 2);  
  30.         _root -> rchild = Build(_root -> rchild,(left + right)/2+1,right);  
  31.     }  
  32.     return _root;        //回溯至最后一层时,则返回整棵树的根节点  
  33. }  
  34.   
  35. //中序遍历,便于从左往右查看各节点存储的线段区间  
  36.   
  37. void inorder(Node *_node){  
  38.     if(_node){  
  39.         inorder(_node -> lchild);  
  40.         printf("第%d个遍历的节点,存储区间为:[%d,%d]\n",flag++,_node -> leftvalue,_node -> rightvalue);  
  41.         inorder(_node -> rchild);  
  42.     }  
  43. }  
  44.   
  45. int main(){  
  46.     Node *root;  
  47.     root = Build(root,1,10);  
  48.     inorder(root);  
  49.     return 0;  
  50. }  

 

 

运行结果:

咱们发现,存储的结果与一开始定义的彻底一致,因而咱们便成功的创建好了一棵空的线段树。

 

3.线段树的一些简单应用:

 

(1).区间查询问题:

RMQ(Range Minimum/Maximum Query),即区间最值查询,是指这样一个问题:对于长度为n的数列A,回答若干询问RMQ(A,i,j)(i,j<=n),返回数列A中下标在i,j之间的最小/大值。

咱们以RMQ为例:即在给定区间内查询最小值,假设咱们已经将对应区间的最小值存入了线段树的节点中,那么咱们利用刚刚创建好的线段树来解决这一问题。

若是查询的区间是[1,2],[3,3]这样的区间,那么咱们直接找到对应节点解决这一问题便可。可是若是查询的区间是[1,6],[2,7]这样的区间时,咱们能够发如今线段树中,没法找到这样的节点,

可是呢,咱们能够找到树中哪几个节点可以组成咱们所要求的区间,而后再取这几个区间内的最小值不就解决问题了吗?

所以有了这样的想法,咱们对于任何在合理范围内的查询,均可以找到若干个相连的区间,而后将这若干个区间合并,获得待求的区间。

一般,咱们用来寻找这样的一个区间的简单办法是:

function 在节点v查询区间[l,r]

if v所表示的区间和[l,r]交集不为空集 if v所表示的区间彻底属于[l,r]

选取v 

else

在节点v的左右儿子分别查询区间[l,r]end if end if

end function

伪代码出自《线段树》讲稿 杨戈

所以根据以上伪代码咱们能够得出如下完整代码:

 

[cpp] view plain copy
 
  1. #include<cstdio>  
  2. #include<cstdlib>  
  3.   
  4. typedef struct node Node;  
  5.   
  6. struct node{  
  7.     int leftvalue;  
  8.     int rightvalue;      //分别用来记录节点记录的区间范围  
  9.     Node *lchild;     //左孩子节点  
  10.     Node *rchild;    //右孩子节点  
  11. };  
  12.   
  13. int flag = 1;      //用于标记当前遍历到哪一个节点  
  14. //节点的初始化  
  15. Node *Init(Node *_node,int lvalue,int rvalue){  
  16.     _node = (Node *)malloc(sizeof(Node));  
  17.     _node -> lchild = NULL;  
  18.     _node -> rchild = NULL;  
  19.     _node -> leftvalue = lvalue;  
  20.     _node -> rightvalue = rvalue;  
  21.     return _node;  
  22. }  
  23.   
  24. //由区间[left,right]创建一棵线段树  
  25.   
  26. Node *Build(Node *_root,int left,int right){  
  27.     _root = Init(_root,left,right);    //节点初始化  
  28.     if(left != right){            //递归终止条件,表示已经线段树创建到了叶节点  
  29.         _root -> lchild = Build(_root -> lchild,left,(left + right) / 2);  
  30.         _root -> rchild = Build(_root -> rchild,(left + right)/2+1,right);  
  31.     }  
  32.     return _root;        //回溯至最后一层时,则返回整棵树的根节点  
  33. }  
  34.   
  35. //中序遍历,便于从左往右查看各节点存储的线段区间  
  36.   
  37. void inorder(Node *_node){  
  38.     if(_node){  
  39.         inorder(_node -> lchild);  
  40.         printf("第%d个遍历的节点,存储区间为:[%d,%d]\n",flag++,_node -> leftvalue,_node -> rightvalue);  
  41.         inorder(_node -> rchild);  
  42.     }  
  43. }  
  44.   
  45. /**用于查询一个给定的区间是由线段树中哪几个子区间构成的,为了简化代码, 
  46.  **所以保证输入的区间范围合法,这里就不做输入的合法性判断了 
  47.  */  
  48. void Query(Node *_node,int left,int right){  
  49.     /**要查询一个给定的区间是由线段树中哪几个子区间构成的 
  50.      **首先要知足的是当前遍历到的区间要和待查询区间有交集, 
  51.      **不然的话再也不继续往当前节点的孩子节点继续遍历,缘由很简单 
  52.      **每一个孩子节点存储的区间范围都是包含于父亲节点的,父亲节点与 
  53.      **待查询区间无交集的话,则孩子节点必定无交集 
  54.      **/  
  55.      if(right >= _node -> leftvalue && left <= _node -> rightvalue){  
  56.         /**若是当前遍历到的线段树区间彻底属于待查询区间, 
  57.          **那么选取该区间,不然的话,继续缩小范围, 
  58.          **在当前节点的左孩子和右孩子节点继续寻找 
  59.          **/  
  60.         if(left <= _node -> leftvalue && right >= _node -> rightvalue){  
  61.             printf("[%d,%d]\n",_node -> leftvalue,_node -> rightvalue);  
  62.         }  
  63.         else{  
  64.             Query(_node -> lchild,left,right);  
  65.             Query(_node -> rchild,left,right);  
  66.         }  
  67.      }  
  68.   
  69. }  
  70.   
  71. int main(){  
  72.     Node *root;  
  73.     root = Build(root,1,10);  
  74.     inorder(root);  
  75.     printf("区间[2,7]由线段树中如下区间构成:\n");  
  76.     Query(root,2,7);  
  77.     return 0;  
  78. }  


咱们以区间[2,7]为例,得出如下运行结果:

 

 

(2).区间修改操做:

在这里咱们依然以RMQ问题为例,假如一开始的时候,线段树中每一个节点的权值都是1,那么如今我要作的是,指定一个合法的区间,而后对这个区间内全部的数加上或者减去某个数,若是咱们按照区间的内的数一 一的

去遍历并修改线段树的节点的话,那么改动的节点数必然远远超过O(logn)个,并且会存在大量的重复遍历操做,那么要怎么样才能提升程序的效率呢?

 

首先,咱们考虑给定的修改区间,按照前面咱们讨论过的问题,咱们能够把待操做区间变成几个相连的子区间,那么咱们试想,当咱们要修改一个给定区间时,咱们对其全部子区间进行修改,这样的话不就把整个

待修改区间处理完毕了吗?这样的话咱们是否能够只经过修改几个子区间节点的值,而不考虑它们的孩子节点,就完成全部的操做了呢?

 

实际上,若是不考虑这些子区间的孩子节点的话,是错误的,由于在父亲节点所带的权值发生变化时,好比说上图示中区间[1,2]中每一个值都加上5,那么咱们把线段树中表示区间[1,2]的节点修改完毕是否就能够了呢?

答案显然是错误的,由于该节点的左孩子([1,1])和右孩子节点所表示的区间([2,2])中的值也都发生了变化。

因此在这里咱们为了方便,咱们在节点定义中加入一个标记的量,用来存储对节点的修改状况。显然,当咱们自上而下的访某节点时,父亲节点的标记要"传给"孩子节点,即修改大的区间,其子区间也必然被改动。

 

有了以上的分析,咱们能够总结操做:

首先找到树中哪几个节点表示的区间,可以组成咱们待修改的区间,而后从这些节点开始向下遍历,将以这些节点为根节点的子树节点权值作相应的改变。(边查找对应子区间,边修改权值)

 

完整代码以下:

 

[cpp] view plain copy
 
  1. #include<cstdio>  
  2. #include<cstdlib>  
  3.   
  4. typedef struct node Node;  
  5.   
  6. struct node{  
  7.     int leftvalue;  
  8.     int rightvalue;      //分别用来记录节点记录的区间范围  
  9.     Node *lchild;     //左孩子节点  
  10.     Node *rchild;    //右孩子节点  
  11.     int weight;      //表示节点的权值  
  12.     int mark;        //表示当前节点改变的值(权重加减处理)  
  13. };  
  14.   
  15. int flag = 1;      //用于标记当前遍历到哪一个节点  
  16. //节点的初始化  
  17. Node *Init(Node *_node,int lvalue,int rvalue){  
  18.     _node = (Node *)malloc(sizeof(Node));  
  19.     _node -> lchild = NULL;  
  20.     _node -> rchild = NULL;  
  21.     _node -> leftvalue = lvalue;  
  22.     _node -> rightvalue = rvalue;  
  23.     _node -> weight = 1;          //为了方便,一开始的时候,线段树每一个节点的权值都设为1  
  24.     _node -> mark = 0;            //初始状态时,全部节点的权重均为1,所以一开始的时候,线段树每一个节点的标记均为0  
  25.     return _node;  
  26. }  
  27.   
  28. //由区间[left,right]创建一棵线段树  
  29.   
  30. Node *Build(Node *_root,int left,int right){  
  31.     _root = Init(_root,left,right);    //节点初始化  
  32.     if(left != right){            //递归终止条件,表示已经线段树创建到了叶节点  
  33.         _root -> lchild = Build(_root -> lchild,left,(left + right) / 2);  
  34.         _root -> rchild = Build(_root -> rchild,(left + right)/2+1,right);  
  35.     }  
  36.     return _root;        //回溯至最后一层时,则返回整棵树的根节点  
  37. }  
  38.   
  39. //中序遍历,便于从左往右查看各节点存储的线段区间  
  40.   
  41. void inorder(Node *_node){  
  42.     if(_node){  
  43.         inorder(_node -> lchild);  
  44.         printf("\n第%d个遍历的节点,存储区间为:[%d,%d]\n",flag,_node -> leftvalue,_node -> rightvalue);  
  45.         printf("\n第%d个遍历的节点,权值为%d,标记为%d\n",flag++,_node -> weight,_node -> mark);  
  46.         inorder(_node -> rchild);  
  47.     }  
  48. }  
  49.   
  50. /**用于查询一个给定的区间是由线段树中哪几个子区间构成的,为了简化代码, 
  51.  **所以保证输入的区间范围合法,这里就不做输入的合法性判断了 
  52.  */  
  53. void Query(Node *_node,int left,int right){  
  54.     /**要查询一个给定的区间是由线段树中哪几个子区间构成的 
  55.      **首先要知足的是当前遍历到的区间要和待查询区间有交集, 
  56.      **不然的话再也不继续往当前节点的孩子节点继续遍历,缘由很简单 
  57.      **每一个孩子节点存储的区间范围都是包含于父亲节点的,父亲节点与 
  58.      **待查询区间无交集的话,则孩子节点必定无交集 
  59.      **/  
  60.      if(right >= _node -> leftvalue && left <= _node -> rightvalue){  
  61.         /**若是当前遍历到的线段树区间彻底属于待查询区间, 
  62.          **那么选取该区间,不然的话,继续缩小范围, 
  63.          **在当前节点的左孩子和右孩子节点继续寻找 
  64.          **/  
  65.         if(left <= _node -> leftvalue && right >= _node -> rightvalue){  
  66.             printf("[%d,%d]\n",_node -> leftvalue,_node -> rightvalue);  
  67.         }  
  68.         else{  
  69.             Query(_node -> lchild,left,right);  
  70.             Query(_node -> rchild,left,right);  
  71.         }  
  72.      }  
  73.   
  74. }  
  75.   
  76. /**对指定区间的值进行增添操做,显然,当某个子区间的值进行修改了以后 
  77.  **以该节点为根节点的子树区间的值均要修改 
  78.  **/  
  79.   
  80. void change(Node *node){  
  81.     if(node){  
  82.         if(node -> lchild){  
  83.             node -> lchild -> mark += node -> mark;  
  84.             node -> lchild -> weight += node -> lchild -> mark;  
  85.             change(node -> lchild);  
  86.         }  
  87.         if(node -> rchild){  
  88.             node -> rchild -> mark += node -> mark;  
  89.             node -> rchild -> weight += node -> rchild -> mark;  
  90.             change(node -> rchild);  
  91.         }  
  92.     }  
  93. }  
  94.   
  95. /**更改某个区间的权值,整棵线段树节点值的变化为了简化代码, 
  96.  **所以保证输入的区间范围合法,这里就不做输入的合法性判断,pos表示增减操做的值 
  97.  **/  
  98. void update(Node *node,int left,int right,int pos){  
  99.   
  100.     //先查找待处理区间的组成区间,再修改区间的权值  
  101.     if(right >= node -> leftvalue && left <= node -> rightvalue){  
  102.         if(left <= node -> leftvalue && right >= node -> rightvalue){  
  103.             node -> mark = pos;  
  104.             node -> weight += node -> mark;  
  105.             //修改以该节点为根的子树全部节点的权值和标记  
  106.             change(node);  
  107.         }  
  108.         else{  
  109.             update(node -> lchild,left,right,pos);  
  110.             update(node -> rchild,left,right,pos);  
  111.         }  
  112.      }  
  113. }  
  114.   
  115.   
  116.   
  117. int main(){  
  118.     Node *root;  
  119.     root = Build(root,1,4);  
  120.     //[1,3]中每一个数都加上5;  
  121.     update(root,1,3,5);  
  122.     inorder(root);  
  123.     return 0;  
  124. }  


 

运行结果:

相关文章
相关标签/搜索