首页 > 其他 > 详细

线段树从零开始

时间:2021-05-05 17:25:24      阅读:40      评论:0      收藏:0      [点我收藏+]

前言

线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN)。而未优化的空间复杂度为2N,实际应用时一般还要开4N的数组以免越界,因此有时需要离散化让空间压缩。

技术分享图片

一:为什么需要线段树?

题目一:
10000个正整数,编号1到10000,用A[1],A[2],A[10000]表示。
修改:无
统计:1.编号从L到R的所有数之和为多少? 其中1<= L <= R <= 10000.

 

方法一:对于统计L,R ,需要求下标从L到R的所有数的和,从L到R的所有下标记做[L..R],问题就是对A[L..R]进行求和。
这样求和,对于每个询问,需要将(R-L+1)个数相加。

方法二:更快的方法是求前缀和,令 S[0]=0, S[k]=A[1..k] ,那么,A[L..R]的和就等于S[R]-S[L-1],
这样,对于每个询问,就只需要做一次减法,大大提高效率。

题目二:
10000个正整数,编号从1到10000,用A[1],A[2],A[10000]表示。
修改:1.将第L个数增加C (1 <= L <= 10000)
统计:1.编号从L到R的所有数之和为多少? 其中1<= L <= R <= 10000.

再使用方法二的话,假如A[L]+=C之后,S[L],S[L+1],,S[R]都需要增加C,全部都要修改,见下表。

 技术分享图片

从上表可以看出,方法一修改快,求和慢。 方法二求和快,修改慢。
那有没有一种结构,修改和求和都比较快呢?答案当然是线段树。
 

二:线段树的点修改

上面的问题二就是典型的线段树点修改。线段树先将区间[1..10000]分成不超过4*10000个子区间,对于每个子区间,记录一段连续数字的和。之后,任意给定区间[L,R],线段树在上述子区间中选择约2*log2(R-L+1)个拼成区间[L,R]。如果A[L]+=C ,线段树的子区间中,约有log2(10000)个包含了L,所以需要修改log2(10000)个。于是,使用线段树的话,A[L]+=C 需要修改log2(10000) 个元素求和A[L...R]需要修改2*log2(R-L+1) <= 2 * log2(10000) 个元素。log2(10000) < 14 所以相对来说线段树的修改和求和都比较快。

 

三、存储结构是怎样的?

线段树是一种二叉树,当然可以像一般的树那样写成结构体,指针什么的。但是它的优点是,它也可以用数组来实现树形结构,可以大大简化代码。数组形式适合在编程竞赛中使用,在已经知道线段树的最大规模的情况下,直接开足够空间的数组,然后在上面建立线段树。简单的记法: 足够的空间 = 数组大小n的四倍。实际上足够的空间 =  (n向上扩充到最近的2的某个次方)的两倍。举例子:假设数组长度为5,就需要5先扩充成8,8*2=16.线段树需要16个元素。如果数组元素为8,那么也需要16个元素。所以线段树需要的空间是n的两倍到四倍之间的某个数,一般就开4*n的空间就好,如果空间不够,可以自己算好最大值来省点空间。

怎么用数组来表示一颗二叉树呢?假设某个节点的编号为v,那么它的左子节点编号为2*v,右子节点编号为2*v+1。然后规定根节点为1.这样一颗二叉树就构造完成了。通常2*v在代码中写成 v<<1 。 2*v+1写成 v<<1|1 。

 

四、代码如何实现?

(1)建树:

 1 /**
 2      * 构造线段树
 3      * @param arr   存储1~n的连续正整数的数组
 4      * @param tree  用于存储构成的线段树的数组(利用满二叉树的形式顺序存储)
 5      * @param root  根节点编号(编号从1开始)
 6      * @param start 左区间(同上)
 7      * @param end   右区间(同上)
 8      */
 9     public static void buildSegTree(int[] arr, int[] tree, int root, int start, int end) {
10         if (start >= end) {
11             tree[root - 1] = arr[start - 1];
12             return;
13         }
14 
15         int mid = (start + end) / 2;
16         // 左孩子
17         int left_root  = root * 2;
18         // 右孩子
19         int right_root = root * 2 + 1;
20         // 递归
21         buildSegTree(arr, tree, left_root, start, mid);
22         buildSegTree(arr, tree, right_root, mid + 1, end);
23         // 更新
24         tree[root - 1] = tree[left_root - 1] + tree[right_root - 1];
25     }

