@代码随想录算法训练营
代码随想录Day2
数组理论基础 977.有序数组的平方 ,209.长度最小的子数组 ,59.螺旋矩阵II ,总结
2024/03/23 今日任务:
完成英语每日阅读+听力
论文实验想想要做什么 - 今天改个屁毕业论文,爱咋咋地,反正还没正式查重,被X就被X了,烦的一逼,妈个鸡
答辩PPT - 今天应该可以搞定吧
完成昨天前天和今天的算法代码学习
路由技术卷2,15页还没看 - 累积到27页了。。。3天只看了18页,估计1天清不掉
Day2题目
1、 977.有序数组的平方
题目地址:https://leetcode.cn/problems/squares-of-a-sorted-array/
给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
示例 1:
输入:nums = [-4,-1,0,3,10]
输出:[0,1,9,16,100]
解释:平方后,数组变为 [16,1,0,9,100]
排序后,数组变为 [0,1,9,16,100]
示例 2:
输入:nums = [-7,-3,2,3,11]
输出:[4,9,9,49,121]
提示:
1 <= nums.length <= 104
-104 <= nums[i] <= 104
nums 已按 非递减顺序 排序
进阶:
请你设计时间复杂度为 O(n) 的算法解决本问题
解法:其实暴力解法,即对所有元素平方后直接快排,是最直观的想法,但是心里总想着肯定是有更好的想法。但是发现好像也不太好。
个人想法是基于Day1的移除元素解法,不额外消耗空间,那么就需要平方之后跟后面的值进行对比,然后把后面的值往前面覆盖。**但是太蠢了,太多个for循环来不停往前面覆盖,甚至比暴力解法的效果还要差,只是仅仅在纸上画一下。**天马行空一下有时候还是不错的XD。
基于双指针的运用,目前学到了第二种方法,1是快慢指针。2.边界值往中间逼近。
考虑两边的大小,往中间最小值推,由于左右两边的指针都是极端的界限值,只要两边互相推完了就一定有一个结果,非常舒服,没有什么其他问题要考虑。
5/31日更新
针对循环不变量的考虑,这里有两个选择:
- 对于卡哥题目中i <= j,也就是说两边边界的指针会晤的时候,条件就结束了。
- 针对新的数组,填满了新的数组空间,条件也结束了,也就是i := len(nums) - 1; i > 0; i–。
两者相同,但判断的条件不相同,但目的条件都是直接为了填满新数组。
2、209.长度最小的子数组
题目地址:https://leetcode.cn/problems/minimum-size-subarray-sum/
给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。
示例:
输入:s = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
提示:
1 <= target <= 10^9
1 <= nums.length <= 10^5
1 <= nums[i] <= 10^5
解法:暴力解基本上是不太可行,O(n^2)。leetcode过不了。
在看完gif动画之后,尝试不看代码自己实现一次。出现了一个极易犯下的思维错误。
简单每次对后续的前后指针总数加起来,会导致一个后果:for循环了一遍。即使你实现了滑动窗口,但是你把中间的数总和加起来会导致时间超出。
想了很久才知道为什么会超出时间。需要通过用“松弛”然后“收缩”的形式来处理。快指针指的是右边界,慢指针指的是左边界。
即:先把窗口滑到第一次满足条件的子数组后(松弛),然后往右继续滑1个,然后检查“收缩”是否成功,成功则继续获得更小的子集长度,直到数组结束为止。“收缩”成功有两个条件:1. 当前窗口总和是否大于等于target。2.这次的sublength是否小于上一次的sublength(需要通过不停增加慢指针的位置,逼近快指针,不停校正和收缩总和值,直到sum < target,则停止收缩,而上一次收缩结果则是贪婪最优解。通过滑到最后,则有全局最优解)。 当满足以上两个条件时,答案自然出现。
因此,需要多花点心思考虑和总结。熬夜好累啊,困死了。
5/31日更新
参考卡哥的代码随想录:
第一个for是为了维护整个解的循环不变量,目的是为慢指针完整地搜索整个数组。
第二个for循环是为了对当前慢指针展开滑动窗口,通过找到最短的sublength,并用result来记录。而该第二个for循环不变量就是维护当前子数组是否足够大。因此,也就是上面的“收缩”成功第二个条件,出现“sum <target”时,则停止收缩。就算再加下去,子数组的长度也会不停增大,因此没有继续加下去的意义。
- “松弛”:因此,要做的是,第一个for循环对这个数组的第一个元素开始遍历。添加到sum
- “收缩”:一旦出现sum >=target时,开始对窗口进行滑动,滑动的过程中计算此时得到的结果sublength,并且计算sublength长度。而收缩过程最后需要减去slow指针的一个数,以不满足sum >= target为条件退出“收缩”过程,转而进入下一阶段的松弛。并继续遍历
- “松弛”:在下一次循环中,继续开始对下一个元素进行遍历,然后再执行第二个操作进行收缩。
- 直到最后,判断是否有结果,没有则返回0,有则返回result。
3、59.螺旋矩阵II
题目地址:https://leetcode.cn/problems/spiral-matrix-ii/description/
给定一个正整数 n,生成一个包含 1 到 n^2 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。
示例:
输入: 3 输出: [ [ 1, 2, 3 ], [ 8, 9, 4 ], [ 7, 6, 5 ] ]
解法:一开始想了很久,以为是有什么特殊的数学公式/规律来组成这个二维数组,想到脑袋爆了。然后发现没有,就老老实实做了。
根据矩阵的大小,生成1个二维数组,然后往内部不同行列的元素进行以螺旋式的方法进行填充。在每次进行处理的圈里,通过对行列(i,j)分别进行2次,即一个螺旋一共4次的数据填充,即可完成。
即上部分、右部分、下部分、到左部分进行处理。
其中必须定义对边界条件的处理,最简单的就是左闭右开,即不考虑每一个部分的最后一个元素,而是让给下一部分处理。
那么每一个部分的边界条件都是相同的,能够避免许多边界条件处理的混乱。
同时,需要考虑圈数,那么圈数根据B站卡哥的评论底下有大神解释:
因为搜索一圈之后,下一圈的上边会往下走,下一圈的下边会往上走,高度就少2,下一圈的左边会往右走,下一圈的右边会往左走,相当于宽少2了,每次下一圈都会比上一圈的高度宽度都少2,直到这个没有外圈了,没有外圈就是宽度是和高度都是0的时候(偶数的情况),每次宽,高缩小2,直到宽,高是0。因为宽,高都一样,而且是一起缩2的,那么就当高缩2到0的时候就结束了,要缩多少次,就是圈。高假设是偶数,偶数-2-2-2一直到0,不就是这个偶数除2吗,就是圈数啦
个人思考之后,也有如下解释:考虑最上面的一条边(长度大小为n),在一圈结束后就会减少2个长度单位(-2),那么多少圈就是n-2-2-2-2-2…直到为1或者为0,那么相当于n/2。
喜欢干什么就干什么,有时候熬熬夜还是挺爽的,夜深人静的时候思考问题或者看书
6/2日更新
建议做完这一题考虑一下类似的题目,螺旋矩阵
螺旋矩阵这一道题目,相比于螺旋矩阵II,需要考虑的东西可能会更加多。
如果直接套卡哥的讲解,在“螺旋矩阵”里会有个疑问:到底用什么来控制循环?怎么确定层数?循环不变量这里所指代的是什么?
循环不变量的逻辑在这里最直观的就是把圈转完。但是这道题把圈转完的逻辑有许多种:
1.侧面判断转圈结束:定义top,bottom,left,right边界,由于在转圈的时候,边界是会收缩的。当边界收缩到left< right,right>left,bottom> top以及top<bottom。也就是4个循环条件只有一个不满足的时候,就能够跳出循环不变量的检测,从而解决问题。(相当于在“终止”阶段反推出不变量的条件。)边界条件不满足的时候,就会及时停止,不需要额外判断装载答案的数组是否装满。
2.直接判断,loop的次数在最短边界的1/2时停止,也就是说只会loop最短边界的1/2往下取整次数。(螺旋矩阵II的做法,将会继续把最后一圈读出来,无论是否形成螺旋形式,要不然就是行大于列,要不然就是列大于行,都会准确读出来)进而把中间缺少的那一块给读出来。但是,这个螺旋的读取方式需要被终止,因为这个for循环并不会检测是否已经读过,举个例子:
1,2,3,4,5,6,7,8,9 列增加的程序读完之后,行处理程序跳过,因为没有下一列。但是,列减少到开始坐标的时候,又会重复读一次,此时,用于装载答案的数组将会超出范围,因此需要及时打断。
func spiralOrder(matrix [][]int) []int {
startx, starty := 0, 0
offsetX := 1
offsetY := 1
count := 0
loopx := len(matrix[0])
loopy := len(matrix)
ansLeng := len(matrix[0]) * len(matrix)
ans := make([]int, ansLeng)
loop := loopx
if loopy > loopx {
loop = loopy
}
if ansLeng == 1 {
return []int{matrix[0][0]}
}
if len(matrix) == 0 {
return []int{}
}
// 思考一下坐标与循环不变量的关系,只要top大于down,即出现问题,同理,down小于up也出现问题,左大于右出现问题,右大于左出现问题
// 是形容不同形状的矩形导致。
for loop > 0 {
j, k := startx, starty
for ; j < loopx-offsetX; j++ {
ans[count] = matrix[k][j]
if count+1 >= ansLeng {
break
}
count++
}
for ; k < loopy-offsetY; k++ {
ans[count] = matrix[k][j]
if count+1 >= ansLeng {
break
}
count++
}
for ; j > startx; j-- {
ans[count] = matrix[k][j]
if count+1 >= ansLeng {
break
}
count++
}
for ; k > starty; k-- {
ans[count] = matrix[k][j]
if count+1 >= ansLeng {
break
}
count++
}
if count+1 >= ansLeng {
break
}
startx++
starty++
offsetX++
offsetY++
loop--
}
// 边界条件,奇数矩阵时,需要读取中间的[n/2,n/2]的值,而偶数矩阵则不用读取,因为上面的转圈逻辑已经实现了读取全部统一列或者同一行的逻辑
// 但是需要及时断开读取逻辑。
if loopx == loopy && (len(matrix)%2 != 0) {
layer := loopx / 2
ans[count] = matrix[layer][layer]
}
return ans
}
出现4种情况需要判断:
1.当n与m是相同且是奇数时,需要单独处理。
2.当n与m是相同时,程序会自动读完,不用处理。
3.当n与m是0时,直接返回。
4.当n或m是1时,要读一个。
总结:螺旋矩阵目前的考虑方案是2种,两者在处理每条边的操作也是不太一致。:
1.考虑转圈圈数,固定每条边读取的大小在转圈时会存在offset以及start(每一个圈、每条边开始读取的地方),需要主动维护。
2.考虑违反转圈成功条件,进而推出转圈失败时的终止条件,而失败条件则随着每条边的大小自动递进调整(判断是否转圈失败时,4条边会自动调整递增)。
两者皆可,第二个方法简单,但是个人觉得第一个方法是万灵药,无论怎么转都可以清晰知道从开始到结束的边界要怎么处理。