数据结构之实现底层SegmentTree

SegmentTree叫线段树,也叫区间树。在某些情况下,只关注某一段区间的数据情况,比如经典的区间染色问题。查询和更新某区间的信息使用数组实现时间复杂度都是O(n),用线段树实现查询和更新时间复杂度为O(log n),线段树更新数组中一个值或者一个区间的值,查询一个区间的最大值、最小值或者是求和等操作。线段树是主要是将传入的数组形成树结构记录区间信息,如下图所示结构

 对于元素个数不是2 ^ 2,其结构是如下图所示,不停的二分区间

对于满二叉树有h层总共有2 ^ h - 1个结点, 每一层节点数为 2 ^ (h -1) 。如果数组数据容量为n = 2 ^ k , 那么对于相应的线段树容量为2n空间,最坏情况 数组n = 2 ^ k + 1,需要开辟的空间为4n。线段树不考虑添加元素,区间固定,使用4n的静态空间即可,用这4n空间的数组表示树结构。其中每个空间的内容定义了一个Merger融合器接口用于自定义数据融合方式,比如求和等操作。

1、Merger接口

public interface Merger<E> {
 E merge(E a, E b); //融合方法,需要自定义实现
}

2、SegmentTree类

public class SegmentTree<E> {
 private E[] tree; //声明树数组,盛放区间数据
 private E[] data; //数据数组,盛放原始数据
 private Merger<E> merger;

 //有参构造函数,用输入数组初始化数据数组,初始化融合器,分配树数组空间
 public SegmentTree(E[] arr, Merger<E> merger) {
  this.merger = merger;
  data = (E[]) new Object[arr.length];
  for (int i = 0; i < arr.length; i++) {
   data[i] = arr[i];
  }
  tree = (E[]) new Object[4 * arr.length];
  buildSegmentTree(0, 0, data.length - 1);
 }

 //构建线段树函数,传入树索引位置,区间左右位置,整个过程采取递归思想
 private void buildSegmentTree(int treeIndex, int l, int r) {
  //当区间左右位置相等则递归宗旨,为树数组相应叶结点位置赋值
  if (l == r) {
   tree[treeIndex] = data[l];
   return;
  }
  int leftTreeIndex = leftChild(treeIndex);  //寻找左孩子索引
  int rightTreeIndex = rightChild(treeIndex);  //寻找右孩子索引

  int mid = l + (r - l) / 2;      //寻找中间点位置
  buildSegmentTree(leftTreeIndex, l, mid);  //构建左区间
  buildSegmentTree(rightTreeIndex, mid + 1, r); //构建右区间
  //利用融合器融合函数融合区间的数据形成区间数据
  tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]);
 }

 public int getSize() { //获取数据数组大小
  return data.length;
 }

 public E get(int index) { //根据索引获取数据数组的值
  if (index < 0 || index >= data.length)
   throw new IllegalArgumentException("Index is illegal.");
  return data[index];
 }

 private int leftChild(int index) { //根据索引获取左孩子索引
  return 2 * index + 1;
 }

 private int rightChild(int index) { //根据索引获取右孩子索引
  return 2 * index + 2;
 }

 //查询某一个区间信息
 public E query(int queryL, int queryR) {
  //判断索引合法性
  if (queryL < 0 || queryL >= data.length || queryR < 0 || queryR >= data.length || queryL > queryR)
   throw new IllegalArgumentException("Index is illegal.");
  //调用私有递归查询函数
  return query(0, 0, data.length - 1, queryL, queryR);
 }
 //根据树数组索引, 区间左右位置标定点, 需要查找区间位置来查询区间信息
 private E query(int treeIndex, int l, int r, int queryL, int queryR) {
  //递归终止条, 左区间等于左查找位置,右区间等于右查找位置,将树中数据返回给上一层
  if (l == queryL && r == queryR)
   return tree[treeIndex];

  int mid = l + (r - l) / 2; //求取区间中间标定位置
  int leftTreeIndex = leftChild(treeIndex); //获取左孩子索引
  int rightTreeIndex = rightChild(treeIndex); //获取右孩子索引

  if (queryL >= mid + 1) { //如果左查找点大于区间中点,则在右区间递归查找
   return query(rightTreeIndex, mid + 1, r, queryL, queryR);
  } else if (queryR <= mid) { //如果右查找点小于区间中点,则在左区间递归查找
   return query(leftTreeIndex, l, mid, queryL, queryR);
  }

  //如果分布于左右都有,则分别将左边部分和右边都找到,最后融合返回给上一层
  E leftResult = query(leftTreeIndex, l, mid, queryL, mid);
  E rightRsult = query(rightTreeIndex, mid + 1, r, mid + 1, queryR);

  return merger.merge(leftResult, rightRsult);
 }
 
 public void set(int index, E e) { //根据索引修改数据数组的值
  //判断索引的合法性
  if (index < 0 || index >= data.length)
   throw new IllegalArgumentException("Index is illegal");
  //将索引位置元素更新为e
  data[index] = e;
  //调取递归元素将树中区间信息更新
  set(0, 0, data.length - 1, index, e);
 }
 //递归更新区间信息
 private void set(int treeIndex, int l, int r, int index, E e) {
  if (l == r) {
   tree[treeIndex] = e;
   return;
  }
  int mid = l + (r - l) / 2;
  int leftTreeIndex = leftChild(treeIndex);
  int rightTreeIndex = rightChild(treeIndex);
  if (index >= mid + 1)
   set(rightTreeIndex, mid + 1, r, index, e);
  else
   set(leftTreeIndex, l, mid, index, e);
  tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]);
 }

 @Override
 public String toString() {
  StringBuilder res = new StringBuilder();
  res.append("[ ");
  for (int i = 0; i < tree.length; i++) {
   if (tree[i] != null) {
    res.append(tree[i]);
   } else {
    res.append("null");
   }
   if (i != tree.length - 1) {
    res.append(", ");
   }
  }
  res.append(" ]");

  return res.toString();
 }
}

以上就是线段树的实现过程,线段树缺点是耗费空间较大,优点是区间查找信息更方便。

猜你喜欢

转载自blog.csdn.net/zhangjun62/article/details/82825378