文章目录

  • 线段树Segment Tree
  • 一、线段树介绍
  • 二、线段树基础实现
  • 三、创建线段树(支持自定义逻辑)
  • 四、线段树的区间查询
  • 五、线段树的点更新


线段树Segment Tree

一、线段树介绍

在竞赛题目中,线段树(区间树)是经常出现的一类题目。LeetCode上面也有线段树的问题。

普通的树是以一个个元素作为结点的,而线段树是以一个个区间作为结点的,它适用于对区间进行操作的题目。

一个很有意思的问题是——染色问题:Java 计算几个点是否在一条直线上 java计算线段长度_线段树 对于一面墙,长度为 Java 计算几个点是否在一条直线上 java计算线段长度_数组_02,每次选择一段墙进行染色。有多次染色。Java 计算几个点是否在一条直线上 java计算线段长度_结点_03 次操作后,我们可以看见多少种颜色?Java 计算几个点是否在一条直线上 java计算线段长度_结点_03 次操作后,我们可以在 Java 计算几个点是否在一条直线上 java计算线段长度_线段树_05

其实就是两种操作,染色操作(更新区间)和查询操作(查询区间)。我们很容易的想到可以用数组进行模拟,但是这样的话,这两种操作的复杂度就是 Java 计算几个点是否在一条直线上 java计算线段长度_线段树_06Java 计算几个点是否在一条直线上 java计算线段长度_结点_03 次就是 Java 计算几个点是否在一条直线上 java计算线段长度_线段树_08。对于大数据的问题就无可奈何了。此时,线段树就大有用武之地了。

另一类问题是区间查询:Java 计算几个点是否在一条直线上 java计算线段长度_线段树 如果我们不断的更新数据,然后对相应区间的和、最大值、最小值进行统计查询。这种更新和查询有多次。对于这种区间的、动态的查询,用静态的数据结构很麻烦,基于区间的线段是很有用的。

总结一下线段树的经典操作:

  • 更新:更新区间中一个元素或者一个区间的值;
  • 查询:查询一个区间中的最大值、最小值、区间和等等。

这两种操作都是 Java 计算几个点是否在一条直线上 java计算线段长度_数组_10的。同时,我们需要知道的是:线段树面对的区间是固定的,我们不考虑添加新的元素。

对于一个大小为 Java 计算几个点是否在一条直线上 java计算线段长度_数组_11 的数组,我们可以构建如下的一棵树,叶节点就是每个元素——或者说长度为 Java 计算几个点是否在一条直线上 java计算线段长度_线段树_12


Java 计算几个点是否在一条直线上 java计算线段长度_Java 计算几个点是否在一条直线上_13

以求和为例,要查询 Java 计算几个点是否在一条直线上 java计算线段长度_数组_14 的区间和,我们一步就可以查询到了:

Java 计算几个点是否在一条直线上 java计算线段长度_线段树_15

当然,不是所有的区间都可以直接得到,比如说查找 Java 计算几个点是否在一条直线上 java计算线段长度_Java 计算几个点是否在一条直线上_16 的和,我们需要访问两个区间的和并相加,尽管如此,这比对整个区间进行操作仍然快得多。

Java 计算几个点是否在一条直线上 java计算线段长度_数组_17

二、线段树基础实现

有一个结论:线段树不一定是完全二叉树;但是线段树一定是平衡二叉树。这样,线段树就几乎不会出现最坏的情况,它不会退化成一个链表,这就是它的优势

