DS14-线段树的实现

线段树是一种比较高级的数据结构,在现实生活中也有比较广泛的应用,比如:实现对整个宇宙中某个星系的天体数的统计等.线段树是一种自底向上更新数据的数据结构,每个节点的值为其两个孩子节点进行某种操作(相加/取最大值/取最小值)得到的结果.

线段树在实现的时候可以使用数组作为底层,线段树并不是一个完全二叉树,但是是一个平衡二叉树,创建一个线段树需要先假设其是一个满二叉树,在其中存储全部的值,对于用不到的节点不做处理(赋值为null)就可以.

对于线段树中两个孩子节点进行某种合并这个操作,可以构造一个接口Merger,在其中定义一个merge方法,用于把两个值通过某种操作合并成一个值.而后建立一个SegmentTree类,完成一系列操作.

具体代码如下:
Merger接口类:

/**
 * 接口类:用于让实现这个接口的类能够把两个类型的值合并成一个(可以是获取最大最小值等)
 * @author ChenZhuJi
 *
 * @param <E>
 */
public interface Merger<E> {
	E merge(E a,E b);
}

SegmentTree类:

/**
 * 线段树的实现
 * @author ChenZhuJi
 *
 * @param <E>
 */
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 ];
		}
		tree = (E[])new Object[4 * arr.length];
		buildSegmentTree(0,0,arr.length - 1);
	}
	/**
	 * 在用数组表示的线段树的指定索引处创建表示从begin到end的线段树
	 * @param treeIndex 数组表示的线段树的指定索引
	 * @param begin 要创建的线段树的起始索引
	 * @param end 要创建的线段树的结束索引
	 */
	private void buildSegmentTree(int treeIndex,int begin,int end) {
		if(begin == end) {	//递归终止条件:要创建的线段树中只有一个元素了
			tree[treeIndex] = data[begin];
			return;
		}
		//若要创建的线段树的长度不为1,则先创建其左子树和右子树
		int leftChild = getLeftChild(treeIndex);	//获取线段树的左子树的索引
		int rightChild = getRightChild(treeIndex);	//获取线段树的右子树的索引
		int mid = begin + ( end - begin ) / 2;	//线段树分为两段,即为左子树和右子树

		buildSegmentTree(leftChild, begin, mid);	//创建线段树的左子树,从起始到中点
		buildSegmentTree(rightChild, mid + 1, end);	//创建线段树的右子树,从中点到终点
		
		tree[treeIndex] = merger.merge(tree[leftChild], tree[rightChild]);
	}
	
	/**
	 * 返回区间[queryL,queryR]的值
	 * @param queryL
	 * @param queryR
	 * @return 区间[queryL,queryR]的值
	 */
	public E query(int queryL,int queryR) {
		if(queryL < 0 || queryL > data.length || queryL >queryR || queryR > data.length || queryR < 0) {
			throw new IllegalArgumentException("Error!!");
		}
		return query(0,0,data.length - 1,queryL,queryR);
	}
	/**
	 * 表示在以treeIndex为根的线段树中[begin,...,end]范围内,搜索区间[queryL,...,queryR]的值
	 * @param treeIndex 根节点
	 * @param begin 线段树的起始索引
	 * @param end	线段树的终止索引
	 * @param queryL	待查找的区间的起始索引
	 * @param queryR	待查找的区间的终止索引
	 * @return 搜索后的结果
	 */
	private E query(int treeIndex,int begin,int end,int queryL,int queryR) {
		if(begin == queryL && end == queryR) {
			//递归终止条件:线段树范围恰好等于要搜索的区间的范围,此时直接返回线段树节点对应的索引值
			return tree[treeIndex];
		}	
		int mid = (begin + end) / 2;	//中间段索引
		int leftChild = getLeftChild(treeIndex);	//左子树的索引
		int rightChild = getRightChild(treeIndex);	//右子树的索引
		if(queryL >= mid + 1) {	//若要搜索的区间完全位于右子树一端,就直接在右子树上查找
			return query(rightChild,mid+1,end,queryL,queryR);
		} else if(queryR <= mid) {	//若要搜索的区间 完全位于左子树一端,就直接在左子树上查找
			return query(leftChild,begin,mid,queryL,queryR);
		} else {	//若要搜索的区间在左子树和右子树上都有,则把要搜索的区间根据中间索引分成左右两段,分别在左子树和右子树上查找,最后合并
			E leftResult = query(leftChild,begin,mid,queryL,mid);
			E rightResult = query(rightChild,mid+1,end,mid+1,queryR);
			return merger.merge(leftResult, rightResult);
		}
	}
	
	/**
	 * 将线段树中指定索引的值进行修改(动态更新)
	 * @param index 索引值
	 * @param e 修改的值
	 */
	public void set(int index,E e) {
		if(index >= data.length || index < 0) {
			throw new IllegalArgumentException("index is error!!");
		}
		data[index] = e;
		set(0,0,data.length - 1,index,e);
	}
	/**
	 * 在以treeIndex为根节点的线段树中,从begin到end范围内寻找索引为index的节点并修改其值
	 * @param treeIndex	根节点的索引值
	 * @param begin	搜索的起始范围
	 * @param end	搜索的终止范围
	 * @param index	待搜索的索引值
	 * @param e	修改的值
	 */
	private void set(int treeIndex,int begin,int end,int index,E e ) {
		if(begin == end ) {	//循环终止条件:起点等于终点,说明已经找到了这个索引
			tree[treeIndex] = e;
			return;
		}
		int mid =begin + ( end - begin ) / 2;	//中间索引
		int leftChild = getLeftChild(treeIndex);	//左孩子的索引值
		int rightChild = getRightChild(treeIndex);	//右孩子的索引值
		
		if(index >= mid + 1) {	//若index不比中间索引值+1小,说明在右孩子这边,去右孩子那边找
			set(rightChild, mid + 1, end, index, e );
		} else {	//若index比中间索引值小,说明在左孩子这边,去左孩子那边找
			set(leftChild, begin, mid, index, e);
		}
		tree[treeIndex] = merger.merge(tree[leftChild], tree[rightChild]);//更新节点值:每个线段树等于左孩子和右孩子通过某种操作得到
	}
	
	public int getSize() {
		return data.length;
	}
	
	public E get(int index) {
		if(index >= data.length || index < 0) {
			throw new IllegalArgumentException("索引越界");
		}
		return data[index];
	}
	
	private int getLeftChild(int index) {
		return 2 * index + 1;
	}
	
	private int getRightChild(int index) {
		return 2 * index + 2;
	}
	
	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder("[");
		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(", ");
			}
		}
		sb.append(" ]");
		return sb.toString();	
	}
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
赫夫曼树是一种特殊的二叉树,它的每个叶子节点都表示一个字符,并且每个字符对应的编码都是唯一的。赫夫曼树常用于数据压缩。 赫夫曼树的编码方式是按照前缀编码进行编码,即一个字符的编码不能是另一个字符编码的前缀。例如,如果字符 A 的编码是 10,那么字符 B 的编码不能是 1,因为这个编码是字符 A 的前缀。 赫夫曼树的解码方式是从根节点开始遍历树,根据编码的 0 或 1 分别选择左子树或右子树,直到到达叶子节点,找到对应的字符。 下面是一个赫夫曼树的例子: ``` /\ / \ / \ /\ /\ / \ / \ a b c d ``` 假设字符 a 的编码为 0,字符 b 的编码为 10,字符 c 的编码为 110,字符 d 的编码为 111。那么,编码串 010110111 可以被解码成字符串 "abcbd"。 具体的解码过程如下: 1. 从根节点开始遍历树,根据第一个字符 0,选择左子树。 2. 继续遍历左子树,根据第二个字符 1,选择右子树。 3. 继续遍历右子树,根据第三个字符 0,选择左子树。 4. 到达叶子节点,找到字符 a。 5. 回到根节点,根据第四个字符 1,选择右子树。 6. 继续遍历右子树,根据第五个字符 1,选择右子树。 7. 继续遍历右子树,根据第六个字符 0,选择左子树。 8. 到达叶子节点,找到字符 b。 9. 回到根节点,根据第七个字符 1,选择右子树。 10. 继续遍历右子树,根据第八个字符 1,选择右子树。 11. 到达叶子节点,找到字符 d。 12. 回到根节点,根据第九个字符 1,选择右子树。 13. 继续遍历右子树,根据第十个字符 1,选择右子树。 14. 到达叶子节点,找到字符 c。 15. 读取完所有的字符,解码完成。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值