977.有序数组的平方 ,209.长度最小的子数组 ,59.螺旋矩阵II ,总结
有序数组的平方
暴力
每个数平方之后,快排,O(n+logn)。
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
for(int i = 0; i < nums.size(); i++) {
nums[i] = nums[i] * nums[i];
}
sort(nums.begin(), nums.end());
return nums;
}
};
双指针
依照左右两端绝对值最大,平方最大,从两边往中间搜索,需要新空间,O(n)。
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
vector <int> ans(nums.size(), 0);
int left = 0;
int right = nums.size() - 1;
int index = right;
while(left <= right) {
int _left = nums[left] * nums[left];
int _right = nums[right] * nums[right];
if(_left <= _right) {
ans[index--] = _right;
right--;
}
else {
ans[index--] = _left;
left++;
}
}
return ans;
}
};
C#
public class Solution {
public int[] SortedSquares(int[] nums) {
int left = 0;
int right = nums.Length - 1;
int index = right;
int []ans = new int[nums.Length];
while(left <= right) {
int _left = nums[left] * nums[left];
int _right = nums[right] * nums[right];
if(_left <= _right) {
ans[index--] = _right;
right--;
} else {
ans[index--] = _left;
left++;
}
}
return ans;
}
}
长度最小的子数组
暴力
遍历两次,第一次遍历每个元素,第二次往后找连续元素。
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int max = 10001;
for(int i = 0; i < nums.size(); i++) {
int ans = 0;
for(int j = i; j < nums.size(); j++) {
ans += nums[j];
if(ans >= target) {
if(j - i + 1 < max) {
max = j - i + 1;
}
break;
}
}
}
if(max == 10001) {
return 0;
} else {
return max;
}
return 0;
}
};
滑动窗口
一次遍历右指针不断移动窗口的终止位置,同时在保证窗口内元素值总和大于等于target的条件下不断收缩窗口的起始位置来更新窗口的起始位置,O(n)。滑动窗口的结束指针是for循环的遍历项,重点和难点是怎么在尾指针遍历的时候合理的移动头指针,比如这道题就是根据窗口区间和>=target来决定头指针的移动,进而决定区间的长度
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int left = 0;
int sum = 0;
int result = INT32_MAX;
for(int right = 0; right < nums.size(); right++) {
sum += nums[right];
while(sum >= target) {
result = result > right - left + 1 ? right - left + 1 : result;
sum -= nums[left++];
}
}
result = result == INT32_MAX ? 0 : result;
return result;
}
};
C#
public class Solution {
public int MinSubArrayLen(int target, int[] nums) {
int left = 0;
int sum = 0;
int result = int.MaxValue;
for(int right = 0; right < nums.Length; right++) {
sum += nums[right];
while(sum >= target) {
result = result > right - left + 1 ? right - left + 1 : result;
sum -= nums[left++];
}
}
result = result == int.MaxValue ? 0 : result;
return result;
}
}
螺旋矩阵
模拟题,重点是定义清楚在四个方向上的循环区间和循环的次数,更新量。
- 每次填元素的区间都是左闭右开,这样循环一周下来是闭合的。
- 循环的次数是n / 2,因为每循环一次,一个方向就收缩两倍。
- 如果n是奇数,中间会空出来一格,补上。
模拟1
按照左上->右上->右下->左上的顺时针顺序来,这样的好处是,过程中i,j都可以重用,实际上startX,startY和n - offset就是每一个方向上遍历时的区间端点,他们是对称的,如果重用已经累加过的i,j,数组下标那相对简洁一些。
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
int startx = 0;
int starty = 0;
int loop = n / 2;
int cnt = 1;
int offset = 1;
vector<vector<int>> ans(n, vector<int>(n, 0));
while(loop --) {
int i, j;
//从左上到右上,i不变,j累加
for(j = starty ; j < n - offset; j++) {
ans[startx][j] = cnt++;
}
//从右上到右下,j不变,i累加
for(i = startx; i < n - offset; i++) {
ans[i][j] = cnt++;
}
//从右下到左下,i不变,j累减
for(; j > startx; j--) {
ans[i][j] = cnt++;
}
//从左下到左上,j不变,i累减
for(; i> starty; i--) {
ans[i][j] = cnt++;
}
startx++;
starty++;
offset++;
}
if(n % 2 == 1) {
ans[n / 2][n / 2] = cnt;
}
return ans;
}
};
模拟2
实际上循环的过程中,每一个方向遍历时,总有一个索引是不动的,这么写也一样。
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
int startx = 0;
int starty = 0;
int loop = n / 2;
int cnt = 1;
int offset = 1;
vector<vector<int>> ans(n, vector<int>(n, 0));
while(loop --) {
int i, j;
//从左上到右上,i不变,j累加
for(j = starty ; j < n - offset; j++) {
ans[startx][j] = cnt++;
}
//从右上到右下,j不变,i累加
for(i = startx; i < n - offset; i++) {
ans[i][n - offset] = cnt++;
}
//从右下到左下,i不变,j累减
for(; j > startx; j--) {
ans[n - offset][j] = cnt++;
}
//从左下到左上,j不变,i累减
for(; i> starty; i--) {
ans[i][starty] = cnt++;
}
startx++;
starty++;
offset++;
}
if(n % 2 == 1) {
ans[n / 2][n / 2] = cnt;
}
return ans;
}
};
C#
索引精简,注意交错数组的初始化和二维数组不一样,C#判断条件==0 ==1 不能不写。
public class Solution {
public int[][] GenerateMatrix(int n) {
//交错数组
int[][] ans = new int[n][];
for(int i = 0; i < n; i++)
ans[i] = new int[n];
int loop = n / 2;
int start = 0;
int end = n - 1;
int cnt = 1;
while(loop-- > 0) {
for(int i = start; i < end; i++) {
ans[start][i] = cnt++;
}
for(int i = start; i < end; i++) {
ans[i][end] = cnt++;
}
for(int i = end; i > start; i--) {
ans[end][i] = cnt++;
}
for(int i = end; i > start; i--) {
ans[i][start] = cnt++;
}
start++;
end--;
}
if(n % 2 == 1) {
ans[n / 2][n / 2] = cnt;
}
return ans;
}
}
二刷
有序数组的平方
两边的数绝对值最大,所以结果不是最左边就是最优先,注意要求从小到大,可以先开闭指定大小的数组,避免reverse;
#include<iostream>
#include<vector>
#include<cstdlib>
using namespace std;
class Soltuion {
public:
vector<int> doit(vector<int>& nums) {
int left = 0;
vector<int> res;
//vector <int> ans(nums.size(), 0);
int right = nums.size() - 1;
while (left <= right) {
int nleft = abs(nums[left]);
int nright = abs(nums[right]);
if ( nleft >= nright) {
res.push_back(nleft * nleft);
left++;
}
else {
res.push_back(nright * nright);
right--;
}
}
reverse(res.begin(), res.end());
return res;
}
};
int main() {
while (1) {
int temp = 0;
vector<int> res;
while (cin >> temp) {
res.push_back(temp);
}
Soltuion s1;
vector<int> ans = s1.doit(res);
for (auto item : ans) {
cout << item << " ";
}
cout << endl;
return 0;
}
}
长度最小的子数组
暴力遍历所有子数组会超时
int left = 0;
int right = 0;
int sum = 0;
int res = INT_MAX;
for (right; right < nums.size(); right++) {
sum += nums[right];
while (sum >= target) {
res = res > right - left + 1 ? right - left + 1 : res;
sum -= nums[left++];
}
}
return res == INT_MAX ? 0 : res;
}
滑动窗口,右边界进元素,左边界出元素,最好用for,右边界是遍历递增的,注意写法,先记录结果再收缩左边界会简洁一些。
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int left = 0;
int right = 0;
int sum = 0;
int res = INT_MAX;
while (right < nums.size()) {
sum += nums[right];
/* while(left < nums.size() - 1 && sum - nums[left] >= target) {
sum -= nums[left++];
}
if (sum >= target) {
res = min(res, right - left + 1);
}
right++;*/
while (sum >= target) {
res = min(res, right - left + 1);
sum -= nums[left++];
}
right++;
}
return res == INT_MAX ? 0 : res;
}
};
水果成篮
滑动窗口,右边界移动还是遍历,把新水果的种类加入到map中计数,如果map中存在两个以上的key说明超了,开始收缩左边界,左边界收缩直到map中仅有两个key(当一个key的value为0时erase他),这题不用哈希表还真不好模拟,主要map.cnt > 2不借助容器不好整。
#include<iostream>
#include<vector>
#include<unordered_map>
using namespace std;
class Solution {
public:
int doit(vector<int>& fruits) {
int left = 0;
int right = 0;
unordered_map<int, int> map;
int res = 0;
int ans = INT_MIN;
for (right; right < fruits.size(); right++) {
map[fruits[right]]++;
while (map.size() > 2) {
auto it = map.find(fruits[left]);
--it->second;
if (it->second == 0) {
map.erase(it);
}
++left;
}
ans = right - left + 1 > ans ? right - left + 1 : ans;
}
return ans;
}
};
int main() {
vector<int> fruits;
int n = 0;
cin >> n;
int temp = 0;
for (int i = 0; i < n; i++) {
cin >> temp;
fruits.push_back(temp);
}
Solution s1;
cout << s1.doit(fruits);
return 0;
}
最小覆盖子串
感觉官方和题解里面的都不是很清晰,有关边界移动的思想都是差不多的,但落实到实现觉得自己这么写比较清晰,同时加了测试输出方便观察(ACM模式写的完整输入输出)。
- 难点一:判断符合最小覆盖的条件该怎么实现:使用一个Map即可(两个map比较有点浪费空间),首先记录t中各个字符出现的次数,在遍历s时,找到就-1,只要所有的map元素出现次数都为0,即代表符合条件。
- 难点二:窗口右边界通过for不断遍历元素,一直减map中的元素,那么左边界收缩移动对应的条件是:map中所有元素出现的次数都 <=0,代表每个t中元素都至少在窗口中出现对应需要的次数,可以多出现,可以恰好,如果恰好,可能前缀有不需要的元素需要剔除,如果多出现,需要剔除多余的元素,并将map计数+1(多一步操作,因此这两个if条件不能合并)。
- 难点三:也是测试样例不容易通过的一点:一旦左边界开始收缩,可能遇到一个不需要的元素,剔除完后面又是多余的元素,也可能剔除完多余的元素,后面又出现了不需要的元素,这两种情况可能交替出现,因此他俩应该是并列的if,外层用while控制不断循环,while的条件即map中所有元素出现的次数都<=0。
- 难点四: 记录结果,一旦左边界收缩结束,就应该记录结果,可以放在收缩外,但还要写一遍check条件,不如直接在while内收缩完毕break之前直接记录即可。
#include<iostream>
#include<vector>
#include<unordered_map>
using namespace std;
class Solution {
public:
bool check(unordered_map<char, int>& data) {
bool flag = true;
for (auto item : data) {
if (item.second > 0) { //数量恰好和多余都通过检验
flag = false;
}
}
if (flag)
cout << "true" << endl;
else
cout << "false" << endl;
return flag;
}
string doit(string s, string t) {
unordered_map<char, int> data;
for (const auto item : t) {
data[item]++;
}
int left = 0;
int ans = 0;
int minLen = INT_MAX;
string minSubstr;
for (int right = 0; right < s.size(); right++) {
auto it = data.find(s[right]);
//削减计数
if (it != data.end()) {
--it->second;
//cout << right << " " << it->first << " " << it->second << endl;
}
while (check(data)) {
//左边界收缩检查是否有多余的单词(t不需要的和需要担多的)
auto leftit = data.find(s[left]);
//t中找不到不需要
if (leftit == data.end()) {
//cout << "不需要,left移动:" << s[left] << " " << left << endl;
left++;
}
//t中找得到但s子串数量已够多余
else if(leftit->second < 0) {
left++;
++leftit->second;
//cout << "多余,left移动: " << left << "计数+1 " << leftit->first << " " << leftit->second << endl;
}
//收缩完成,保存本轮结果,跳出
else {
if (right - left + 1 < minLen) {
minLen = right - left + 1;
minSubstr = s.substr(left, right - left + 1);
//cout << "minLen: " << minLen << " " << minSubstr << endl;
}
break;
}
//cout << "当前left指针:" << left << "当前right: " << right << endl;
}
//cout << endl;
}
return minSubstr;
}
};
int main() {
string s;
string t;
cin >> s >> t;
Solution s1;
cout << s1.doit(s, t);
return 0;
}
螺旋矩阵
注意边界的可循环,左闭右开,二刷15min,用的for循环控制,用while (cnt < k * k)也可。
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> ans(n, vector<int>(n, 0));
int k = 1;
for (int step = 0; step < (n / 2); step++) {
int line = step;
int column = step;
for (column; column < n - step - 1; column++) {
ans[line][column] = k++;
}
for (line; line < n - step - 1; line++) {
ans[line][column] = k++;
}
for (column; column > step; column--) {
ans[line][column] = k++;
}
for (line; line > step; line--) {
ans[line][column] = k++;
}
}
if (n % 2 == 1) {
ans[n / 2][n / 2] = k;
}
return ans;
}
};
数组小结
- 二分,常用左闭右闭,左闭右开,左开右闭和左闭右闭注意边界条件,核心是搜索区间的固定不变。
- 在排序数组中查找元素的第一个和最后一个位置,两次二分,为了找第一个和最后一个位置,收敛搜索区间,当找到元素时不停,控制mid分别向左或者向右即可。
- 搜索插入位置,二分找到第一个大于等于target的数(有序),区间是左闭右开(因为mid的可以取大于target的数),如果有目标,就直接返回,mid小于target,向右搜索,mid大于target,向左包括当前点搜索,最后截止时left = right (循环条件是left < right),就是插入点。(三刷再想想)
- 删除元素,双快慢指针,可以在一次遍历完成两次遍历的事情,降复杂度。
- 有序数组的平方,双指针从两边往中间搜,也是一次遍历能干两次遍历的事情,设计方式不一样,这个是两边往中间。
- 长度最小的子数组,滑动窗口(双指针),尾指针遍历的同时,根据当前你窗口内和是否超过限制移动头指针,更新窗口区间和记录值。
- 水果成篮,滑动窗口,右边界移动向map中加入新水果的计数,当水果种类超过限制时,左边界收缩直到map中的key回到2,借助哈希表实现。
- 最小覆盖子数组,滑动窗口,右边界向map中加入可能符合需要字符的记录(实际实现在计数--),左边界触发收缩的条件是计数<=0,收缩有量两种情况,可能交替进行,外层while控制(水果成篮和长度最小子数组一层while就够,不像这么麻烦),同时注意满足条件要break,否则一直循环(因为条件符合也可能还可以收缩【有不需要的元素】,不能在while条件语句中以此跳出,必须单独考虑break),break之前记录最短子串。
- 螺旋矩阵,模拟,搞清楚左闭右开的循环区间,分清楚行列和子循环之间的递进关系(start++,end--)。
巧用双指针,让两次遍历数组的事情在一次遍历就可以完成是重点,代码随想录总结
- 三刷理解一下二分法扩展的几道题,控制收敛方向来确保是第一个找到的元素的思想。