(2)点修改:

 1 /**
 2      * 修改线段树
 3      * 时间复杂度:O(log(n))
 4      * @param arr   存储1~n的连续正整数的数组
 5      * @param tree  用于存储构成的线段树的数组(利用满二叉树的形式顺序存储)
 6      * @param root  根节点编号(编号从1开始)
 7      * @param start 左区间(同上)
 8      * @param end   右区间(同上)
 9      * @param pos   要修改的位置
10      * @param val   要修改后的值
11      */
12     public static void updateSegTree(int[] arr, int[] tree, int root, int start, int end, int pos, int val) {
13         if (start >= end) {
14             if (start == pos) {
15                 arr[start - 1] = val;
16                 tree[root - 1] = val;
17             }
18             return;
19         }
20 
21         int mid = (start + end) / 2;
22         int left_root  = root * 2;
23         int right_root = root * 2 + 1;
24         updateSegTree(arr, tree, left_root, start, mid, pos, val);
25         updateSegTree(arr, tree, right_root, mid + 1, end, pos, val);
26         // 更新
27         tree[root - 1] = tree[left_root - 1] + tree[right_root - 1];
28     }

(3)区间查询(本题为求和):

 1  /**
 2      * 查询线段树中区间[L, R]
 3      * 时间复杂度:O(log(n))
 4      * @param arr   存储1~n的连续正整数的数组
 5      * @param tree  用于存储构成的线段树的数组(利用满二叉树的形式顺序存储)
 6      * @param root  根节点编号(编号从1开始)
 7      * @param start 左区间(同上)
 8      * @param end   右区间(同上)
 9      * @param L     区间左边界
10      * @param R     区间右边界
11      */
12     public static int querySegTree(int[] arr, int[] tree, int root, int start, int end, int L, int R) {
13         // 处理三种特殊情况
14         if (R < start || L > end) {
15             // 不相交
16             return 0;
17         }
18         if (L <= start && R >= end) {
19             // 包含
20             return tree[root - 1];
21         }
22         if (start >= end) {
23             // 只有一个元素,直接返回
24             return arr[start - 1];
25         }
26 
27         int mid = (start + end) / 2;
28         int left_root  = root * 2;
29         int right_root = root * 2 + 1;
30         int leftSum  = querySegTree(arr, tree, left_root, start, mid, L, R);
31         int rightSum = querySegTree(arr, tree, right_root, mid + 1, end, L, R);
32         return leftSum + rightSum;
33     }

