线段树(一)

问题:

先抛出一个问题,坐标轴上有若干线段,现在给定若干个点,对于每个点,求出包含点的线段的数量

如果用常规的解法,时间复杂度是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)  

 

 

 

 

 

 

由于本人水平有限,文中难免有不当和错误之处,欢迎大家批评指正,愿共同进步!!!

 

你可能感兴趣的:(线段树)