这一题比起来304https://blog.csdn.net/chaochen1407/article/details/86572593就难多了。这一题首先是一维数组,那题是二维的。但是这题加入了一个情况就是有一个update的api可以让你更新数组的内容。在这个情况下原来那个办法就没有用了,因为原来的办法我update一个数字的话最坏的情况我要update n个已经求好的和。所以这里就只能另寻他法了。基本上我们还是需要新开空间去进行某种记忆和运算以达到最好的效率,但是就不是那么无脑了。

第一种比较简单易用的方法就是分段法,把原来的数组分成长度的开根号的段落,就是开一个新数组长度就是长度的开根号,然后新数组里面每一个元素就是每一个段落的总和,所以当你进行update的时候你只需要update一个段落即可。举例说明,如果我们的原数组是1,2,3,4,5,6,7,8,9。那么新数组刚好就是长度为3,内容是6,15,24。 当你call update(1, 5)的时候,你首先根据1定位到是那个段落,这里也就是0,所以你更新就是6 - 2(原来的数字) + 5 = 9,新数组的内容就是9, 15, 24。当你做sumRange的时候,你会因为新生成的内容省去遍历全部原数组的时间。你最多需要的操作就是3倍的原长度的根号的复杂度。譬如你要sumRange(1, 7)的时候,因为1, 7涉及到了新数组的内容的index的0, 1, 2(也就是全部)。所以你第一步先把全部加起来就是9 + 15 + 24 = 48。但是头和尾并没有完全覆盖到那两个所属的段落,所以你又需要从原数组那边去稍微剪掉一部分,也就是原数组的index为0和8的部分,也就是48 - 1 - 9 = 38。这样你就可以得到答案了。因为chunk总数和每一个chunk代表的内容的长度都是原长度的根号,你最多需要遍历完全部chunk然后再遍历两个chunk的原数组内容即可,也就是3倍的原数组长度的开方。

根据这个算法,可以得到代码如下:

    private int[] chunks;private int[] nums;private int chunkSize;public NumArray(int[] nums) {this.nums = nums;if (nums.length == 0) return;double sq = Math.sqrt(nums.length);chunkSize = (int)Math.ceil(sq);int chunkLen = nums.length / chunkSize;chunkLen += nums.length % chunkSize == 0 ? 0 : 1;this.chunks = new int[chunkLen];for (int i = 0; i < nums.length; i++) {int curIndex = i / chunkSize;chunks[curIndex] += nums[i];}}public void update(int i, int val) {int chunkIdx = i / chunkSize;chunks[chunkIdx] += val - nums[i];nums[i] = val;}public int sumRange(int i, int j) {int beginIdx = i / chunkSize;int endIdx = j / chunkSize;int result = 0;for (int k = beginIdx; k <= endIdx; k++) result += chunks[k];for (int k = beginIdx * chunkSize; k < i; k++) result -= nums[k];for (int k = j + 1; k < Math.min(nums.length, (endIdx + 1) * chunkSize); k++) result -= nums[k];return result;        }

上面这个做法相对是比较邪道的,也不符合题目里面update和sumRange call次数差不多的暗示。因为sumRange是O(根号N),update是O(1),很屌的呢

那个提示其实就是要说最好update和sumRange的复杂度是差不多的。这一题的王道做法其实应该是segment tree。关于segment tree的具体介绍请各位自行google。

关于segment tree,大略上有两种实现方式。第一种是依据segment tree的严格定义作出一棵树来,这基于下面的数据结构:

class SegmentTreeNode {SegmentTreeNode left, right;int sum, start, end;public SegmentTreeNode(int sum, int start, int end) {this.sum = sum;this.start = start;this.end = end;}
}

解答可以分为几步:

1.第一步建树,复杂度是O(N)

基本上我是参考了https://www.jianshu.com/p/91f2c503e62f 做出来的所以首先建树这一步,我和它基本无异。和二叉树建树的方式差不多,只是多了一步求和的过程。走的是后序遍历的先左再右最后中的过程。

    SegmentTreeNode root;int[] nums;public NumArray(int[] nums) {this.root = this.buildTree(nums, 0, nums.length - 1);this.nums = nums;}public SegmentTreeNode buildTree(int[] nums, int start, int end) {if (start > end) {return null;}int mid = (start + end) / 2;SegmentTreeNode node = new SegmentTreeNode(nums[start], start, end);if (start == end) return node;node.left = buildTree(nums, start, mid);node.right = buildTree(nums, mid + 1, end);node.sum = node.left.sum + node.right.sum;return node;}

