线段树是一种高级的数据结构,一般我们根本接触不到他,然而他的出现解决了特定的问题。这种数据结构的设计还是值得我们学习的。
目录
分析与拓展
1、线段树的设计栗子
假如给定一组数组元素:
A[0],A[1],A[2],A[3],A[4],A[5],A[6],A[7]
这些数组用区间表示可为A[0…7],则我们可以吧[0,7]这个区间不断均分取整,最终得到了如下图的形式。这就是我们的线段树模型。
1、如上图,每一个节点存储的为一个线段,或者可以说为一个区间的数据。
2、线段树不一定是一颗满二叉树,这点跟用户提供的元素个数有关。(如下图用户提供10个元素我们封装成的线段树)
3、线段树是一颗平衡二叉树。其实之前我们学过的堆也是一颗平衡二叉树。
4、线段树不一定是完全二叉树,这点跟线段树的元素个数有关。
2、平衡二叉树简介
一棵二叉树中树的最大深度与树的最小深度的差值不超过1。则这棵树为平衡二叉树。
3、假设有n个元素,使用数组表示线段树要申请多大的空间?
用户提供了指定个数数组元素,我们把这些元素放入线段树中存储。当我们的线段树底层使用数组实现时,这个数组要申请多大的空间呢?如上图,给了A[0,7]8个元素我们组成二叉树有15个节点,这时用数组实现要申请15个空间,那么我们申请的空间有规律么?接下来我们就探讨下。
(1)满二叉树情况下需要申请空间推导
假如用户提供的元素个数为n,我们正好可构成满二叉树,如下栗子:
特殊情况下,即用户提供元素个数为n 时,这时我们只需申请2n的空间即可。
注意:
1、这种特殊的情况为n=2^h,即n的数值为2的h次方。
2、满足 n = 2^h时,正好可构成满二叉树。
(2)非满二叉树情况下需要申请空间推导
我们知道用户提供的元素为n,n不一定满足 n = 2^h,有可能 n = 2 ^h +k这时就可能在叶子节点就多出几个元素了。如下图:这时我们还是可以吧这颗树当做满二叉树,处理的,吧空余的位置都看做空元素即可。
为啥申请4n个空间:
这里考虑到最坏的情况,n = 2^h +k,多出来一层,本来 n=2 ^h时需要申请2n个空间,多出一层,即多了2n。
代码设计
1、线段树的构建
/**
* Create by SunnyDay on 2020/08/15
*/
public interface Merger<E> {
// 吧泛型E 代表的两种类型元素融合成为一种元素。
E merge(E a, E b);
}
/**
* Create by SunnyDay on 2020/08/12
* 线段树,基于数组方式实现。
*/
public class SegmentTree<E> {
private E[] data; // 内部维护用户传递过来的数组
private E[] tree;//线段树的数组实现
private Merger<E> merger; // 融合器,消除类型之间的兼容性。
/**
* 构造,用户传一个数组,我们内部维护这个数组。
*/
@SuppressWarnings("unchecked")
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];
// }
// 使用for 遍历数组,给另一个数组赋值时。系统建议使用 System.arraycopy 函数
System.arraycopy(arr, 0, data, 0, arr.length);
tree = (E[]) new Object[4 * arr.length]; // 申请数组元素四倍空间
// 默认情况下根节点的索引为0,区间左右端点为[0,data.length-1]
buildSegmentTree(0, 0, data.length - 1);
}
/**
* 返回完全二叉树中 给定索引所代表元素左孩子节点的索引
*/
private int leftChild(int index) {
return index * 2 + 1;// 公式 参考推导图
}
/**
* 返回完全二叉树中 给定索引所代表元素有孩子节点的索引
*/
private int rightChild(int index) {
return index * 2 + 2;// 公式 参考推导图
}
public int getSize() {
return data.length;
}
/**
* 获得指定索引的元素
*/
public E get(int index) {
if (0 < index || index >= data.length) {
throw new IllegalArgumentException("index is illegal");
}
return data[index];
}
}
/**
* 在treeIndex 位置 创建区间为[left,right]的线段树
*/
private void buildSegmentTree(int treeIndex, int left, int right) {
// 1、递归终结条件(递归到底,区间就一个元素)
//(1)找到底的条件,写判断。
//(2)return
if (left == right) {
// left 代表数组索引区间,treeIndex代表 线段树数组表示中的索引位置
tree[treeIndex] = data[left];//data[right] 意思一样
return;
}
//2、区间元素为多个时,treeIndex 有左右孩子。
int leftTreeIndex = leftChild(treeIndex);// 左孩子索引
int rightTreeIndex = rightChild(treeIndex);// 右孩子索引
// (1)总的区间有了,则中点也可找出。
//int middle = (left + right) / 2;// 可能会整型溢出
int middle = left + (right - left) / 2;
// (2)treeIndex 位置子孩子的区间也就可标识出来了即:[left,middle],[middle+1,right]
// (3) 有了索引,区间表示,则可递归创建左右子树作为线段树。
buildSegmentTree(leftTreeIndex, left, middle);
buildSegmentTree(rightTreeIndex, middle + 1, right);
// treeIndex 索引对应区间元素和则为其左右子树元素之和
//tree[treeIndex] = tree[leftTreeIndex] + tree[rightTreeIndex];
//Operator '+' cannot be applied to 'E', 'E'
//思考:+ 的使用范围应该是同种类型。
// Object a = 10;
// Object b = "a";
// Object c = a+b;
// 上面不仅出现类型兼容问题,而且+的处理过于局限,用户只能处理区间之和。这里使用接口融合器
// 消除兼容问题,并且业务逻辑用户自己实现。求和,区间极值都可。
tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]);
}
注意点:
1、用户指定一个数组后,其实线段或者区间就已经确定
2、由于树的天然递归性,可采用递归实现。
3、留意融合器接口的设计
4、留意两整数求平均值溢出计算
2、查询设计
/**
* @param queryL
* @param queryR
* @function 用户要查询的区间[queryL, queryR]
*/
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);
}
/**
* 在根节点为treeIndex,区间为[left,right] 中查询[queryL,queryR] 区间
*/
private E query(int treeIndex, int left, int right, int queryL, int queryR) {
// 1、递归终结条件
if (left == queryL && right == queryR) {
return tree[treeIndex];
}
//2、划分区间
int leftTreeIndex = leftChild(treeIndex);
int rightTreeIndex = rightChild(treeIndex);
int middle = left + (right - left) / 2;
// 3、判断区间
if (queryL >= middle + 1) {
//[queryL,queryR] 区间在[left,right] 去见的右孩子区间
return query(rightTreeIndex, middle + 1, right, queryL, queryR);
} else if (queryR <= middle) {
//[queryL,queryR] 区间在[left,right] 去见的左孩子区间
return query(leftTreeIndex, left, middle, queryL, queryR);
} else {
//[queryL,queryR] 区间在[left,right] 区间的左右孩子都有
E leftResult = query(leftTreeIndex, left, middle, queryL, middle);
E rightResult = query(rightTreeIndex, middle + 1, right, middle + 1, queryR);
return merger.merge(leftResult, rightResult);
}
}
3、更新操作
public void set(int index, E e) {
if (index < 0 || index >= data.length) {
throw new IllegalArgumentException("index is illegal");
}
data[index] = e;
set(0, 0, data.length - 1, index, e);
}
/**
* 更新以treeIndex 为根节点,区间为[left,right] 内索引为 index 的元素
*/
private void set(int treeIndex, int left, int right, int index, E e) {
if (left == right) {
tree[index] = e;
return;
}
//2、划分区间
int leftTreeIndex = leftChild(treeIndex);
int rightTreeIndex = rightChild(treeIndex);
int middle = left + (right - left) / 2;
if (index>=middle+1){
set(rightTreeIndex,middle+1,right,index,e);
}else {
set(leftTreeIndex,left,middle,index,e);
}
// 更新
tree[treeIndex] =merger.merge(tree[leftTreeIndex],tree[rightTreeIndex]);
}
4、输出
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("[");
for (int i = 0; i < tree.length; i++) {
if (null != tree[i]) {
sb.append(tree[i]);
} else {
sb.append("null");
}
if (i != tree.length - 1) {
sb.append(",");
} else {
sb.append("]");
}
}
return sb.toString();
}
end
简单的总结下,收获颇丰,溜溜球!