LeetCode-307. 区域和检索 - 数组可修改-Java-medium

本文详细介绍了树状数组和线段树的基本概念、核心操作及应用场景,包括树状数组的lowbit技巧、查询与更新操作,以及线段树的构建、更新和查询过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目链接

法一(树状数组)
/**
 * 法一(树状数组)
 * 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));
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值