线段树

什么是线段树

线段树(英语:Segment tree)是一种二叉树形数据结构,1977年由Jon Louis Bentley发明[1],用以存储区间线段,并且允许快速查询结构内包含某一点的所有区间。

一个包含n个区间的线段树,空间复杂度为O(n),查询的时间复杂度则为O(logn+k)},其中k是匹配条件的区间数量。

此数据结构亦可推广到高维度。

摘自《维基百科》

为什么要使用线段树

image.png

线段树的时间复杂度分析:

image.png

线段树基础表示

image.png

如果区间有n个元素,用数组表示线段树,需要多少结点?

0层:1

1层:2

2层:4

3层:8

h-1层:2^(h-1)

对于满二叉树:

h层,一共有2h)

最后一层(h-1),有2^(h-1)个结点

最后一层的结点数大致等于前面所有层的结点数之和

image.png

**由此,可得出结论,我们线段树不考虑添加元素,即区间固定,使用4n的静态空间即可。

  1. public class SegmentTree<E> {
  2. private E[] tree;
  3. private E[] data;
  4. public SegmentTree(E[] arr){
  5. data=(E[])new Object[arr.length];
  6. for(int i=0;i<arr.length;i++){
  7. data[i]=arr[i];
  8. }
  9. tree=(E[])new Object[4*arr.length];
  10. }
  11. public int getSize(){
  12. return data.length;
  13. }
  14. public E get(int index){
  15. if(index<0 || index>=data.length){
  16. throw new IllegalArgumentException("Index is illegal");
  17. }
  18. return data[index];
  19. }
  20. //返回完全二叉树的数组表示,一个索引所表示的元素的左孩子结点的索引
  21. public int leftChild(int index){
  22. return 2*index+1;
  23. }
  24. //返回完全二叉树的数组表示,一个索引所表示的元素的右孩子结点的索引
  25. public int rightChild(int index){
  26. return 2*index+2;
  27. }
  28. }

创建线段树

  1. public class SegmentTree<E> {
  2. private E[] tree; //存储线段树中数据
  3. private E[] data;
  4. private Merger<E> merger;
  5. public SegmentTree(E[] arr, Merger<E> merger){
  6. this.merger=merger;
  7. data=(E[])new Object[arr.length];
  8. for(int i=0;i<arr.length;i++){
  9. data[i]=arr[i];
  10. }
  11. tree=(E[])new Object[4*arr.length];
  12. buildSegmentTree(0,0,data.length-1);
  13. }
  14. //在treeIndex根节点的位置创建区间在[l,r]的线段树
  15. private void buildSegmentTree(int treeIndex,int l,int r){
  16. if(l==r){
  17. tree[treeIndex]=data[l];
  18. return;
  19. }
  20. int leftTreeIndex=leftChild(treeIndex);
  21. int rightTreeIndex=rightChild(treeIndex);
  22. int mid=l+(r-l)/2;
  23. //创建左子树的线段树
  24. buildSegmentTree(leftTreeIndex,l,mid);
  25. //创建右子树的线段树
  26. buildSegmentTree(rightTreeIndex,mid+1,r);
  27. //当左右子树创建完成之后,综合左右子树的结果,
  28. //如何去综合是由业务逻辑去决定的。
  29. tree[treeIndex]=merger.meger(tree[leftTreeIndex],tree[rightTreeIndex]);
  30. }
  31. public int getSize(){
  32. return data.length;
  33. }
  34. public E get(int index){
  35. if(index<0 || index>=data.length){
  36. throw new IllegalArgumentException("Index is illegal");
  37. }
  38. return data[index];
  39. }
  40. //在完全二叉树的数组表示中,获取左孩子节点的索引
  41. private int leftChild(int index){
  42. return 2*index+1;
  43. }
  44. //在完全二叉树的数组表示中,获取右孩子节点的索引
  45. private int rightChild(int index){
  46. return 2*index+2;
  47. }
  48. @Override
  49. public String toString() {
  50. StringBuilder res=new StringBuilder();
  51. res.append("[");
  52. for(int i=0;i<tree.length;i++){
  53. if(tree[i]!=null){
  54. res.append(tree[i]);
  55. }else{
  56. res.append("null");
  57. }
  58. if(i!=tree.length-1){
  59. res.append(", ");
  60. }
  61. }
  62. res.append("]");
  63. return res.toString();
  64. }
  65. }
  • 线段树的合并器接口
    融合器,根据业务逻辑来融合左右子树的内容
  1. public interface Merger<E> {
  2. E meger(E a,E b);
  3. }

