leetcode 307. 区域和检索 - 数组可修改(树状数组)

题目链接
思路:树状数组
首先:直接用前缀和数组肯定是会超时的,假设每次修改的都是第一个数组,那么后面的每个前缀和都要修改,最坏情况下,就是O(n2),显然就超时了,所以换个思路,使用树状数组。
树状数组是一种可以维护序列前缀和的数据结构,并且:
单点修改add(index,val):在index位置上加上val,它的时间复杂度是O(log(n))
前缀和prefixSum(index):查询[0,index]范围内的累加和,它的时间复杂度是O(log(n))
介绍一下树状数组这个数据结构:
一个数组A
在这里插入图片描述

这里的数组A就是装原始数据的数组
可以看出:
t[1]=a[1]
t[2]=a[1]+a[2]
t[3]=a[3]
t[4]=a[1]+a[2]+a[3]+a[4]
t[5]=a[5]
t[6]=a[5]+a[6]
t[7]=a[7]
t[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8]

这里T数组和A数组的关系就要找找了。
看下图:
在这里插入图片描述
T[i]=A[i-2k+1] +A[i-2k+2]+A[i-2k+3]+…+A[i];
这里的K的值表示i的二进制从最低位开始有几个连续的0 。
例如:i=4的时候,4的二进制是(0100),后面有两个0,那么k就等于2;
那么T[4]=A[4-4+1]+A[4-4+2]+A[4-4+3]+A[4-4+4]
也即T[4]=A[1]+A[2]+A[3]+A[4]
其他的可自行验证。
那么这里的2k指的其实也就是i这个数字换算成二进制之后,最低位的1形成的数字。

再来看看前缀和怎么求
SUM[1]=T[1]
SUM[2]=T[2]
SUM[3]=T[3]+T[2]
SUM[4]=T[4]
SUM[5]=T[5]+T[4]
SUM[6]=T[6]+T[4]
SUM[7]=T[7]+T[6]+T[4]
SUM[8]=T[8]
这样如果看不出规律,将SUM后的下标换算成二进制看看。
SUM[0001]
SUM[0010]
SUM[0011]
SUM[0100]
SUM[0101]
SUM[0110]
SUM[0111]
SUM[1000]
第一个规律: 二进制里面有几个1,那么后面就有几个T元素。
第二个规律: T元素里面的下标每次都是将前一个T元素的下标去掉最低位的1,然后再累加。(当然这个下标不能为0)也就是说,将上一个T元素的下标减去上一个T元素下标最低位的1形成的数字。

上面计算T数组的时候,也涉及到要计算最低为的1形成的数字。

那么这里前缀和已经可以求了。
现在还有一个问题,如果A数组中某个元素更新了,应该怎么更新?

这里也就是,修改A数组的某个元素,T数组如何修改。
T[i]=A[i-2k+1] +A[i-2k+2]+A[i-2k+3]+…+A[i];
那么被A[i]影响的位置有
T[i+2k]、T[i+2k+2k1]、…
这里解释一下k、k1、k2、k3…
k指的是i的最低位的1形成的数字
k1指的是i+2k最低位的1形成的数字
下面依次类推

举个例子:
上面那个图中:
假设更新了a[5]的值,那么被影响的就是t[5]、t[6]、t[8]
看看二进制就能发现规律了,
更新的是a[0101]
被影响的是t[0101]、t[0110]、t[1000]。
也就是后面一个下标等于前面一个下标加上最低位1形成的数字。

这里又是与最低位1形成的数字有关。
那么如何去计算一个数字的最低位形成的数字呢?

这里使用到了lowbit函数,这个函数功能就是得到一个数字最低位1形成的数字。
当然,也可以使用其他的方法去求得,只不过这个方法很快。
这里就显示了前人的智慧了。

		//功能是x加上  x的二进制最低的1形成的数字
        private int lowbit(int x){
            return x&(-x);
        }

为什么可以这么算呢?

这里利用的负数的存储特性,负数是以补码存储的,对于整数运算 x&(-x)有
       ●当x为0时,即 0 & 0,结果为0;
       ●当x为奇数时,最后一个比特位为1,取反加1没有进位,故x和-x除最后一位外前面的位正好相反,按位与结果为0。结果为1。
       ●当x为偶数,且为2的m次方时,x的二进制表示中只有一位是1(从右往左的第m+1位),其右边有m位0,故x取反加1后,从右到左第有m个0,第m+1位及其左边全是1。这样,x& (-x) 得到的就是x。 
       ●当x为偶数,却不为2的m次方的形式时,可以写作x= y * (2^k)。其中,y的最低位为1。实际上就是把x用一个奇数左移k位来表示。这时,x的二进制表示最右边有k个0,从右往左第k+1位为1。当对x取反时,最右边的k位0变成1,第k+1位变为0;再加1,最右边的k位就又变成了0,第k+1位因为进位的关系变成了1。左边的位因为没有进位,正好和x原来对应的位上的值相反。二者按位与,得到:第k+1位上为1,左边右边都为0。结果为2^k。
        总结一下:x&(-x),当x为0时结果为0;x为奇数时,结果为1;x为偶数时,结果为x中2的最大次方的因子。

在这里插入图片描述
到此,解决的问题有

  1. 建立了T数组与A数组的对应关系,即找到了T数组的计算方法。
  2. 建立了前缀和与T数组的关系,即找到了计算前缀和的计算方法。
  3. 找到了更新A数组,被影响的T数组的规律,即找到了因更新A数组,而更新T数组的计算方法。

到此,就可以解决这个题目了,其实这里就是用到了树状数组数据结构的特点。
代码:

    class NumArray {
        //树状数组
        int[] A =null;
        int[] T = null;
        int len = 0;
        public NumArray(int[] nums) {
            len = nums.length;
            this.A = new int[len+1];
            for(int i=0; i<len; i++){
                this.A[i+1] = nums[i];
            }
            //初始化T数组
            init();

        }

        public void update(int index, int val) {
            int i=index+1;
            //在原来的i位置上加上了change就变成了val,也就说变化值是change
            int change = val-A[i];
            A[i]= val;
            while(i<=len){
                T[i]+=change;
                i=i+lowbit(i);
            }
        }

        public int sumRange(int left, int right) {
        	//这里要注意一个小地方,就是这里的left和right是包含0的,但是我们的T数组是从1开始的。所以right+1,left-1+1
            int sum1 = getSum(right+1);
            int sum2 = getSum(left);
            return sum1-sum2;
        }

        //计算[1,end]的和,这里就是利用前缀和与T数组的关系
        private int getSum(int end){
            int res=0;
            int i=end;
            while(i>0){
                res = res + T[i];
                i=i-lowbit(i);
            }
            return res;
        }

        //初始化T
        private void init(){
            this.T = new int[len+1];
            for(int x=1; x<=len; x++){
                int i=x-lowbit(x)+1;
                while(i<=x){
                    T[x]+= A[i];
                    i++;
                }
            }
        }

        //功能是x加上  x的二进制最低的1形成的数字
        private int lowbit(int x){
            return x&(-x);
        }
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值