2. 节点更新, 这一步和上面那个link也差不多。只是它自下而上回归结果,而我因为用一个nums缓存了原数组,所以我最开始就会知道每一个节点需要更新的值是多少,所以我自上而下循环改变每一个对应的节点即可。复杂度是O(logN)。

    public void update(int i, int val) {int oldVal = nums[i];nums[i] = val;int start = 0, end = nums.length - 1;SegmentTreeNode current = this.root;while (current != null) {int mid = (current.start + current.end) / 2;current.sum += val - oldVal;if (i <= mid) {// 如果i在mid中左边,表示需要更新的节点在左子树current = current.left;} else {//如果i在mid右边,表示需要更新的节点在右子树current = current.right;}}}

3. 区间查询。这一点我的做法和原文差距就比较大了。当然原理还是一样的。先解释一下我的做法。我的做法是基于循环。在原题目api public int sumRange(int i, int j) 中,用i扫一次,用j扫一次。遍历的方式都是一样的,就是每次和节点的mid做比较,如果小于mid就往左走,大于mid就往右走。虽然都是同样的走法,但对于i和j来说,意义是不一样的。对于i,也就是range的起始点来说,往左走表示左边子树还有元素需要被包括进去,往右走就表示左边子树已经不存在应该被包含的元素了。对于j,也就是range的终结点来说,往左走就表示右边子树不存在应该被包含的元素,往右走就表示右边应该存在还需要被包含的元素。基于上面这套理论,我的做法就是这样的:基数是全部数字的和。当i遍历的时候,往左走不做计算,每当往右走的时候,就把母亲节点的左子树的和的部分减去。当j遍历的时候,往右走不做计算,每当往左走的时候,就把母亲节点的右子树的和的部分减去。这样就符合上述的描述。也可以得到代码如下:

    public int sumRange(int i, int j) {int sum = root.sum;SegmentTreeNode left = root, right = root;sum -= traverse(left, i, true) + traverse(right, j, false);        return sum;}public int traverse(SegmentTreeNode node, int target, boolean isLeft) {int sum = 0;while (node != null) {int mid = (node.start + node.end) / 2;if (target <= mid) {if (!isLeft) sum += node.right != null ? node.right.sum : 0;node = node.left;} else {if (isLeft) sum += node.left != null ? node.left.sum : 0;node = node.right;}}return sum;}

当然,如果按照那篇文章的做法,应该需要做的就是自下往上的加和。就会变成以下这个样子

    public int sumRange(int i, int j) {return query(this.root, i, j);}public int query(SegmentTreeNode node, int start, int end) {if (start <= node.start && end >= node.end) {return node.sum;}int result = 0;int mid = (node.start + node.end) / 2;if (start <= mid) {result += query(node.left, start, end);}if (end > mid) {result += query(node.right, start, end);}return result;}

实际上也是差不多的。这个递归看上去好像会导致O(n)的复杂度其实不然。因为每一个节点总会有一边子节点终结于第一个if的条件或者下面两个if都碰不到。自己试试就知道了。

Segment Tree还有第二种实现方式,数组。这种实现方式和clrs里面介绍如何用数组构建heap其实是差不多的。在上面的链接里,它用了4 * n的大小的数组去实现这个segment tree。经过我自己研究,其实最多3 * n就可以了。大概解释是这样的。首先用数组构建segment tree最小理论空间是n + n / 2 + n / 4 .... ~= 2n。这是假设这是一个完全二叉树的情况下,也就是一个满二叉树。但是往往情况就是它会多一层零散的出来,多出来的这一层的最大限度是n个(就是假设这一层铺满的话也是n个,因为数组就只有n个那么多)。所以这种构造树的方式需要的空间为3n,并且很多情况下这个3n的空间有些元素是永远不会访问的。自己用size为10的数组画一下就知道了。

正如我所说,这种用数组构建树的方式非常接近数组构建heap。parent节点是treeArr[n]的话,那么它的左子树的节点就是treeArr[2 * n + 1], 右子树的节点就是treeArr[2 * n + 2]。这样就能保证你treeArr[0]就是这棵树的根节点。如果你用treeArr[1]作为根节点,那么左子树就是[2 * n],右子树就是[2 * n + 1]这样也可以的。和上面树结构是一样的,如果treeArr[n]表示的范围是 [i ... j],那么treeArr[2 * n + 1]表示的就是[i .. mid],treeArr[2 *n + 2]就是[mid + 1 .. j]。譬如一个大小为10的数组arr,其中treeArr[0]就是表示arr[0 .. 9]的和,treeArr[1]就表示arr[0..4]的和,treeArr[2]表示arr[5..9]的和,以此类推。代码的整体架构和树结构的代码差别不大,唯一差别就在于如何访问左右子树。

建树的代码如下:

    int[] treeArr;int end;public NumArray(int[] nums) {this.treeArr = new int[nums.length * 3];int start = 0;this.end = nums.length - 1;if (this.end >= 0)buildTree(nums, start, this.end, 0);}public int buildTree(int[] nums, int start, int end, int idx) {if (start == end) {this.treeArr[idx] = nums[start];} else {        int mid = (start + end) / 2;int left = buildTree(nums, start, mid, 2 * idx + 1);int right = buildTree(nums, mid + 1, end, 2 * idx + 2);this.treeArr[idx] = left + right;}return this.treeArr[idx];}

update节点的代码如下:

    public void update(int i, int val) {this.updateTree(0, this.end, i, val, 0);}public void updateTree(int start, int end, int i, int val, int idx) {if (start == end) {this.treeArr[idx] = val;} else {int mid = (start + end) / 2;if (i <= mid) {updateTree(start, mid, i, val, 2 * idx + 1);} else {updateTree(mid + 1, end, i, val, 2 * idx + 2);}this.treeArr[idx] = this.treeArr[idx * 2 + 1] + this.treeArr[idx * 2 + 2];}}

求和的代码如下:

    public int sumRange(int i, int j) {return querySum(i, j, 0, this.end, 0);}public int querySum(int i, int j, int start, int end, int idx) {if (start > j || end < i) return 0;if (i <= start && j >= end) {return this.treeArr[idx];} else {int mid = (start + end) / 2;return querySum(i, j, start, mid, idx * 2 + 1) + querySum(i, j, mid + 1, end, idx * 2 + 2);}}

代码和上面用真树的代码是很类似的。

下面给出另一种也是用数组建树的代码,这段代码来自于leetcode的答案以及某些youtuber的介绍。这可以认为是最优的segment tree的数组解法。因为它的空间占用率是最理想的2n,也就是我刚刚说的n + n / 2 + 4 / n + .... + 1。做到这一点的原因是,上面那种办法是根节点往下递归遍历叶子结点,所以会遍历到一些空叶子结点造就了空间的浪费。而这一种方式是相反的,它们先从叶子结点遍历,然后返回到根节点,所以遍历到的每一个节点都是实际存在的。

具体做法是:
1. 首先构建一个size为2n的数组treeArr。(n为原数组的大小)
2. 复制原数组到treeArr[n ... 2n - 1]里。
3. 然后做这样的循环:for i = n - 1, i >= 1, i-- do treeArr[i] = treeArr[2 * i] + treeArr[2 * i + 1]。

这样最后treeArr[1]就是根节点,treeArr[0]是无意义的。。

其实我不是很推荐这一种做法的,很多描述包括leetcode的,各位youtuber的,它们对于数组和树的关系的描述是错误的(仅仅是完全二叉树的情况下他们的描述才是对的),我还没有找到一种很合理解释这种建树方式的文章,所以下面是我自己的理解。这种构建树的方式,数组和左右子树的结构关联是不稳定的,这也是为了节省空间的一种tradeoff。举个例子,譬如说arr的size为5。那么treeArr的size就为10,那么treeArr[5 ~ 9]就对应arr[0 ~ 4]。然后treeArr[4]就是arr[3 ~ 4],treeArr[3]就是arr[1 ~ 2],但是到了treeArr[2]的时候,就奇怪了,其实它对应的是arr[0, 3, 4],然后treeArr[1]就是arr[0 ~ 4]的总和。这种现象其实可以理解这种构树方式的子树包含的范围是一个shift过的数组,也就是说它包含的依旧是一个连续的区间,但是shift过了,3下一个是4,4下一个就是回到头的0,否则这种解题方式都不能称之为segment tree,也无从通过leetcode的检测了。但解释起来真的很麻烦,树和数组的结构关系也很不直观,不是很推荐,我就放一下代码就好了。

    private int[] segTree;public NumArray(int[] nums) {this.segTree = new int[nums.length * 2];int n = nums.length;for (int i = n; i < this.segTree.length; i++) {this.segTree[i] = nums[i - n];}for (int i = n - 1; i > 0; i--) {this.segTree[i] = this.segTree[i * 2] + this.segTree[i * 2 + 1];}}public void update(int i, int val) {int index = this.segTree.length / 2 + i;// 在这里找到真正的节点位置。从叶子节点往上递归更新。this.segTree[index] = val;while (index > 1) {index /= 2;this.segTree[index] = this.segTree[index * 2] + this.segTree[index * 2 + 1];}}public int sumRange(int i, int j) {int l = i + this.segTree.length / 2;int r = j + this.segTree.length / 2;int res = 0;while (l <= r) {if (l % 2 == 1) {res += this.segTree[l];l++;}if (r % 2 == 0) {res += this.segTree[r];r--;}l /= 2;r /= 2;}return res;}

其实还有一种做法,叫做binary index tree。这不是一棵真的树。和上面用数组表达segment tree类似,这是一种用数组表达的数据结构。具体可以参考https://blog.csdn.net/Yaokai_AssultMaster/article/details/79492190 或者http://www.cnblogs.com/grandyang/p/4985506.html 。 我就不重复解释细节了,我也解释不清楚。总而言之,在bitarr里,对于某个节点bitarr[i],它的子节点是bitarr[i + (i & ~i)],它的父亲节点就是bitarr[i - (i & -i)]。在更新的时候,update(int i, int val) 以i为根节点,不停更新delta到子节点直到所有节点被更新完毕。在求和的时候,譬如prefixSum(int i), 则是反过来的,以i为叶子结点,不停往上归溯父亲节点,把所有路过的父亲节点加进去就可以了。要注意的是这个结构并不能直接求range(i, j),它能求的是从第一个index开始到某个index节点的prefix。所以range(i, j)其实也可以就变成了prefix(j) - prefix(i - 1)。而最初始的建树的过程其实就是不停update节点的过程。要注意的是update是基于delta的,所以初始建树就相当于对一颗空树不停update原始数组的值(也就是这个值就成为了delta)的过程。算法复杂度:update是O(logN),求和是O(logN),建树因为是update n次,所以是O(nlogn),当然,可以优化成O(N),下面给出的代码的建树过程就是O(N)的。

(差点忘了,为了配合binary index tree的位操作的性质,这个数组是1-based的array,所以所有index都要右移一位)

class NumArray {private int[] bitArr;private int[] nums;public NumArray(int[] nums) {bitArr = new int[nums.length + 1];this.nums = nums;for (int i = 0; i < nums.length; i++) {bitArr[i + 1] = nums[i];}for (int i = 1; i <= nums.length; i++) {int j = i + (i & -i);if (j <= nums.length) {bitArr[j] += bitArr[i];}}}public void update(int i, int val) {int delta = val - nums[i];int idx = i + 1;while (idx < bitArr.length) {bitArr[idx] += delta;idx = idx + (idx & -idx);}nums[i] = val;}public int sumRange(int i, int j) {return prefixSum(j) - prefixSum(i - 1);}public int prefixSum(int i) {int result = 0;i++;while (i > 0) {result += bitArr[i];i = i - (i & -i);}return result;}
}

[LC] 307. Range Sum Query - Mutable相关推荐

  1. 307. Range Sum Query - Mutable | 307. 区域和检索 - 数组可修改(数据结构:线段树,图文详解)

    题目 https://leetcode.com/problems/range-sum-query-mutable/ 吐槽官方题解 这题的 英文版官方题解,配图和代码不一致,而且描述不清:力扣国内版题解 ...

  2. 数据结构线段树介绍与笔试算法题-LeetCode 307. Range Sum Query - Mutable--Java解法

    此文首发于我的个人博客:zhang0peter的个人博客 LeetCode题解文章分类:LeetCode题解文章集合 LeetCode 所有题目总结:LeetCode 所有题目总结 线段树(Segme ...

  3. LeetCode Range Sum Query - Mutable(树状数组、线段树)

    问题:给出一个整数数组,求出数组从索引i到j范围内元素的总和.update(i,val)将下标i的数值更新为val 思路:第一种方式是直接根据定义,计算总和时直接计算从i到j的和 第二种方式是使用树状 ...

  4. leetcode307. Range Sum Query - Mutable

    题目要求 Given an integer array nums, find the sum of the elements between indices i and j (i ≤ j), incl ...

  5. leetcode 304. Range Sum Query 2D - Immutable |304. 二维区域和检索 - 矩阵不可变(二维前缀和问题)

    题目 https://leetcode.com/problems/range-sum-query-2d-immutable/ 题解 本题是 medium 难度,二维前缀和问题.相似题目有: Easy: ...

  6. 304. Range Sum Query 2D - Immutable

    题目: Given a 2D matrix matrix, find the sum of the elements inside the rectangle defined by its upper ...

  7. LeetCode Range Sum Query 2D - Immutable

    问题:给出一个二维数组,求其子矩阵的和 思路: 第一种方式暴力法,直接遍历 第二种方式针对每行求前缀和 第三种方法是dp(x,y)=dp(x-1,y)+dp(x,y-1)-dp(x-1,y-1)+ma ...

  8. [LeetCode] 303. Range Sum Query - Immutable

    https://leetcode.com/problems/range-sum-query-immutable/ 用一个 sum 数组,sum[i] -- nums 中选出前 i 个元素,求和所得到的 ...

  9. Leetcode题目:Range Sum Query - Immutable

    题目: Given an integer array nums, find the sum of the elements between indices i and j (i ≤ j), inclu ...

最新文章

  1. Facebook发布Detectron2,下一个万星目标检测新框架
  2. 百度快照被劫持跳转的解决办法
  3. 【Pygame小游戏】剧情流推荐:什么样的游戏才能获得大家的喜欢呢?(魔鬼恋人、霸总娇妻版)
  4. php内存映射,如何用ZwMapViewOfSection将Driver分配的内存映射到App空间?
  5. 字体大小 js 控制
  6. 结对开发 随机产生数组并求最大子数组的和
  7. sleep、wait、yield、join区别
  8. 升级 卸载 ubuntu的kernel版本
  9. 初始化与赋值哪个效率高?
  10. 在 Windows下使用 fastText
  11. Linux驱动开发|电容触摸屏
  12. 【Windows】Mathpix Snip-公式神器
  13. pkg学习--使用pkg打包应用
  14. Spring中实现HTTP缓存
  15. 测试的意义并不是能找到全部的缺陷
  16. 极客创新大赛|微创机器人号探索飞船即将启航
  17. 学python安装-Python学习笔记-Python安装
  18. TCP四次挥手及原因
  19. 福州农信计算机类待遇怎么样,福建农村信用社联合社待遇怎么样?农信社工资如何...
  20. The application was unable to start correctly (0xc000007b)的勉强解决方案

热门文章

  1. 新款MacBook Pro 13英寸 支持Wi-Fi 6,搭载M1 芯片!
  2. ntpdate同步更新时间
  3. 国产智能机利润调查:中高端硬件成本仅千元
  4. Python_Openpyxl
  5. 为什么Scrum赢了
  6. Java实现斐波那契数列与黄金分割比精确位数问题
  7. 重磅:2022年国家社科基金立项名单公示!| 附完整名单
  8. concat()函数用法
  9. 我的世界服务器死亡信息在哪看,我的世界:死亡记录点?不需要地图,就可以看到“死”在哪里!...
  10. android RecyclerView一步步打造分组效果、类似QQ分组、折叠菜单、分组效果(二)