Leetcode-307-区域和检索-数组可修改[线段树]
|字数总计:2.3k|阅读时长:10分钟|阅读量:|
Leetcode-307-区域和检索-数组可修改
题目
给你一个数组 nums ,请你完成两类查询。
-
其中一类查询要求 更新 数组 nums 下标对应的值
-
另一类查询要求返回数组 nums 中索引 left 和索引 right 之间( 包含 )的nums元素的 和 ,其中 left <= right
实现 NumArray 类:
-
NumArray(int[] nums) 用整数数组 nums 初始化对象
-
void update(int index, int val) 将 nums[index] 的值 更新 为 val
-
int sumRange(int left, int right) 返回数组 nums 中索引 left 和索引 right 之间( 包含 )的nums元素的 和 (即,nums[left] + nums[left + 1], …, nums[right])
解题思路
暴力法
根据题意不难直接写出暴力解决方案, 当然直接TLE
可以得出, 更新操作时间复杂度O(1), 求和操作时间复杂度O(r−l)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class NumArray { public: vector<int>& nums; NumArray(vector<int>& nums): nums(nums) {}
void update(int index, int val) { nums[index] = val; }
int sumRange(int left, int right) { int s = 0; for(int i=left;i<=right;i++) s+=nums[i]; return s; } };
|
线段树
线段树讲解
线段树是算法竞赛中常用的用来维护 区间信息 的数据结构。
线段树可以在O(log(N))的时间复杂度内实现单点修改、区间修改、区间查询(区间求和,求区间最大值,求区间最小值)等操作。
如下图, 有数组[10, 11, 12, 13, 14] 我们可以把区间[1, 5]划分为2份, 4份, 5份, 直到划分为单点
对区间[start, end]的划分算法如下,
计算区间中点mid = (start + end) / 2
-
划分得到左区间[start, mid]
-
划分得到右区间[mid+1, end]
最后可得到如下的一棵树,
数据结构定义
由此, 可轻易写出如下结构体定义, 表示线段树上的某个区间结点
1 2 3 4 5 6 7 8 9 10 11 12
| struct TreeNode { int start; int end; int sum; TreeNode *left; TreeNode *right;
void display(int deep) { for (int i = 0; i < deep; i++) cout << " "; cout << "[" << start << ", " << end << "] (" << sum << ")" << endl; } };
|
建立线段树
建立线段树即给定一个数组, 建立一棵线段树
算法流程
-
开辟树根结点
-
计算中点mid
-
判断区间左右端点大小
-
start < end: 即区间还可以继续划分
-
对区间[start, mid]建立线段树作为根结点的左子树
-
对区间[mid+1, end]建立线段树作为根结点的右子树
-
更新树根结点的sum值为两子树sum之和
-
start == end: 区间已为单点区间, 不可继续划分
-
置左右子树为空
-
更新树根结点的sum值为单点元素值
-
返回树根结点即为所建立的线段树
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| TreeNode *buildTree(vector<int> nums, int start, int end) { int mid = (end - start) / 2 + start; TreeNode *root = new TreeNode; root->start = start; root->end = end; if (start < end) { root->left = buildTree(nums, start, mid); root->right = buildTree(nums, mid + 1, end); root->sum = root->left->sum + root->right->sum; } else if (start == end) { root->left = root->right = nullptr; root->sum = nums[start]; }
return root; }
|
单点更新
线段树的单点更新即更新指定下标处的值, 修改这棵线段树
算法流程
已知update函数可以修改某个线段树中指定下标的值
-
判断是否为叶子结点, 如果是叶子结点
- 直接更新sum值
-
如果不是叶子结点
-
更新子线段树
-
若下标在左子树, 则调用update函数更新左子线段树
-
若下标在右子树, 则调用update函数更新右子线段树
-
最后更新区间和, 该结点的sum值为两子线段树的sum值之和
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| void update(TreeNode *node, int index, int val) { if (node->start == node->end) { node->sum = val; return; } else { int mid = (node->end - node->start) / 2 + node->start; if (index <= mid) { update(node->left, index, val); } else { update(node->right, index, val); } node->sum = node->left->sum + node->right->sum; } }
|
区间求和
线段树的区间求和即给定一个区间[left, right], 求该区间的所有值之和
算法流程
已知query函数可以求处区间[left, right]的所有值之和
-
若所求区间正好为单点, 即叶子结点, 那么就直接返回该结点的sum值
-
计算线段树根结点的中点mid值
-
若所求区间整体在树根的左子线段树区间(right <= mid), 那么直接在左子线段树中求query操作的值为返回结果
-
若所求区间整体在树根的右子线段树区间(left>mid), 那么直接在右子线段树中求query操作的值为返回结果
-
最后一种情况就是所求区间一部分在左边, 一部分在右边, 需要分割求解
-
在树根的左子线段树中计算[left, mid]的query
-
在树根的右子线段树中计算[mid+1, right]的query
-
左右区间的query结果求和作为返回结果
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| int query(TreeNode *node, int left, int right) { if (node->start == left && node->end == right) return node->sum; int mid = (node->end - node->start) / 2 + node->start;
if (right <= mid) return query(node->left, left, right);
if (left > mid) return query(node->right, left, right);
return query(node->left, left, mid) + query(node->right, mid + 1, right); }
|
整体代码
C++实现
注意, 这里的线段树代码仍然会TLE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
| class NumArray { struct TreeNode { int start; int end; int sum; TreeNode *left; TreeNode *right;
void display(int deep) { for (int i = 0; i < deep; i++) cout << " "; cout << "[" << start << ", " << end << "] (" << sum << ")" << endl; } };
public: TreeNode *root; NumArray(vector<int>& nums) { root = buildTree(nums, 0, nums.size()-1); }
void update(int index, int val) { update(root, index, val); }
int sumRange(int left, int right) { return query(root, left, right); }
TreeNode *buildTree(vector<int> nums, int start, int end) { int mid = (end - start) / 2 + start; TreeNode *root = new TreeNode; root->start = start; root->end = end; if (start < end) { root->left = buildTree(nums, start, mid); root->right = buildTree(nums, mid + 1, end); root->sum = root->left->sum + root->right->sum; } else if (start == end) { root->left = root->right = nullptr; root->sum = nums[start]; }
return root; }
void update(TreeNode *node, int index, int val) { if (node->start == node->end) { node->sum = val; return; } else { int mid = (node->end - node->start) / 2 + node->start; if (index <= mid) { update(node->left, index, val); } else { update(node->right, index, val); } node->sum = node->left->sum + node->right->sum; } }
int query(TreeNode *node, int left, int right) { if (node->start == left && node->end == right) return node->sum; int mid = (node->end - node->start) / 2 + node->start;
if (right <= mid) return query(node->left, left, right);
if (left > mid) return query(node->right, left, right);
return query(node->left, left, mid) + query(node->right, mid + 1, right); } };
|
Java实现
但是同样的算法如果我们换成java实现却能AC, 很迷
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| class NumArray { class TreeNode { int start, end, sum; TreeNode left, right; }
TreeNode buildTree(int[] nums, int start, int end) { int mid = (end - start) / 2 + start; TreeNode root = new TreeNode(); root.start = start; root.end = end; if (start < end) { root.left = buildTree(nums, start, mid); root.right = buildTree(nums, mid + 1, end); root.sum = root.left.sum + root.right.sum; } else if (start == end) { root.left = null; root.right = null; root.sum = nums[start]; }
return root; }
int query(TreeNode node, int left, int right) { if (node.start == left && node.end == right) return node.sum; int mid = (node.end - node.start) / 2 + node.start;
if (right <= mid) return query(node.left, left, right);
if (left > mid) return query(node.right, left, right);
return query(node.left, left, mid) + query(node.right, mid + 1, right); }
void update(TreeNode node, int index, int val) { if (node.start == node.end) { node.sum = val; return; } else { int mid = (node.end - node.start) / 2 + node.start; if (index <= mid) { update(node.left, index, val); } else { update(node.right, index, val); } node.sum = node.left.sum + node.right.sum; } }
TreeNode root;
public NumArray(int[] nums) { root = buildTree(nums, 0, nums.length - 1); }
public void update(int index, int val) { update(root, index, val); }
public int sumRange(int left, int right) { return query(root, left, right); } }
|
分析总结
估计大概率是存储结构的问题(可能new性能不高), 由于考虑到线段树是一颗完全二叉树, 故可考虑优化成堆存储(即通过数组存储), 减少通过new开辟内存的次数, 完全可以一次性开辟足够存放的数组.