完整代码+测试

  1 package com.lzp.util;
  2 
  3 import java.util.Arrays;
  4 
  5 /**
  6  * @Author LZP
  7  * @Date 2021/5/5 10:04
  8  * @Version 1.0
  9  *
 10  * 线段树
 11  */
 12 public class SegmentTree {
 13 
 14     /**
 15      * 构造线段树
 16      * @param arr   存储1~n的连续正整数的数组
 17      * @param tree  用于存储构成的线段树的数组(利用满二叉树的形式顺序存储)
 18      * @param root  根节点编号(编号从1开始)
 19      * @param start 左区间(同上)
 20      * @param end   右区间(同上)
 21      */
 22     public static void buildSegTree(int[] arr, int[] tree, int root, int start, int end) {
 23         if (start >= end) {
 24             tree[root - 1] = arr[start - 1];
 25             return;
 26         }
 27 
 28         int mid = (start + end) / 2;
 29         // 左孩子
 30         int left_root  = root * 2;
 31         // 右孩子
 32         int right_root = root * 2 + 1;
 33         // 递归
 34         buildSegTree(arr, tree, left_root, start, mid);
 35         buildSegTree(arr, tree, right_root, mid + 1, end);
 36         // 更新
 37         tree[root - 1] = tree[left_root - 1] + tree[right_root - 1];
 38     }
 39 
 40     /**
 41      * 修改线段树
 42      * 时间复杂度:O(log(n))
 43      * @param arr   存储1~n的连续正整数的数组
 44      * @param tree  用于存储构成的线段树的数组(利用满二叉树的形式顺序存储)
 45      * @param root  根节点编号(编号从1开始)
 46      * @param start 左区间(同上)
 47      * @param end   右区间(同上)
 48      * @param pos   要修改的位置
 49      * @param val   要修改后的值
 50      */
 51     public static void updateSegTree(int[] arr, int[] tree, int root, int start, int end, int pos, int val) {
 52         if (start >= end) {
 53             if (start == pos) {
 54                 arr[start - 1] = val;
 55                 tree[root - 1] = val;
 56             }
 57             return;
 58         }
 59 
 60         int mid = (start + end) / 2;
 61         int left_root  = root * 2;
 62         int right_root = root * 2 + 1;
 63         updateSegTree(arr, tree, left_root, start, mid, pos, val);
 64         updateSegTree(arr, tree, right_root, mid + 1, end, pos, val);
 65         // 更新
 66         tree[root - 1] = tree[left_root - 1] + tree[right_root - 1];
 67     }
 68 
 69     /**
 70      * 查询线段树中区间[L, R]
 71      * 时间复杂度:O(log(n))
 72      * @param arr   存储1~n的连续正整数的数组
 73      * @param tree  用于存储构成的线段树的数组(利用满二叉树的形式顺序存储)
 74      * @param root  根节点编号(编号从1开始)
 75      * @param start 左区间(同上)
 76      * @param end   右区间(同上)
 77      * @param L     区间左边界
 78      * @param R     区间右边界
 79      */
 80     public static int querySegTree(int[] arr, int[] tree, int root, int start, int end, int L, int R) {
 81         // 处理三种特殊情况
 82         if (R < start || L > end) {
 83             // 不相交
 84             return 0;
 85         }
 86         if (L <= start && R >= end) {
 87             // 包含
 88             return tree[root - 1];
 89         }
 90         if (start >= end) {
 91             // 只有一个元素,直接返回
 92             return arr[start - 1];
 93         }
 94 
 95         int mid = (start + end) / 2;
 96         int left_root  = root * 2;
 97         int right_root = root * 2 + 1;
 98         int leftSum  = querySegTree(arr, tree, left_root, start, mid, L, R);
 99         int rightSum = querySegTree(arr, tree, right_root, mid + 1, end, L, R);
100         return leftSum + rightSum;
101     }
102 
103     public static void main(String[] args) {
104         int n = 8;
105         int[] arr = new int[n];
106         for (int i = 0; i < n; i++) {
107             arr[i] = i + 1;
108         }
109         // 线段树
110         // 线段树先将区间[1..n]分成不超过4*n个子区间,对于每个子区间,记录一段连续数字的和。
111         int[] tree = new int[4 * n + 10];
112         buildSegTree(arr, tree, 1, 1, n);
113         System.out.println("建树之后:" + Arrays.toString(tree));
114 
115         updateSegTree(arr, tree, 1, 1, n, 4, 5);
116         System.out.println("修改之后:" + Arrays.toString(tree));
117 
118         // 原本2~7之和为27,但是由于前面已经修改过(把4改成5),所以区间[2, 7]之和为28
119         int left = 2;
120         int right = 7;
121         int result = querySegTree(arr, tree, 1, 1, n, left, right);
122         System.out.printf("区间[%d, %d] = %d\n", left, right, result);
123     }
124 }

测试结果

技术分享图片

最后

线段树还有更多的用法,比如区间修改,扫描线,非递归写法等,详见原文博主这篇文章: 线段树详解
原文链接:https://blog.csdn.net/zearot/article/details/52280189

线段树从零开始

原文:https://www.cnblogs.com/pengsay/p/14731282.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!