目录
剑指 Offer 专项突击版刷题笔记 2
第 4 天 数组
剑指 Offer II 010. 和为 k 的子数组
给定一个整数数组和一个整数 k ,请找到该数组中和为 k 的连续子数组的个数。
示例 1:
输入:nums = [1,1,1], k = 2
输出: 2
解释: 此题 [1,1] 与 [1,1] 为两种不同的情况
示例 2:
输入:nums = [1,2,3], k = 3
输出: 2
提示:
1 <= nums.length <= 2 * 104
-1000 <= nums[i] <= 1000
-107 <= k <= 107
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/QTMn0o
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
暴力解失败
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
int ans = 0;
int sum = 0;
for(int i = 0; i < nums.size(); ++i){
sum = 0;
for(int j = i; j < nums.size(); ++j){
sum += nums[j];
if(sum == k) ++ans;
}
}
return ans;
}
};
类比之前的,想到了滑动窗口
但是之前那个求积为 k 的子数组的题,是因为我每多考虑一个数,积就会单调递增,所以我可以在移动滑动窗口的右边界的时候,发现积等于 target 就停止
现在这个数组,每多考虑一个数,和不一定是单调递增的,即使发现了和等于 target 也不能停止(因为你不知道它是否是先增后减或者先减后增)
前缀和 + 哈希表优化
官方题解
这里,[j, i]
子数组的和为 k 可以转化为 pre[i] - pre[j-1] == k
那么下标 j
需要满足 pre[j-1] == pre[i] - k
那么我们按照原来的双循环的思路,我们应该是
int ans = 0;
vector<int> pre = 0;
for(int i = 0; i < nums.size(); ++i){
if(i > 0) pre[i] += pre[i - 1];
pre[i] += nums[i];
}
for(int i = 0; i < nums.size(); ++i){
for(int j = 0; j <= i; ++j){
if(j == 0){
if(0 == pre[i] + k) ++ans;
}
else if(pre[j - 1] == pre[i] + k) ++ans;
}
}
return ans;
更进一步,这个 pre 数组的更新可以和 j 的查找放在同一个 i 循环中,因为查找 j 的时候用不到以后的没遍历到的 i
int ans = 0;
vector<int> pre = 0;
for(int i = 0; i < nums.size(); ++i){
// update pre array
if(i > 0) pre[i] += pre[i - 1];
pre[i] += nums[i];
// find [j, i]
for(int j = 0; j <= i; ++j){
if(j == 0){
if(0 == pre[i] + k) ++ans;
}
else if(pre[j - 1] == pre[i] + k) ++ans;
}
}
return ans;
但是我们之所以要用双循环这样的遍历来查找一个数,是因为数组他是具有这个顺序访问的特性
虽然以前不常用……但是现在很合适的,就是使用哈希表
如果我们创建了一个哈希表,以某个数组的元素值为键,以这个数组的元素值的出现次数为值
建立的时候可能是 map.insert(pair<int, int>(array[i], 1))
但是你不是必须要用 array[i]
去查 map
呀
你可以用 map.find(array[i] - k)
或者任何值……都是可以的……
所以说我之前的思维就有点被限制了
在外层 i 指向某一个数的时候,就可以更新 map[pre]++;
然后直接使用 pre - k
去查 map
因为我看这个式子 pre[j-1] == pre[i] - k
的时候,我下意识地就以为我是要遍历 j 了,实际上不是,因为无论 pre[i] - k
还是 pre[j-1]
都是一个哈希表的键而已,是键的话那我直接查就好了
然后又因为查找 j 的时候用不到以后的没遍历到的 i,所以一边更新 pre 数组一边查找是没有问题的
感觉这个思路最重要的还是能够把一个区间的查找目标转化为一个数组上的操作
这个转换应该是从加减法的线性可加性得到的……
一开始我写的
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
unordered_map<int, int> sum_map;
int sum = 0;
int ans = 0;
for(int i = 0; i < nums.size(); ++i){
sum += nums[i];
++sum_map[sum];
if(sum_map.find(sum - k) != sum_map.end())
ans += sum_map[sum - k];
}
return ans;
}
};
后面发现我需要初始化 map[0] = 1;
因为 map[0] 就相当于 j == 0
的情况
然后我又发现计算出 sum
之后,对 sum_map
的更新要放在查找 j 之后
这主要是为了避免 k == 0
的情况
这时,如果计算出 sum
之后,立即对 sum_map
更新,那么就相当于找到 i == j
的情况
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
unordered_map<int, int> sum_map;
int sum = 0;
int ans = 0;
sum_map[0] = 1;
for (int& num : nums) {
sum += num;
if (sum_map.find(sum - k) != sum_map.end())
ans += sum_map[sum - k];
++sum_map[sum];
}
return ans;
}
};
剑指 Offer II 011. 0 和 1 个数相同的子数组
给定一个二进制数组 nums , 找到含有相同数量的 0 和 1 的最长连续子数组,并返回该子数组的长度。
示例 1:
输入: nums = [0,1]
输出: 2
说明: [0, 1] 是具有相同数量 0 和 1 的最长连续子数组。
示例 2:
输入: nums = [0,1,0]
输出: 2
说明: [0, 1] (或 [1, 0]) 是具有相同数量 0 和 1 的最长连续子数组。
提示:
1 <= nums.length <= 105
nums[i] 不是 0 就是 1
一开始我本来想写堆栈的
但是我想了一下,感觉我要是顺序地扫过去,不再管已经用掉的数字,就会算错
比如最后一种情况,将前面的 1 1 0 0 算为 4 之后,假设我通过堆栈中是否还有值来判断我这一次得到的连续子数组是否与之前的连续子数组能够合并
那么之后的 1 1 1 0 0 其实还可以与前面的 1 1 0 0 一起考虑,得到 一个 0 0 1 1 1 0 的连续子数组
但是如果前面的 1 1 0 0 遍历过了一遍就不再考虑的话,就会忽略这种情况
所以看上去我这个想法也不行……
于是我尝试使用上一题的解法
但是上一题是能够在双层循环中找一个数,所以相当于单循环
这题如果要用两个数组,一个数组记录从位置 0 到 i 中出现的 0 的个数,一个数组记录从位置 0 到 i 中出现的 1 的个数
那么最后还是要在双层循环中判断 [i,j] 中 0 和 1 的个数……
题目还有一个,不是 0 就是 1 的条件没用上……
这个感觉像是位运算相关的
前缀和 + 哈希表优化
之后看了题解,说是把数组中的 0 转化成 - 1,然后转化成和为 0 的最长连续子数组的问题
但是这个哈希表的设置跟之前是有点不一样的
为了整理思路,还是从单独使用前缀和开始
单独使用前缀和就是暴力双重循环
class Solution {
public:
int findMaxLength(vector<int>& nums) {
vector<int> sum(nums.size(), 0);
int ans = 0;
int tmp = 0;
for(int i = 0; i < nums.size(); ++i){
if(nums[i] == 0) tmp = -1;
else tmp = 1;
if(i == 0){
sum[0] = tmp;
continue;
}
sum[i] = sum[i-1] + tmp;
}
for(int i = 0; i < nums.size(); ++i){
for(int j = 0; j < i; ++j){
if(j == 0){
if(sum[i] == 0)
ans = max(ans, i - j + 1);
}
else{
if(sum[i] - sum[j-1] == 0)
ans = max(ans, i - j + 1);
}
}
}
return ans;
}
};
由此写出前缀和 + 哈希表的方法
class Solution {
public:
int findMaxLength(vector<int>& nums) {
unordered_map<int, int> num_map;
int sum = 0;
int ans = 0;
int tmp = 0;
// 第一次找到 0 之前
num_map[0] = -1;
for (int i = 0; i < nums.size(); ++i) {
if (nums[i] == 0) tmp = -1;
else tmp = 1;
if (i == 0) {
sum = tmp;
num_map[sum] = i;
continue;
}
sum += tmp;
if(num_map.find(sum) != num_map.end())
ans = max(ans, i - num_map[sum]);
num_map[sum] = i;
}
return ans;
}
};
但是这个会报错……
输入:
[0,1,0,1]
输出:
2
预期结果:
4
问题出在 i = 3 时,sum = 0,查找到的 num_map[sum] == 1
但是我们的期望是长度为 4
所以我们这个时候找到的应该是 num_map[sum] == -1
这就说明或许我需要在更新 num_map 的时候取 min
于是写成
class Solution {
public:
int findMaxLength(vector<int>& nums) {
unordered_map<int, int> num_map;
int sum = 0;
int ans = 0;
int tmp = 0;
// 第一次找到 0 之前
num_map[0] = -1;
for (int i = 0; i < nums.size(); ++i) {
if (nums[i] == 0) tmp = -1;
else tmp = 1;
if (i == 0) {
sum = tmp;
num_map[sum] = i;
continue;
}
sum += tmp;
if(num_map.find(sum) != num_map.end())
ans = max(ans, i - num_map[sum]);
else
num_map[sum] = i;
}
return ans;
}
};
通过判断 num_map.find(sum) != num_map.end()
来对 num_map[sum]
取 min
这就通过了
剑指 Offer II 012. 左右两边子数组的和相等
给你一个整数数组 nums ,请计算数组的 中心下标 。
数组 中心下标 是数组的一个下标,其左侧所有元素相加的和等于右侧所有元素相加的和。
如果中心下标位于数组最左端,那么左侧数之和视为 0 ,因为在下标的左侧不存在元素。这一点对于中心下标位于数组最右端同样适用。
如果数组有多个中心下标,应该返回 最靠近左边 的那一个。如果数组不存在中心下标,返回 -1 。
示例 1:
输入:nums = [1,7,3,6,5,6]
输出:3
解释:
中心下标是 3 。
左侧数之和 sum = nums[0] + nums[1] + nums[2] = 1 + 7 + 3 = 11 ,
右侧数之和 sum = nums[4] + nums[5] = 5 + 6 = 11 ,二者相等。
示例 2:
输入:nums = [1, 2, 3]
输出:-1
解释:
数组中不存在满足此条件的中心下标。
示例 3:
输入:nums = [2, 1, -1]
输出:0
解释:
中心下标是 0 。
左侧数之和 sum = 0 ,(下标 0 左侧不存在元素),
右侧数之和 sum = nums[1] + nums[2] = 1 + -1 = 0 。
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/tvdfij
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
一眼双指针……
但是我一开始没有考虑到负数的问题
class Solution {
public:
int pivotIndex(vector<int>& nums) {
int left = 0, right = nums.size() - 1;
int left_sum = 0, right_sum = 0;
while(left < right){
if(left_sum >= right_sum){
right_sum += nums[right--];
}
else{
left_sum += nums[left++];
}
}
if(left_sum == right_sum) return left;
else return -1;
}
};
输入:
[-1,-1,-1,-1,-1,0]
输出:
-1
预期结果:
2
右边先动,取到 -1 之后,右边的 sum 更加小,就会想要更多……结果最后一路走到了最左
后面我又想了一个方法
我只看 left_sum - right_sum
,它大于 0 的时候,那么我有两种方式,减少 left_sum
或者增加 right_sum
小于 0 同理
好吧,那么不减也可以……就是有多种选择
于是我写成
class Solution {
public:
int pivotIndex(vector<int>& nums) {
int left = 0, right = nums.size() - 1;
int left_sum = 0, right_sum = 0;
while(left < right){
if(left_sum >= right_sum){
// 右指针先检查,以满足找到尽可能左的中心坐标
if(nums[right] > 0)
right_sum += nums[right--];
else
left_sum += nums[left++];
}
else{
// 右指针先检查,以满足找到尽可能左的中心坐标
if(nums[right] < 0)
right_sum += nums[right--];
else
left_sum += nums[left++];
}
}
if(left_sum == right_sum) return left;
else return -1;
}
};
但是遇到
输入:
[2,1,-1]
输出:
-1
预期结果:
0
这个时候我才发现,他这个中心坐标指向的元素是不包含在左侧和或者右侧和之中的
为了通过这个,我写成
class Solution {
public:
int pivotIndex(vector<int>& nums) {
int left = -1, right = nums.size();
int left_sum = 0, right_sum = 0;
while (left < right) {
if (left_sum > right_sum) {
// 右指针先检查,以满足找到尽可能左的中心坐标
if (nums[right - 1] >= 0)
right_sum += nums[--right];
else
left_sum += nums[++left];
}
else {
// 右指针先检查,以满足找到尽可能左的中心坐标
if (nums[right - 1] <= 0)
right_sum += nums[--right];
else
left_sum += nums[++left];
}
}
if (left_sum == right_sum) return left;
else return -1;
}
};
但是还是错了
输入:
[-1,-1,-1,-1,0,1]
输出:
-1
预期结果:
1
我感觉应该是问题出在 左右侧的和相等的时候,该怎么判断指针移动
于是我默认都是右指针移动
class Solution {
public:
int pivotIndex(vector<int>& nums) {
int left = -1, right = nums.size();
int left_sum = 0, right_sum = 0;
while (left < right) {
if (left_sum > right_sum) {
// 右指针先检查,以满足找到尽可能左的中心坐标
if (nums[right - 1] >= 0)
right_sum += nums[--right];
else
left_sum += nums[++left];
}
else if (left_sum < right_sum) {
// 右指针先检查,以满足找到尽可能左的中心坐标
if (nums[right - 1] <= 0)
right_sum += nums[--right];
else
left_sum += nums[++left];
}
else {
// 右指针先检查,以满足找到尽可能左的中心坐标
right_sum += nums[--right];
}
if (left == nums.size() - 1 || right == 0)
break;
}
if (left_sum == right_sum) return left;
else return -1;
}
};
这个写法我是把指针放到了数组范围外面
但是还是过不了 2, 1, -1 的情况
问题还是出在左右侧和相等的时候……right 指针遍历了 -1, 1 之后左右侧的和又相等了,这时我又让 right 向左移动了……
那么或许我觉得我应该是指针的位置设置有问题,一开始就应该让指针占位
class Solution {
public:
int pivotIndex(vector<int>& nums) {
// 左侧和不包含左指针指向的元素,右侧和不包含右指针指向的元素
int left = 0, right = nums.size()-1;
int left_sum = 0, right_sum = 0;
while (left < right) {
if (left_sum > right_sum) {
// 右指针先检查,以满足找到尽可能左的中心坐标
if (nums[right] >= 0)
right_sum += nums[right--];
else
left_sum += nums[left++];
}
else if (left_sum < right_sum) {
// 右指针先检查,以满足找到尽可能左的中心坐标
if (nums[right] <= 0)
right_sum += nums[right--];
else
left_sum += nums[left++];
}
else {
// 右指针先检查,以满足找到尽可能左的中心坐标
right_sum += nums[right--];
}
}
if (left_sum == right_sum) return left;
else return -1;
}
};
输入:
[-1,-1,-1,0,1,1]
输出:
-1
预期结果:
0
这时的问题是,左侧和知道自己比右侧和小了之后,首先看右侧和能不能减小,不能的话才让左侧指针移动
……
我的感觉是,这就说明我这种简单的贪心的双指针似乎是行不通的
于是还是老老实实地用 (0,n) (1,n-1), (2, n-2) 这种组合了……
class Solution {
public:
int pivotIndex(vector<int>& nums) {
int left_sum = 0, right_sum = 0;
for (int& num : nums) {
right_sum += num;
}
for (int i = 0; i < nums.size(); ++i) {
// 左侧和不包含左指针指向的元素,右侧和不包含右指针指向的元素
// 因此在 right_sum 更新之后,left_sum 更新之前判断
right_sum -= nums[i];
if (left_sum == right_sum) return i;
left_sum += nums[i];
}
return -1;
}
};
看了一下官方题解,虽然他总结的很简练……但是思路是差不多的
剑指 Offer II 013. 二维子矩阵的和
暴力解法的 o(mn) 超时
class NumMatrix {
private:
vector<vector<int>> _mat;
public:
NumMatrix(vector<vector<int>>& matrix) {
_mat = matrix;
}
int sumRegion(int row1, int col1, int row2, int col2) {
int sum = 0;
for(int i = row1; i <= row2; ++i){
for(int j = col1; j <= col2; ++j){
sum += _mat[i][j];
}
}
return sum;
}
};
/**
* Your NumMatrix object will be instantiated and called as such:
* NumMatrix* obj = new NumMatrix(matrix);
* int param_1 = obj->sumRegion(row1,col1,row2,col2);
*/
常见的计算和的方法就是使用前缀和之差
所以这里可以使用一维前缀和,具体来说,就是对于每一行都计算第 j 列的位置上的前缀和
class NumMatrix {
private:
vector<vector<int>> _mat;
public:
NumMatrix(vector<vector<int>>& matrix) {
_mat = matrix;
if(_mat.size() == 0) return;
for(int i = 0; i < _mat.size(); ++i){
for(int j = 1; j < _mat[0].size(); ++j){
// 一维前缀和
_mat[i][j] += _mat[i][j-1];
}
}
}
int sumRegion(int row1, int col1, int row2, int col2) {
int sum = 0;
for(int i = row1; i <= row2; ++i){
if(col1 == 0) sum += _mat[i][col2];
else sum += (_mat[i][col2] - _mat[i][col1 - 1]);
}
return sum;
}
};
/**
* Your NumMatrix object will be instantiated and called as such:
* NumMatrix* obj = new NumMatrix(matrix);
* int param_1 = obj->sumRegion(row1,col1,row2,col2);
*/
执行用时:228 ms, 在所有 C++ 提交中击败了18.37% 的用户
内存消耗:65.4 MB, 在所有 C++ 提交中击败了63.51% 的用户
此时的时间复杂度是 o(m),这只是一维前缀和的情况,如果使用二维前缀和,还可以降到 o(1)
但是问题是这个二维具体是怎么二维
一开始我还以为是把二维坐标转化成一维坐标
后面发现这样的话在计算某个子矩阵的元素和的时候也不好算
二维前缀和
然后看了题解才知道原来他要算 f(i,j) 是右下角为 (i,j) 的子矩阵的元素和,这样的前缀和
这样确实容易理解了
取任意一个子矩阵也只需要稍微运算一下面积就好了
class NumMatrix {
private:
vector<vector<int>> _mat;
public:
NumMatrix(vector<vector<int>>& matrix) {
_mat = matrix;
if (_mat.size() == 0) return;
for (int i = 0; i < _mat.size(); ++i) {
for (int j = 1; j < _mat[0].size(); ++j) {
// 一维前缀和
_mat[i][j] += _mat[i][j - 1];
}
}
for (int i = 1; i < _mat.size(); ++i) {
for (int j = 0; j < _mat[0].size(); ++j) {
// 二维前缀和
_mat[i][j] += _mat[i - 1][j];
}
}
}
int sumRegion(int row1, int col1, int row2, int col2) {
int sum = 0;
int S = 0, S_1 = 0, S_2 = 0, S_3 = 0;
S = _mat[row2][col2];
if (col1 > 0) {
S_1 = _mat[row2][col1 - 1];
}
if (row1 > 0) {
S_2 = _mat[row1 - 1][col2];
}
if (row1 > 0 && col1 > 0) {
S_3 = _mat[row1 - 1][col1 - 1];
}
sum = S - S_1 - S_2 + S_3;
return sum;
}
};
执行用时:156 ms, 在所有 C++ 提交中击败了53.26% 的用户
内存消耗:65.3 MB, 在所有 C++ 提交中击败了92.39% 的用户
或者这个初始化还可以合并一下
NumMatrix(vector<vector<int>>& matrix) {
_mat = matrix;
if (_mat.size() == 0) return;
for (int i = 0; i < _mat.size(); ++i) {
for (int j = 0; j < _mat[0].size(); ++j) {
// 一维前缀和
if(j > 0) _mat[i][j] += _mat[i][j - 1];
}
for (int j = 0; j < _mat[0].size(); ++j) {
// 二维前缀和
if(i > 0) _mat[i][j] += _mat[i - 1][j];
}
}
}
但是确实也就这样了……我觉得这样比较简单易懂一点