算法学习14: 线段树

线段树解决的问题

线段树要解决的问题是: 在一个实时更新的动态数组中查询区间和(或广义的区间状态,如区间积,区间最大最小值).

若所查询的数组为静态数组,则此问题只需要使用数组前缀和即能解决.

线段树的结构

线段树是一种平衡二叉树,其每个节点存储数组中一段区间和.其左子节点存储区间左半部分和;其右子节点存储区间右半部分和.

线段树是一个平衡二叉树,可以像完全二叉树那样用数组存储.例如: 将数组[0,1,2,3,4,5]保存在线段树数组中,树的结构如下:

在这里插入图片描述

线段树数组的内容为[15, 3, 12, 1, 2, 7, 5, 0, 1, 0, 0, 3, 4],注意到数组中对应完全二叉树的下标9下标10的节点不存在.

为了便于下标计算,我们的实际线段树数组下标是从1开始的,0位被浪费了.

线段树的操作leetcode 307

构造线段树

线段树的递归构造: 若某节点存储原数组nums[leftIndex] ~ nums[rightIndex]区间数组和,其左子节点存储原数组nums[leftIndex] ~ nums[midIndex]区间数组和,其右子节点存储原数组nums[midIndex+1] ~ nums[rightIndex]区间数组和.

线段树开辟的数组长度应为原数组长度的四倍,证明如下:

若原数组nums的长度为N,线段树深度H应为 ⌈ l o g 2 N + 1 ⌉ \lceil log_{2}N+1\rceil log2N+1,则该完全二叉树的数组长度应为 2 H + 1 ⩽ 4 N 2^{H+1}\leqslant4N 2H+14N.

private int[] segmentTree; // 线段树的结构

// 构造线段树
private void buildSegmentTree(int[] nums) {
	segmentTree = new int[4 * nums.length]; // 线段树数组长度要开到原数组的4倍
	buildSegmentTree(nums, 1, 0, nums.length - 1);
}

// 构造线段树上的segment[pos]节点,该节点存储原nums数组[leftIndex,rightIndex]部分的数组和
private int buildSegmentTree(int[] nums, int pos, int leftIndex, int rightIndex) {
	if (leftIndex == rightIndex) {
		// 若该节点存储原数组区间范围内只有一个节点,则其没有子节点
		segmentTree[pos] = nums[leftIndex];
	} else {
		// 若该节点存储原数组区间范围内有多于一个节点,则递归创建其左右节点
		//	其左子节点存储原该节点存储原nums数组[leftIndex,midIndex]部分的数组和
		//	其左子节点存储原该节点存储原nums数组[midIndex+1,rightIndex]部分的数组和			
		int midIndex = (leftIndex + rightIndex) / 2;
		segmentTree[pos] = buildSegmentTree(nums, pos * 2, leftIndex, midIndex)
				+ buildSegmentTree(nums, pos * 2 + 1, midIndex + 1, rightIndex);
	}
	// 返回该节点对应的区间和
	return segmentTree[pos];
}

打印线段树

// 打印整颗线段树
public void printSegmentTree() {
	printSegmentTree(1, 0, nums.length, 0);
}

// 先序遍历打印线段树第pos位为树根的子树,该节点存储原nums数组leftIndex至rightIndex部分的数组和
private void printSegmentTree(int pos, int leftIndex, int rightIndex, int layer) {
	// 显示层数
	for (int i = 0; i < layer; i++) {
		System.out.print("---");
	}
	// 显示树根节点
	System.out.println("[" + leftIndex + " " + rightIndex + "]: " + segmentTree[pos]);
	// 若存在子树,则递归打印左右子树
	if (leftIndex < rightIndex) {
		int midIndex = (leftIndex + rightIndex) / 2;
		printSegmentTree(pos * 2, leftIndex, midIndex, layer + 1);
		printSegmentTree(pos * 2 + 1, midIndex + 1, rightIndex, layer + 1);
	}
}

打印结果如下:

在这里插入图片描述

查询数组区间和

区间和的查询应用二分思想,不断将当前区间二分直到当前区间全部位于查询区间内,这时将该节点所存储的区间和其它所有位于查询区间内的子区间和相加得到总的查询区间和.

// 查询原sum数组qleftIndex到qrightIndex部分数组和
public int sumRange(int qleft, int qright) {
	return sumRange(1, 0, nodeNum - 1, qleft, qright);
}

// 在 `线段树数组中代表原sum数组[leftIndex,rightIndex]区间内` 二分查询 `原sum数组[qleftIndex,qrightIndex]区间和`
private int sumRange(int pos, int leftIndex, int rightIndex, int qleftIndex, int qrightIndex) {
	// [leftIndex, rightIndex]表示    当前进入线段树的区间
	// [qleftIndex, qrightIndex]表示	查询原数组区间
    // 当前进入线段树的区间和 保存在 segmentTree[pos] 中
	if (qleftIndex > rightIndex || qrightIndex < leftIndex) {
		// 若 当前进入线段树的区间 与 查询原数组区间 不重合,则不存在求和项,返回0
		return 0;
	} else if (qleftIndex <= leftIndex && qrightIndex >= rightIndex) {
		// 若 当前进入线段树的区间 被 查询原数组区间 覆盖,则整个[leftIndex, rightIndex]区间都是求和项,直接返回当前区间和
		return segmentTree[pos];
	} else {
		// 若 当前进入线段树的区间 与 查询原数组区间 互有重合,则二分查找当前进入区间的两个子区间
		int mid = (leftIndex + rightIndex) / 2;
		return sumRange(pos * 2, leftIndex, mid, qleftIndex, qrightIndex) + sumRange(pos * 2 + 1, mid + 1, rightIndex, qleftIndex, qrightIndex);
	}
}

更新数组某一位

若更新了数组某一位,则线段树中包含该位的左右区间和也必然被更新.

从根节点(对应原nums数组[0, nums.len-1]区间和)开始,不断将其所有包含第i位的子区间加上对应的偏移量.

// 将原数组nums第i位的值改为val
public void update(int i, int val) {
	int delta = val - nums[i]; 	// 求出增量
	nums[i] = val;				// 更新原数组
	// 上边两行顺序不能互换,务必要先求增量再更新数组(这TM不是废话么)
	update(1, i, delta, 0, nodeNum - 1); // 修改线段树
}

// 将线段树中 包含原数组第i位的所有区间和 加上一个 增量delta
private void update(int pos, int i, int delta, int leftInndex, int rightIndex) {
	segmentTree[pos] += delta; // 加上增量
	// 当前进入到 原数组区间[leftIndex, rightIndex] ,进入函数前已保证区间内包含i
	// 若其存在子区间,则其中一个子区间必然也包含i
	if (leftInndex != rightIndex) {
		int mid = (leftInndex + rightIndex) / 2;
		// 将该节点包含第i位的子节点也 加上增量delta
		if (i <= mid)
			update(pos * 2, i, delta, leftInndex, mid);
		else
			update(pos * 2 + 1, i, delta, mid + 1, rightIndex);
	}
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值