线段树的查询

  1. //查询区间[queryL,queryR]的值
  2. public E query(int queryL, int queryR){
  3. if(queryL<0 || queryL>=data.length
  4. || queryR<0 || queryR>=data.length || queryL>queryR){
  5. throw new IllegalArgumentException("Index is illegal");
  6. }
  7. return query(0,0,data.length-1,queryL,queryR);
  8. }
  9. //以treeIndex为根的线段树[l...r]的范围搜索[queryL,queryR]区间
  10. private E query(int treeIndex,int l,int r,int queryL,int queryR){
  11. //搜索到目标区间
  12. if(l==queryL && r==queryR){
  13. return tree[treeIndex];
  14. }
  15. int mid=l+(r-l)/2;
  16. int leftTreeIndex=leftChild(treeIndex);
  17. int rightTreeIndex=rightChild(treeIndex);
  18. if(queryL>=mid+1){
  19. //仅在右部分
  20. return query(rightTreeIndex,mid+1,r,queryL,queryR);
  21. }else if(queryR<=mid){
  22. //仅在左部分
  23. return query(leftTreeIndex,l,mid,queryL,queryR);
  24. }
  25. //剩下的情况是:有部分落在左区间,另一部分落在右区间
  26. //在左子树中寻找
  27. E leftResult=query(leftTreeIndex,l,mid,queryL,mid);
  28. //在右子树中寻找
  29. E rightResult=query(rightTreeIndex,mid+1,r,mid+1,queryR);
  30. return merger.merge(leftResult,rightResult);
  31. }

线段树的更新

  1. //更新index位置的值
  2. public void set(int index,E e){
  3. if(index<0 || index>data.length){
  4. throw new IllegalArgumentException("Index is illegal");
  5. }
  6. set(0,0,data.length-1,index,e);
  7. }
  8. //更新以treeIndex为根的线段树[l...r]的值为e
  9. private void set(int treeIndex,int l,int r,int index,E e){
  10. //搜索到目标,直接更新
  11. if(l==r){
  12. tree[treeIndex]=e;
  13. return;
  14. }
  15. int mid=l+(r-l)/2;
  16. int leftTreeIndex=leftChild(treeIndex);
  17. int rightTreeIndex=rightChild(treeIndex);
  18. if(index>=mid+1){
  19. //继续到右子树更新
  20. set(rightTreeIndex,mid+1,r,index,e);
  21. }else if(index<=mid){
  22. //继续到左子树更新
  23. set(leftTreeIndex,l,mid,index,e);
  24. }
  25. //更新祖辈节点
  26. tree[treeIndex]=merger.merge(tree[leftTreeIndex],tree[rightTreeIndex]);
  27. }

LeetCode中有关线段树的问题

LeetCode 303题 区域和检索 - 数组不可变

  1. public class NumArray {
  2. private SegmentTree<Integer> segmentTree;
  3. public NumArray(int[] nums) {
  4. if(nums.length>0){
  5. Integer[] data=new Integer[nums.length];
  6. for(int i=0;i<nums.length;i++){
  7. data[i]=nums[i];
  8. }
  9. segmentTree=new SegmentTree<Integer>(data,(a,b)-> a+b);
  10. }
  11. }
  12. public int sumRange(int i, int j) {
  13. if(segmentTree==null){
  14. throw new IllegalArgumentException("Segment Tree is null");
  15. }
  16. return segmentTree.query(i,j);
  17. }
  18. }

LeetCode 307题 区域和检索 - 数组可修改