概念:
线段树是一种特殊的结构,它每个节点记录着一个区间和这个区间的一个计数,表示此区间出现的次数。
线段树分为构造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陈小旭
线段树是一种数据结构,用于高效处理区间查询和修改。它通过空间换时间,将区间操作的时间复杂度降低到对数级别。文章介绍了线段树的构造、插入和查询操作,并通过实例解释了其工作原理。线段树可以解决如计算一维空间中建筑受雨量等问题,提供O(n + m) * log(len)的解决方案,优于传统方法。
3万+

被折叠的 条评论
为什么被折叠?