为什么呢?原因很简单,我们每次将一个区间一分为二,两个区间的元素数量要么相等,要么相差 Java 计算几个点是否在一条直线上 java计算线段长度_线段树_12 个元素的数量,这样到叶子结点的时候,左右区间最多相差一层(多 Java 计算几个点是否在一条直线上 java计算线段长度_线段树_12

虽然线段树不一定是完全二叉树,但是这样一棵平衡二叉树,我们仍然可以使用数组来表示,就将它看做一棵满二叉树,那些不存在的元素就当做 就行了。

尽然使用数组来表示,那么对于一棵满二叉树,有 Java 计算几个点是否在一条直线上 java计算线段长度_结点_20 层,总结点数量是多少呢——Java 计算几个点是否在一条直线上 java计算线段长度_数组_21 个结点。我们就将其作为 Java 计算几个点是否在一条直线上 java计算线段长度_结点_22,这样一定可以装下一棵满二叉树。同时,满二叉树最后一层有 Java 计算几个点是否在一条直线上 java计算线段长度_Java 计算几个点是否在一条直线上_23 个结点,大致等于前面所有的结点数量之和 Java 计算几个点是否在一条直线上 java计算线段长度_数组_24

那么,如果区间有 Java 计算几个点是否在一条直线上 java计算线段长度_数组_02 个元素,数组表示需要开多大的空间,需要多少个结点?假设 Java 计算几个点是否在一条直线上 java计算线段长度_结点_26,即最后一层的大小为 Java 计算几个点是否在一条直线上 java计算线段长度_Java 计算几个点是否在一条直线上_27,那么根据前面的情况,此时我们存储整个二叉树,只需要 Java 计算几个点是否在一条直线上 java计算线段长度_结点_28

当然,通常 Java 计算几个点是否在一条直线上 java计算线段长度_数组_02 不一定等于 Java 计算几个点是否在一条直线上 java计算线段长度_Java 计算几个点是否在一条直线上_30,可能为 Java 计算几个点是否在一条直线上 java计算线段长度_Java 计算几个点是否在一条直线上_31,这意味着 Java 计算几个点是否在一条直线上 java计算线段长度_结点_28 的空间不一定能够存放叶子结点。最坏情况,叶子结点可能到达下一层,我们加一层,emmmm,假设为满二叉树,则最后一层的结点数量大致等于前面所有的结点数量之和,因此最后我们需要 Java 计算几个点是否在一条直线上 java计算线段长度_结点_33

结论:Java 计算几个点是否在一条直线上 java计算线段长度_线段树_34 个元素的区间,构建线段树最大需要 Java 计算几个点是否在一条直线上 java计算线段长度_线段树_35

如果我们使用指针,可以完全避免这种浪费,平时可以这样实现,不过做题的时候用指针容易出错,因此建议用数组实现。

基础的代码如下:

public class SegmentTree<E> {
	private E[] data;
	private E[] tree;
	
	public SegmentTree(E[] arr) {
		data = (E[])new Object[arr.length];
		for (int i = 0; i < arr.length; ++i)
			data[i] = arr[i];
		tree = (E[])new Object[arr.length * 4];
		buildSegmentTree(0, 0, data.length - 1); //treeIndex, l, r
	}
	
	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) { //从0开始
		return 2 * index + 1;
	}
	//返回一个索引所表示的左孩子的索引
	private int rightChild(int index) {
		return 2 * index + 2;
	} 
}

三、创建线段树(支持自定义逻辑)

用一个接口 Merger<E>,可以自定义两个区间“合并”的逻辑。

public interface Merger<E> {
	E merge(E a, E b); //将两个E转换为一个E返回去
}

代码如下:

public class SegmentTree<E> {
	private E[] data; //原始数据
	private E[] tree;
	private Merger<E> merger; //融合器
	
	public SegmentTree(E[] arr, Merger<E> merger) {
		data = (E[])new Object[arr.length];
		for (int i = 0; i < arr.length; ++i)
			data[i] = arr[i];
		tree = (E[])new Object[arr.length * 4];
		this.merger = merger;
		buildSegmentTree(0, 0, data.length - 1); //treeIndex, l, r
	}
	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);
		//区间和就是用+; 最大值最小值就是max,min
		//问题是E上面不一定定义了加法; 同时, 我们希望用户根据业务场景自由组合逻辑使用线段树
		tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]);
	} 
	......
	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();
		sb.append('[');
		for (int i = 0; i < tree.length; ++i) {
			if (tree[i] != null) 
				sb.append(tree[i]);
			else
				sb.append("null");
			if (i != tree.length - 1) sb.append(' ');
		}
		return sb.toString();
	} 
}

四、线段树的区间查询

比如要在下面的线段树中查询一个区间 Java 计算几个点是否在一条直线上 java计算线段长度_Java 计算几个点是否在一条直线上_16,我们需要分别到左右两边的子树查询,并合并结果。

