线段树(一)
问题:
先抛出一个问题,坐标轴上有若干线段,现在给定若干个点,对于每个点,求出包含点的线段的数量
如果用常规的解法,时间复杂度是O(mn),空间复杂度是O(m + n)
能不能降低一下时间复杂度呢?答案是肯定的,这些线段里有大量相交或者覆盖的线段,而上面的解法显然没有利用这些信息,导致时间复杂度较高,现在我们引入
一个数据结构,线段树(Segment Tree),从Wikipedia抄过来定义如下:
In computer science, a segment tree is a tree data structure for storing intervals, or segments. It allows querying which of the
stored segments contain a given point. It can be implemented as a dynamic structure. A similar data structure is the interval tree.
第一次看我也是一头雾水,什么也没说嘛,下面从给出一个规范化的线段树的定义:
定义1 长度为1的线段称为元线段。 定义2 一棵树被成为线段树,当且仅当这棵树满足如下条件: (1) 该树是一棵二叉树。 (2) 树中每一个结点都对应一条线段[a,b]。 (3) 树中结点是叶子结点当且仅当它所代表的线段是元线段。 (4) 树中非叶子结点都有左右两个子树,左子树树根对应线段[a , (a + b ) / 2],右子树树根对应线段[( a + b ) / 2 + 1, b]
线段树主要有三种操作,建树,插入,查询,先面我们一一看一下这三种操作
A. 建树
给出8条线段,S[1-8],区间分别为[1,3],[2,4],[3,5],[4,6],[7,8],[7,9],[9,10],[8,10],总区间为[1,10],一棵线段树要覆盖区间,因此用区间[1,10]
建立一棵线段树,如上图,可以看出,线段树是一棵二叉平衡树,简单性质如下:
1. 对于非叶子节点:r[a,b](a < b) r的左儿子节点的区间为[a,(a + b) / 2];r的右儿子节点的区间为[(a + b) / 2 + 1, b]
2. 叶子节点对应一个点,也即r[a,b](a = b)
3. 线段树把区间上的任意一条线段都分成不超过2logL条线段(L为总区间的长度)[证明可参考论文《线段树在算法中的应用》]
性质1保证了线段树是一棵平衡二叉树,性质3则能够保证插入,查询操作可在O(logL)的时间复杂度内完成
B. 插入线段
那么建立起这样一棵树后,怎么插入一条线段呢?方法很简单,以插入S8为例,看图:
可以看出,方法很简单:
1. 从根节点r(a,b)开始, 待插线段s(c,d)
1.0 如果a == c && b == d,转到2
1.1 如果d <= (a + b) / 2,以r的左孩子为根,转到1
1.2 如果c > (a + b) / 2,以r的右孩子为根,转到1
1.3 否则搜索r的左孩子和右孩子
2. 节点的值+1, 插入结束
由性质3可以知道,插入线段操作能够在O(logL)次内完成
C. 查询
最后查询某个点的包含次数,就非常简单了,沿着根节点搜索到叶子节点,将一路搜索过程中走过节点的值加和即可
给出代码:
//线段树节点数据结构 typedef struct { int count;//完全包含区间[left, right]次数 int left;//区间开始 int right;//区间结束 } ST;
1 /*建树*/ 2 void build_ST(int i, int left, int right) { 3 st[i].left = left; 4 st[i].right = right; 5 st[i].count = 0; 6 if(left == right) { 7 return; 8 } 9 build_ST(i * 2, left, (left + right) / 2); 10 build_ST(i * 2 + 1, (left + right) / 2 + 1, right); 11 }
1 /*插入线段*/ 2 void insert_ST(int i, int left, int right) { 3 int l = st[i].left; 4 int r = st[i].right; 5 if(l == left && r == right) { //刚好覆盖 6 st[i].count++; 7 } 8 else if(right <= (r + l) / 2) { 9 insert_ST(i * 2, left, right); 10 } 11 else if(left > (r + l) / 2) { 12 insert_ST(i * 2 + 1, left, right); 13 } 14 else { 15 insert_ST(i * 2, left, (r + l) / 2); 16 insert_ST(i * 2 + 1, (r + l) / 2 + 1, right); 17 } 18 }
1 /* 查询 */ 2 int query_ST(int i, int value) { 3 int l = st[i].left; 4 int r = st[i].right; 5 if(l == r) { 6 return st[i].count; 7 } 8 else if(value <= (r + l) / 2) 9 return st[i].count + query_ST(i * 2, value); 10 else 11 return st[i].count + query_ST(i * 2 + 1, value); 12 }
//测试函数 #define N 1024 ST st[N] = { 0 }; int main() { int n;//区间长度 int m;//插入线段数 scanf("%d", &n); scanf("%d", &m); build_ST(1, 1, n); for(int i = 0; i < m; i++) { int segs, sege; scanf("%d %d", &segs, &sege); insert_ST(1, segs, sege); } for(int i = 0; i < n; i++) { printf("%d\n", query_ST(1, i + 1)); } return 0; }
最后看一下复杂度:
操作 | 时间复杂度 | 空间复杂度 |
建树 | O(L) | O(L) |
插入 |
O(logL) |
|
查询 | O(logL) |
由于本人水平有限,文中难免有不当和错误之处,欢迎大家批评指正,愿共同进步!!!