977.有序数组的平方 ,209.长度最小的子数组 ,59.螺旋矩阵II
LeetCode 977.有序数组的平方
977我看到题目后第一个形成的思路是:
一轮中,慢指针对应要挪到后面排序的那个负值元素,快指针一直++到后面找一个使nums[慢]+nums[快]>=0。
也就是快对应的正值元素绝对值大于等于慢指针的负值元素绝对值,而且是第一个大于等于的,之前都是小于的。
然后把从nums[慢+1]到nums[快-1]的这一堆往前挪,空出的nums[快-1]用原先的nums[慢]去替代。
然后发现写起来很难搞,边界条件就绕到云了,压根执行不了,fast?fast-1?边界怎么搞?脑子转不动。
这个第一个思路很能说明问题,说明我对于题目关键信息是没有提取到的,也没有捕捉到双指针的使用方法,整个脑子思维是非常混乱的,非常云,已有条件不利用,没有最好的利用
vector<int> sortedSquares(vector<int>& nums) {
错误的
int slow,fast = 0;
while(nums[slow]<0){
while(nums[slow]+nums[fast]<0&&fast<nums.size()-1) fast+=1;
int tmp = nums[slow];
int i = slow;
while(i<fast-1){
nums[i]=nums[i+1];
i+=1;
}
nums[i]=-tmp;
if(slow<nums.size())slow+=1;
else break;
}
for(int j = 0;j<nums.size();j++){
nums[j]=nums[j]*nums[j];
}
return nums;
}
然后想的思路不如全都先平方,然后当无序数组冒个泡。这样能解决问题,但就没有很好的利用已有的,负数是已经排好序的,这么一个条件,没有利用好,复杂度肯定高,不是优化的解法
vector<int> sortedSquares(vector<int>& nums) {
for(int i = 0;i < nums.size(); i++){
nums[i] = nums[i]*nums[i];
}
for(int i = 0;i < nums.size(); i++){
for(int j = 0; j < nums.size() - i - 1; j++){
if(nums[j] > nums[j + 1]){
int tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
}
}
}
return nums;
}
虽然复杂度上烂完了,但还好的是冒泡自己能按直觉写出来,要是冒泡都忘记了就真烂完了
还是做不到吗…至少写个好点的,这个冒泡复杂度也太烂了
可能前天双指针也没太搞明白本质
刚才和鸡皮题聊了下,鸡皮题建议是一样先算平方值,但然后的排序是从两端到中间合并,合并过程用双指针
这个好啊,我有想法了
平方后,左指针=0和右指针=nums.size()-1的值比大小,左小于等于右则右指针左移动,直到左大于右,左大于右,就把左+1到右全都左挪1格,把原来的左放到空出来的右这里
vector<int> sortedSquares(vector<int>& nums) {
for(int i = 0; i < nums.size(); i++){
nums[i] = nums[i]*nums[i];
}
int left = 0;
int right = nums.size() - 1;
while(left < right){
if(nums[left] <= nums[right]) right--;
else {
int tmp = nums[left];
for(int i = left + 1;i<=right;i++){
nums[i-1]=nums[i];
}
nums[right] = tmp;
}
}
return nums;
}
可以,一遍过,思路清晰写起来还是比较流畅的,这个时间复杂度要节省下来很多,充分利用了原先有序的条件
看看卡哥的
卡哥这个好啊,拿空间换时间,新搞一个数组,节省了我挨个挪元素空位置这个大头时间花销。
vector<int> sortedSquares(vector<int>& A) {
int k = A.size() - 1;
vector<int> result(A.size(), 0);
for (int i = 0, j = A.size() - 1; i <= j;) { // 注意这里要i <= j,因为最后要处理两个元素
if (A[i] * A[i] < A[j] * A[j]) {
result[k--] = A[j] * A[j];
j--;
}
else {
result[k--] = A[i] * A[i];
i++;
}
}
return result;
}
双指针这里的本质在于,充分利用了已有条件特性:平方后最大值只可能在两端取到,而不可能在中间取到
LeetCode 209.长度最小的子数组
这是过了测试的代码,漏一个等号耽搁了好久,一直查,查不出来,还怀疑是思路问题,最后调试出来的,发现这个边界不对劲,sum有问题,才发现这个漏掉了等号
int minSubArrayLen(int target, vector<int>& nums) {
错的
int left,right;
long len=999999999;
int sum;
for(left = 0;left < nums.size(); left++){
for(right = left;right < nums.size(); right++){
sum = 0;
for(int i = left; i <= right; i++){
//这里i<=right原来漏掉等号,憋着头查了好久好久,思维还是混乱
sum+=nums[i];
}
if(sum>=target) break;
}
if(sum>=target&&(right - left + 1)<len) len = right - left + 1;
}
if(len != 999999999)return len;
else return 0;
}
但这个仅仅是过了测试,没有通过,大数组的测试用例超时了,这个时间复杂度是高的啊,三层循环了都
咋能减减呢,卡在这儿了
看看卡哥的吧
int minSubArrayLen(int s, vector<int>& nums) {
int result = INT32_MAX; // 最终的结果
int sum = 0; // 子序列的数值之和
int subLength = 0; // 子序列的长度
for (int i = 0; i < nums.size(); i++) { // 设置子序列起点为i
sum = 0;
for (int j = i; j < nums.size(); j++) { // 设置子序列终止位置为j
sum += nums[j];
if (sum >= s) { // 一旦发现子序列和超过了s,更新result
subLength = j - i + 1; // 取子序列的长度
result = result < subLength ? result : subLength;
break; // 因为我们是找符合条件最短的子序列,所以一旦符合条件就break
}
}
}
// 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
return result == INT32_MAX ? 0 : result;
}
这个sum明显我没玩明白,外层sum=0,内层sum累加,就不用求sum了
避免了我单独再内嵌一个循环,求sum,导致的套三层
数组操作中另一个重要的方法:滑动窗口
滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果。
在暴力解法中,是一个for循环滑动窗口的起始位置,一个for循环为滑动窗口的终止位置,用两个for循环 完成了一个不断搜索区间的过程。
滑动窗口就是要用一个for循环来完成这个操作
首先要思考
如果用一个for循环
那么应该表示 滑动窗口的起始位置,还是 终止位置?
(这是个关键问题,能提出来很重要)
如果
只用一个for循环来表示 滑动窗口的起始位置,那么如何遍历剩下的终止位置?
此时难免再次陷入 暴力解法的怪圈。
(上面这个思维很好,值得学习)
所以 只用一个for循环,那么这个循环的索引,一定是表示 滑动窗口的终止位置。
那么问题来了, 滑动窗口的起始位置如何移动呢?
窗口的起始位置如何移动:如果当前窗口的值大于s了,窗口就要向前移动了(也就是该缩小了)。
窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,也就是for循环里的索引。
窗口就是 满足其和 ≥ s 的长度最小的 连续 子数组。
解题的关键在于 窗口的起始位置如何移动
滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)暴力解法降为O(n)。
int minSubArrayLen(int s, vector<int>& nums) {
int result = INT32_MAX;
int sum = 0; // 滑动窗口数值之和
int i = 0; // 滑动窗口起始位置
int subLength = 0; // 滑动窗口的长度
for (int j = 0; j < nums.size(); j++) {
sum += nums[j];
// 注意这里使用while,每次更新 i(起始位置),并不断比较子序列是否符合条件
while (sum >= s) {
subLength = (j - i + 1); // 取子序列的长度
result = result < subLength ? result : subLength;
sum -= nums[i++]; // 这里体现出滑动窗口的精髓之处,不断变更i(子序列的起始位置)
}
}
// 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
return result == INT32_MAX ? 0 : result;
}
不要以为for里放一个while就以为是O(n^2)啊, 主要是看每一个元素被操作的次数,每个元素在滑动窗后进来操作一次,出去操作一次,每个元素都是被操作两次,所以时间复杂度是 2 × n 也就是O(n)。
LeetCode 59.螺旋矩阵II
没写出来,思维很乱,也是之前21年时候写过痛过了,现在24年写通不过
错误的代码贴一下
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> ans(n,vector<int>(n));
//(1)i不变j++
//(2)j不变i++
//(3)i不变j--
//(4)j不变i--但不到头而是准备进入内圈,进入内圈后i和j都小了2*1,起点(2,2)
//再下个内圈少2*2,起点(3,3)
//再下个内圈少2*3,起点(4,4)
//终止条件是i=j=1,最后一个元素n方,在(n-1,n-1)
//思路是这个思路,但ij具体的用法我很混乱,ij是下标啊,有意义的,不能随便给减去了
int i,j = 0;int lun = 0;int num = 1;
//原先while里是i!=(n-1) || j!=(n-1)
while(lun < (n-1) ){
i = lun;j = lun;
while( j < (n-lun*2) ){ans[i][j++] = num++;}
j--;i++;
while( i < (n-lun*2) ){ans[i++][j] = num++;}
i--;j--;
while( j >= lun*2 ){ans[i][j--] = num++; }
if(i==lun){lun+=1;continue;}
j++;i--;
while( i > lun*2){ans[i--][j] = num++;}
lun+=1;
}
if(n%2){
if(lun>0)ans[lun-1][lun-1] = num;
else ans[0][0] = num;
}
return ans;
}
可以看出来我一直不停面向测试用例调试,但还是没整明白,思维混乱
看看卡哥的吧
要写出正确的二分法一定要坚持循环不变量原则。
而求解本题依然是要坚持循环不变量原则。
模拟顺时针画矩阵的过程:
填充上行从左到右
填充右列从上到下
填充下行从右到左
填充左列从下到上
由外向内一圈一圈这么画下去。
可以发现这里的边界条件非常多,在一个循环中,如此多的边界条件,如果不按照固定规则来遍历,那就是一进循环深似海,从此offer是路人。
这里一圈下来,我们要画每四条边,这四条边怎么画,每画一条边都要坚持一致的左闭右开,或者左开右闭的原则,这样这一圈才能按照统一的规则画下来。
左闭右开
这里每一种颜色,代表一条边,我们遍历的长度,可以看出每一个拐角处的处理规则,拐角处让给新的一条边来继续画。
这也是坚持了每条边左闭右开的原则。
一些同学做这道题目之所以一直写不好,代码越写越乱。
就是因为在画每一条边的时候,一会左开右闭,一会左闭右闭,一会又来左闭右开,岂能不乱。
代码如下,已经详细注释了每一步的目的,可以看出while循环里判断的情况是很多的,代码里处理的原则也是统一的左闭右开。
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> res(n, vector<int>(n, 0)); // 使用vector定义一个二维数组
int startx = 0, starty = 0; // 定义每循环一个圈的起始位置
int loop = n / 2; // 每个圈循环几次,例如n为奇数3,那么loop = 1 只是循环一圈,矩阵中间的值需要单独处理
int mid = n / 2; // 矩阵中间的位置,例如:n为3, 中间的位置就是(1,1),n为5,中间位置为(2, 2)
int count = 1; // 用来给矩阵中每一个空格赋值
int offset = 1; // 需要控制每一条边遍历的长度,每次循环右边界收缩一位
int i,j;
while (loop --) {
i = startx;
j = starty;
// 下面开始的四个for就是模拟转了一圈
// 模拟填充上行从左到右(左闭右开)
for (j = starty; j < n - offset; j++) {
res[startx][j] = count++;
}
// 模拟填充右列从上到下(左闭右开)
for (i = startx; i < n - offset; i++) {
res[i][j] = count++;
}
// 模拟填充下行从右到左(左闭右开)
for (; j > starty; j--) {
res[i][j] = count++;
}
// 模拟填充左列从下到上(左闭右开)
for (; i > startx; i--) {
res[i][j] = count++;
}
// 第二圈开始的时候,起始位置要各自加1, 例如:第一圈起始位置是(0, 0),第二圈起始位置是(1, 1)
startx++;
starty++;
// offset 控制每一圈里每一条边遍历的长度
offset += 1;
}
// 如果n为奇数的话,需要单独给矩阵最中间的位置赋值
if (n % 2) {
res[mid][mid] = count;
}
return res;
}
卡哥的loop变量对应我的lun变量,这个奇偶的问题我没有处理好,我采用的是在结尾处判断奇偶,卡哥是定义轮次的时候给弄了,省得我在结尾判断套了两层if一堆分支搞得自己很云
矩阵中间位置、遍历边长度,这些我压根没想着去定义,都想靠着对lun变量的循环和对ij的小微操来调整好,但结果是很混乱,现在也没绕明白
说实话
数组这就搞完了,有点快,接下来今天要继续链表
前天的双指针法,今天现在的这个滑动窗口,还有模拟里用的这些定义变量的方法,循环不变量原则
目前没有吸收好,沉淀沉淀,卡哥的视频多看两遍
补充:vecotr创建二维数组
//1
//使用vector一次性完成二维数组的定义(注意:此种方法适用于每一行的列数相等的二维数组)
vector<vector<int>> matrix(m, vector<int>(n, -1));
//以下是拆分理解
//创建一维数组matirx,这个数组里有m个元素,元素是int型vector。
vector<vector<int>> matrix(m);
//除了定义数组类型及数组大小外,同时给数组中的元素赋值:将元素赋值为大小为n的int型vector。
vector<vector<int>> matrix(m, vector<int>(n));
//除了定义数组类型、数组大小、列的大小,同时给数组列中的元素(或者说,数组中的所有元素)赋值为-1。
vector<vector<int>> matrix(m, vector<int>(n, -1));
//比较具有普遍性的写法(注意:此种方法适用于每一行的列数相等的二维数组)
vector<vector<int>> matrix;//创建一维数组matirx,这个数组里的元素是int型vector。
int m = 3; //matrix有m行
int n = 10; //matrix有n列
int value = 1; //最终matrix成为二维数组后,其中每个元素的值为1(如果不需要进行初始化,此语句可以省略)
for (int i = 0; i < m; ++ i) {
vector<int> tmp(n, value); //定义int型一维数组tmp,该数组有n个int型元素,且每个元素的初始值为value
matrix.push_back(tmp); //将一维数组tmp(小容器)加入matrix(大容器)中,使之成为matrix的元素,令matrix成为二维数组
}
//2
//如果需要每一行的列数不同(虽然一般很少这样做),也可以使用下面这种写法进行定义、初始化
vector<vector<int>> matrix;
vector<int> a(10, 1); //单独定义每个小容器的元素个数和元素初始值
vector<int> b(5, 2);
vector<int> c(10, 3);
matrix.push_back(a); //将每个小容器加入matrix(大容器)中
matrix.push_back(b);
matrix.push_back(c);
//3
//使用vector的resize函数进行二维数组的定义(注意:此种方法适用于每一行的列数相等或不相等的二维数组,调整for循环内的resize函数的参数即可)
vector<vector<int>> matrix(m); //创建一维数组matirx,这个数组里有m个元素,元素是int型vector。不能省略m。
for (int i = 0; i < m; ++ i) {
matrix[i].resize(10, 1); //使用vector的resize函数,对matrix(大容器)中的每个元素的大小进行更新(可以同时进行初始化)。此处表示:将matrix(大容器)中第i个int型vector的大小定义为10,且其元素均初始化为1。
//如果不需要进行初始化,resize函数的第二个参数可以省略
}