Java 计算几个点是否在一条直线上 java计算线段长度_数组_17

事实上,区间查询是很简单的。由于每次我们都是将区间折半,因此我们很容易可以算出区间的 Java 计算几个点是否在一条直线上 java计算线段长度_线段树_38 以及 Java 计算几个点是否在一条直线上 java计算线段长度_数组_39。如果我们要查询的区间 Java 计算几个点是否在一条直线上 java计算线段长度_Java 计算几个点是否在一条直线上_40 在中轴 Java 计算几个点是否在一条直线上 java计算线段长度_数组_39 左边或者右边,就分别到两边的子树去查询;如果 Java 计算几个点是否在一条直线上 java计算线段长度_Java 计算几个点是否在一条直线上_40

Java 计算几个点是否在一条直线上 java计算线段长度_线段树 查询 Java 计算几个点是否在一条直线上 java计算线段长度_Java 计算几个点是否在一条直线上_44

  • Java 计算几个点是否在一条直线上 java计算线段长度_结点_45Java 计算几个点是否在一条直线上 java计算线段长度_线段树_46,到根节点 Java 计算几个点是否在一条直线上 java计算线段长度_线段树_47
  • Java 计算几个点是否在一条直线上 java计算线段长度_结点_45Java 计算几个点是否在一条直线上 java计算线段长度_数组_49,同时 Java 计算几个点是否在一条直线上 java计算线段长度_结点_45Java 计算几个点是否在一条直线上 java计算线段长度_Java 计算几个点是否在一条直线上_51,因此同时向 Java 计算几个点是否在一条直线上 java计算线段长度_数组_52 的左子树查询 Java 计算几个点是否在一条直线上 java计算线段长度_Java 计算几个点是否在一条直线上_53,向右子树查询 Java 计算几个点是否在一条直线上 java计算线段长度_线段树_54
  • Java 计算几个点是否在一条直线上 java计算线段长度_Java 计算几个点是否在一条直线上_53Java 计算几个点是否在一条直线上 java计算线段长度_数组_56,因此到 Java 计算几个点是否在一条直线上 java计算线段长度_数组_57 的右区间查询 Java 计算几个点是否在一条直线上 java计算线段长度_Java 计算几个点是否在一条直线上_53Java 计算几个点是否在一条直线上 java计算线段长度_线段树_54Java 计算几个点是否在一条直线上 java计算线段长度_线段树_60,因此到 Java 计算几个点是否在一条直线上 java计算线段长度_线段树_61 的左区间查询 Java 计算几个点是否在一条直线上 java计算线段长度_线段树_54
  • 得到结果。

代码如下:

//返回[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.");
	//treeIndex, l, r, queryL, queryR
	return query(0, 0, data.length - 1, queryL, queryR); 
}
//在以treeindex为根的线段树[l...r]的范围中,搜索区间[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 rightResult = query(rightTreeIndex, mid + 1, r, mid + 1, queryR);
		return merger.merge(leftResult, rightResult); //两半区间融合用merger
}
//一个小小的测试用例
public static void main(String[] args) {
	Integer[] nums = {-2, 0, 3, -5, 2, -1};
	SegmentTree<Integer> segTree = new SegmentTree<>(nums, (a, b) -> a + b); //lambda表达式
	
	System.out.println(segTree.query(0, 2)); //计算区间[1,2]的和-2+0+3=1
	System.out.println(segTree.query(2, 5)); //-1
	System.out.println(segTree.query(0, 5)); //-3从
}

Java 计算几个点是否在一条直线上 java计算线段长度_结点_63


五、线段树的点更新

修改元素,直接修改叶子结点上元素的值,然后从底部往上更新线段树,操作次数也是 Java 计算几个点是否在一条直线上 java计算线段长度_Java 计算几个点是否在一条直线上_64

//将index位置的元素更新为e
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, l,r, index, e
}
//在以treeIndex为根的线段树中更新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 //index <= mid
		set(rightTreeIndex, l, mid, index, e);
	//从底部往上更新线段树 
	tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]); //两半区间融合用merger
}