法一(树状数组)
/**
* 法一(树状数组)
* 1. 介绍
* (1)给定一个数组A,若数组C是树状数组,则C满足
* C[1] = A[1]
* C[2] = A[1] + A[2]
* C[3] = A[3]
* C[4] = A[1] + A[2] + A[3] + A[4]
* C[5] = A[5]
* C[6] = A[5] + A[6]
* C[7] = A[7]
* C[8] = A[1] + A[2] + A[3] + A[4] + A[5] + A[6] + A[7] + A[8]
* (2)设k为i的二进制中从最低位到高位连续零的长度,i从1开始算
* 则 C[i] = A[i - 2^k + 1] + A[i - 2^k + 2] + ... + A[i],而2^k = lowBit(i) = i & -i
* (3)当求A[1] + A[2] + ... + A[x]时
* C[x]包含的不一定是1...x的全部和,就需要再找一个C[p](p < x)累加起来,这个p我们称之为x的前驱
* 前驱为比自己小的,最近的,末尾连续0比自己多的数,x的前驱 p = x - lowbit(x),相当于去掉了二进制中的低位1以及1后面所有的0
* 例如:A[1] + A[2] + ... + A[6] = C[6] + C[4],110(6) - 10(2) -> 100(4),4为6的前驱
* (4)如果修改了某个A[i]
* 需要改动所有包含A[j]的C[i],也就是要更改从叶子节点到根节点路径上所有的C[i]
* (5)如何找父节点
* 父节点是比自己大的,最近的,末位连续0比自己多的数,x的父节点 f = x + lowBit(x),相当于每次增加了二进制的低位1以及1后面所有的0
* 例如:101(5) + 1(1) -> 110(6), 110(6) + 10(2) -> 1000(8),6为5的父节点,8为6的父节点
* 2. 树状数组与线段树具体区别和联系:
* (1)两者在复杂度上同级,但是树状数组的常数明显优于线段树,其编程复杂度也远小于线段树
* (2)树状数组的作用被线段树完全涵盖,凡是可以使用树状数组解决的问题,使用线段树一定可以解决,但是线段树能够解决的问题树状数组未必能够解决
* (3)树状数组的突出特点是其编程的极端简洁性,使用lowbit技术可以在很短的几步操作中完成树状数组的核心操作,其代码效率远高于线段树
*/
public class Solution307_1 {
/**
* 树状数组tree和更新后的数组nums
*/
private int[] tree;
private int[] nums;
/**
* 取出x的最低位1以及1后面所有的0
* 1. 理解
* (1)一个数的负数就等于对这个数取反+1,即 x & -x = x & (~x + 1)
* (2)补码和原码必然相反,所以原码有0的部位补码全是1,补码再+1之后由于进位那么最右边的1和原码最右边的1一定是同一个位置
* (3)x & -x,当x为0时结果为0,x为奇数时结果为1,x为偶数时结果为x中2的最大次方的因子
* (4)以二进制数110为例:110的补码为001,加1后为010,两者相与便是010
* 2. 例子
* (1)lowbit(6) = 2,6的二进制表示为110,2的二进制表示为10
* (2)lowbit(4) = 4,4的二进制表示为100
*
* @param x
* @return
*/
private int lowBit(int x) {
return x & -x; // 取出x的最低位1以及1后面所有的0
}
/**
* 查询树状数组的前缀和
* 1. 理解
* (1)每次去掉了二进制中的低位1以及1后面所有的0
* 2. 复杂度
* (1)时间复杂度:O(logn)
*
* @param x
* @return
*/
private int query(int x) {
int sum = 0;
for (int i = x; i > 0; i -= lowBit(i)) { // 使用 x -= lowBit(x) 来寻找x的前驱
sum += tree[i]; // 累加所有前驱
}
return sum;
}
/**
* 在树状数组x-1位置中增加值val
* 1. 理解
* (1)每次增加了二进制的低位1以及1后面所有的0
* 2. 复杂度
* (1)时间复杂度:O(logn)
*
* @param x
* @param val
*/
private void add(int x, int val) {
for (int i = x; i < tree.length; i += lowBit(i)) { // 使用 x += lowBit(x) 来寻找x的父节点
tree[i] += val; // 需要改动所有包含A[j]的C[i]
}
}
/**
* 用整数数组nums初始化对象
* 1. 理解
* (1)调用add函数,相当于把没有修改为一个值
* 2. 复杂度
* (1)时间复杂度:O(nlogn)
* (2)空间复杂度:O(n)
*
* @param nums
*/
public Solution307_1(int[] nums) {
this.nums = nums;
this.tree = new int[nums.length + 1]; // 原数组长度+1,+1的原因是计算lowbit时,使用下标0会进入死循环
for (int i = 0; i < nums.length; i++) {
add(i + 1, nums[i]); // 初始化树状数组
}
}
/**
* nums[index]的值更新为val
* 更新树状数组以及前缀和
*
* @param index
* @param val
*/
public void update(int index, int val) {
add(index + 1, val - nums[index]);
nums[index] = val;
}
/**
* 返回数组nums中索引left和索引right之间(包含)的nums元素的和
* 求出前缀和并相减
*
* @param left
* @param right
* @return
*/
public int sumRange(int left, int right) {
return query(right + 1) - query(left);
}
}
法二(线段树)
/**
* 法二(线段树)
* 1、介绍
* (1)假设有一个数组nums,数组中元素个数为n,现需要对数组进行单点修改,区间查询操作,可引入线段树解决
* (2)线段树每个节点表示的是一个区间,每个节点将其表示的区间一分为二,左边分到左子树,右边分到右子树
* (3)根节点表示的是整个区间(也就是整个数组所有元素),叶子节点表示的是一个单元区间(也就是单个元素)
* (4)因为每次对半分的缘故,线段树构建出来是平衡的,也就是说树的高度是logn,这是后续很多高效操作的基础
* (5)线段树节点除了存储区间的左右边界,还可以储存不同的值,例如区间和,区间内最大值、最小值等
* (6)可以使用数组来表示线段树,假如根节点为 i,那么左孩子就为 2*i,右孩子就为 2*i+1 (i从1开始)
* 2、模板题
* (1)单点修改,区间查询
* (2)区间修改,单点查询
* (3)区间修改,区间查询
* 3、线段树节点
* (1)线段树的每个节点代表一个区间,而节点的值可以根据题目问题,改变表示的含义
* (2)数字之和「总数字之和 = 左区间数字之和 + 右区间数字之和」
* (3)最大公因数 (GCD)「总 GCD = gcd(左区间 GCD, 右区间 GCD)」
* (4)最大值「总最大值 = max(左区间最大值,右区间最大值)」
* 4、线段树基本操作
* (1)build:构建线段树,时间复杂度O(n)
* (2)update:更新输入数组中的某一个元素并对线段树做相应的改变,时间复杂度O(logn)
* (3)query:用来查询某一区间对应的信息(如最大值,最小值,区间和等),时间复杂度O(logn)
*/
public class Solution307_2 {
/**
* 线段树数组tree和更新后的数组nums
*/
private int[] tree;
private int[] nums;
/**
* 构建线段树
* 1. 思路:
* (1)自上而下而下递归分裂,自下而上回溯更新
* (2)从根节点到叶子节点不断地将区间一分为二,从叶子节点开始返回值,一直到根节点,不断地更新区间和信息
* 2. 复杂度
* (1)时间复杂度O(n)
* 构建的线段树有2n个节点,即底层有n个节点,那么倒数第二次约n/2个节点,倒数第三次约n/4个节点,依次类推
* n + 1/2 * n + 1/4 * n + 1/8 * n + ... = (1 + 1/2 + 1/4 + 1/8 + ...) * n = 2n
* 由于构建每个节点只花了O(1)的时间,因此整个构建的时间复杂度就是O(2n),忽略常数项,也就是O(n)
* (2)空间复杂度O(n)
* 保存线段树需要O(n)的空间
*
* @param node 线段树根节点下标
* @param start nums区间左边界
* @param end nums区间右边界
*/
private void build(int node, int start, int end) {
if (start == end) { // 到达线段树叶子节点
tree[node] = nums[start];
return;
}
int mid = start + (end - start) / 2; // 二分nums区间
int leftNode = node * 2 + 1; // 当前节点左孩子在线段树中的下标
int rightNode = node * 2 + 2; // 当前节点右孩子在线段树中的下标
build(leftNode, start, mid); // 递归左孩子区间[start, mid]
build(rightNode, mid + 1, end); // 递归右孩子区间[mid + 1, end]
tree[node] = tree[leftNode] + tree[rightNode]; // 向上更新区间和信息
}
/**
* 单点更新
* 1. 思路
* (1)更新需要从叶子节点一路走到根节点,去更新线段树上的值
* 2. 复杂度
* (1)时间复杂度O(logn)
* 因为线段树的高度为logn,所以涉及更新的节点数不超过logn
*
* @param node 线段树节点下标
* @param start nums区间左边界
* @param end nums区间右边界
* @param index 待更新的元素在nums中的下标
* @param val 需要更新的值
*/
private void update(int node, int start, int end, int index, int val) {
if (start == end) { // 找到需要更新的节点
nums[index] = val; // 更新nums
tree[node] = val; // 更新线段树
return;
}
int mid = start + (end - start) / 2; // 二分nums区间
int leftNode = node * 2 + 1; // 当前节点左孩子在线段树中的下标
int rightNode = node * 2 + 2; // 当前节点右孩子在线段树中的下标
if (index <= mid) { // 如果index在当前节点的左边
update(leftNode, start, mid, index, val); // 递归左孩子区间[start, mid]
} else { // 如果index在当前节点的右边
update(rightNode, mid + 1, end, index, val); // 递归右孩子区间[mid + 1, end]
}
tree[node] = tree[leftNode] + tree[rightNode]; // 向上更新区间和信息
}
/**
* 区间和查询
* 1. 思路
* (1)如果nums区间和查找区间完全重合
* 直接返回该结点的值,即当前区间和
* (2)如果nums区间和查找区间不完全重合
* 当查找区间在nums的左子区间时,到左子区间查询
* 当查找区间在nums的右子区间时,到右子区间查询
* 查找区间跨越nums的两个子区间时,把它切成2块,分在两个子区间查询,最后把查询结果合起来处理
* 2. 复杂度
* (1)时间复杂度O(logn)
* 每层节点最多访问四个,总共访问的节点数不超过4*logn
*
* @param node 线段树节点下标
* @param start nums区间左边界
* @param end nums区间右边界
* @param left 查找区间左边界
* @param right 查找区间右边界
* @return
*/
private int query(int node, int start, int end, int left, int right) {
if (left == start && right == end) { // 如果nums区间和查找区间完全重合,直接返回这个区间的值
return tree[node]; // 直接返回这个区间的值
}
int mid = start + (end - start) / 2; // 二分nums区间
int leftNode = node * 2 + 1; // 当前节点左孩子在线段树中的下标
int rightNode = node * 2 + 2; // 当前节点右孩子在线段树中的下标
if (right <= mid) { // 查找区间在nums的左子区间
return query(leftNode, start, mid, left, right); // 递归左孩子区间[start, mid]
} else if (left > mid) { // 查找区间在nums的右子区间
return query(rightNode, mid + 1, end, left, right); // 递归右孩子区间[mid + 1, end]
} else { // 查找区间跨越nums的两个子区间
return query(leftNode, start, mid, left, mid) + query(rightNode, mid + 1, end, mid + 1, right); // 切成2块,分在两个子区间查询
}
}
/**
* 用整数数组nums初始化对象
*
* @param nums
*/
public Solution307_2(int[] nums) {
if (nums.length == 0) {
return;
}
this.nums = nums;
this.tree = new int[nums.length * 4]; // 必须开4倍长度的数组
build(0, 0, nums.length - 1);
}
/**
* nums[index]的值更新为val
*
* @param index
* @param val
*/
public void update(int index, int val) {
update(0, 0, nums.length - 1, index, val);
}
/**
* 返回数组nums中索引left和索引right之间(包含)的nums元素的和
*
* @param left
* @param right
* @return
*/
public int sumRange(int left, int right) {
return query(0, 0, nums.length - 1, left, right);
}
}
本地测试
/**
* 307. 区域和检索 - 数组可修改
*/
lay.showTitle(307);
int[] nums307_1 = new int[]{1, 3, 5};
System.out.println(Arrays.toString(nums307_1));
Solution307_1 sol307_1 = new Solution307_1(nums307_1);
System.out.println(sol307_1.sumRange(0, 2));
sol307_1.update(1, 2);
System.out.println(Arrays.toString(nums307_1));
System.out.println(sol307_1.sumRange(0, 2));
int[] nums307_2 = new int[]{1, 3, 5};
System.out.println(Arrays.toString(nums307_2));
Solution307_2 sol307_2 = new Solution307_2(nums307_2);
System.out.println(sol307_2.sumRange(0, 2));
sol307_2.update(1, 2);
System.out.println(Arrays.toString(nums307_2));
System.out.println(sol307_2.sumRange(0, 2));