Leetcode 307. Range Sum Query - Mutable
Given an integer array nums, find the sum of the elements between indices i and j (i ≤ j), inclusive.
The update(i, val) function modifies nums by updating the element at index i to val.
Example:
Given nums = [1, 3, 5] sumRange(0, 2) -> 9 update(1, 2) sumRange(0, 2) -> 8
Note:
- The array is only modifiable by the update function.
- You may assume the number of calls to update and sumRange function is distributed evenly.
之前没有接触过,上来就暴力算法,结果是AC不了的。
解决方案
方案1,每次求和,直接遍历子数组进行求和。每次更新,直接根据下标更新元素值。求和操作时间复杂度为 O(n), 更新操作时间复杂度为O(1)。--> 不能AC
方案2,采用线段树存储原数组以及中间结果。
方案3, 采用树状数组的方案(待更。。)
则用 线段树 解法 或者 树状数组
首先了解一下线段树的概念(之前没有接触过):线段树的本质是一颗二叉树,每一个节点代表着一个区间。子节点区间都是父节点区间的二分子集。线段树适合的题型有非常鲜明的特点,即一个大区间的问题可以分解为若干小区间来求解并归并。比如一个大区间的SUM,等于两个子区间的sum的和;再比如,一个大区间的max,等于两个子区间的max的最大值。构建线段树的时间复杂度、空间复杂度都是O(2n),查询、更新(单个元素)是o(logn),总和性能不错。线段树的模板比较长,常见的操作包括buildTree, modifyTree 和queryTree。但是思路非常清晰简洁。如果写熟练了,并不比TrieNode模板慢。
概述: 类似于区间树,它在各个节点保存一个线段(数组中的一段子数组),主要用于高校解决连续区间的查询问题,由于二叉树的特性,基本能够保持每个操作的复杂度为O(logn)。线段树的每个节点表示一个区间,子节点则分别表示父节点的左右半区间,例如,父亲的区间是[a,b], 那么(c = (a+b)/2)左儿子的区间是[a,c], 右儿子的区间是[c+1, b].
由于线段树的父节点区间是平均分割到左右子树,因此线段树是完全二叉树,对于包含n个叶子节点的完全二叉树,它一定有n-1个非叶节点,总共2n-1个节点,因此存储线段是需要的空间复杂度是O(n)。
非递归的解题思路:视频教程
/* 线段树,类似于区间树,主要用来高效解决连续区间的动态查询问题,比如求数组区间的最大值、最小值、和的一些问题 */ class NumArray { private: int size; // 存贮nums的长度值 int *arr; // 用来创建线段树的数组存储,这俩个值需要在构造函数中初始化 public: void buildTree(vector<int>& nums){ // 用来创建线段树 for (int i = size, j = 0; j < nums.size(); i++, j++){ // 初始化线段树的后半部分,也就是树的叶子节点(数组原本的值) arr[i] = nums[j]; } for (int i = size - 1; i > 0; i--){ // 初始化线段树的前半部分,即树的非叶子节点(父节点),节点的值保存 其左右子节点的值的和 arr[i] = arr[i * 2] + arr[i * 2 + 1]; } } NumArray(vector<int> nums) { // 构造函数,注意私有成员变量的使用 size = nums.size(); if (size > 0){ arr = new int[size * 2]; // 动态开辟数组空间, 为什么创建的线段树是原来数组长度的2倍值 // 对于包含n个叶子节点的完全二叉树,它一定有n-1个非叶节点,总共2n-1个节点? buildTree(nums); // 首先先建造一个树 } } void update(int i, int val) { i += size; // 转换到树中的新的索引值 arr[i] = val; // 更新叶子节点的值 while ( i > 0){ // 更新父节点的值,先判断是左子节点还是右子节点 int left = i; int right = i; if (i % 2 == 0){ right++; }else{ left--; } arr[i/2] = arr[left] + arr[right]; // 更新父结点的值 i /= 2; } } int sumRange(int i, int j) { // 因为父节点存放的肯定是从一个树的左子结点到另一个的右子节点,所以要先判断i和j 是不是左子节点和右子结点 i += size; // 先将索引转换到线段树的数组索引中 j += size; int sum = 0; while ( i <= j){ // 相当于是找最小公共父结点??? if (i % 2 != 0){ // 若i是右子结点,则加上这个值,i加1之后变成左子结点 sum += arr[i]; ++i; } if (j % 2 == 0){ // 若j是左子结点,则加上这个值,j减1之后变成右子结点 sum += arr[j]; --j; } i /= 2; // i 和 j 往上更新 j /= 2; } return sum; // 为什么返回的是sum值,不是结点值? 最后一次i 和 j相等的时候 j变为左子结点,这时候会把arr[j]的值加上 } }; /** * Your NumArray object will be instantiated and called as such: * NumArray obj = new NumArray(nums); * obj.update(i,val); * int param_2 = obj.sumRange(i,j); */
递归解法(线段树):
树状数组: