代码随想录算法训练营第二天 | 977.有序数组的平方 209.长度最小的子数组 59.螺旋矩阵II
977.有序数组的平方
本题可以直接暴力计算出每一个数的平方后再进行排序,但时间复杂度最低也为O(n+nlogn),一次遍历,一次快速排序。
本题也可以使用双指针在不进行排序的情况下完成,时间复杂度为O(n),一次遍历即可。时间复杂度明显更优。
注意题目提示数组是有序的,意味着题目要求的目标矩阵里较大的值只会出现在原数组的两端,较小的值只会出现在原数组的中间。
刚开始做题时脑子没转过弯,想着两个指针从中间开始向两边走,从小到大依次插入目标数组,结果怎么写都会出问题,后来一看,从大到小倒序插入目标数组就完事了……指针从两侧开始向中间靠拢就没那么多幺蛾子要处理了。
class Solution {
public int[] sortedSquares(int[] nums) {
int leftIndex = 0;
int rightIndex = nums.length - 1;
int[] result = new int[nums.length];
int insertIndex = nums.length - 1;
while (leftIndex <= rightIndex) {
if (Math.abs(nums[leftIndex]) <= Math.abs(nums[rightIndex])) {
result[insertIndex--] = (int) Math.pow(nums[rightIndex], 2);
//Math.pow()默认输出double类型数值,需要强转。n * n形式也行但是太长了。记住Java没有“n ** 2”!
rightIndex--;
} else {
result[insertIndex--] = (int) Math.pow(nums[leftIndex], 2);
leftIndex++;
}
}
return result;
}
}
209.长度最小的子数组
说来惭愧,一开始连暴力解法都没想出来……乖乖学习滑动窗口解法。
也是双指针的应用,一个for循环内完成两个for循环的工作。
使用一个指针指向最短子数组的左侧,一个指针指向最短子数组的右侧。每一次寻找最短子数组时移动的是右侧的指针,拉长数组长度,直到当前两个指针范围内的子数组满足题目要求,元素总和大于等于target
。此时开始移动左侧指针,不断将元素移出子数组,并检查此时的子数组是否仍然满足元素综合大于等于target
的要求,如果仍然满足,比较当前子数组长度与原来的最小子数组的长度,如果更小则更新,不满足则重新开始移动右侧指针。
第一次进行子数组长度的比较时,因为此前并没有最小子数组长度的记录,所以需要确保预设的记录一定比这个最小子数组的长度大,保证这个值能够更新。因此,最小子数组长度的记录需要预设为Integer.MAX_VALUE
。当不存在满足题目要求的子数组时,最小子数组的长度一定为Integer.MAX_VALUE
,输出结果时输出0即可;一旦存在满足题目要求的子数组,最小子数组的长度一定不会为Integer.MAX_VALUE
,输出此时的最小长度记录即可。
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int leftIndex = 0;
int rightIndex;
int compare = 0;
int result = Integer.MAX_VALUE;
for (rightIndex = 0; rightIndex < nums.length; rightIndex++) {
compare += nums[rightIndex]; // 移动右侧指针寻找是否存在满足题目条件的子数组
while (compare >= target) {
result = Math.min(result, rightIndex - leftIndex + 1);
compare -= nums[leftIndex++]; // 移动左侧指针,缩短子数组长度,确保找到最小长度
}
}
return result == Integer.MAX_VALUE ? 0 : result; // 只有在不存在满足条件的子数组的情况下result才会等于Integer.MAX_VALUE
}
}
补充:题目里提示,除了滑动窗口这种时间复杂度为O(n)的算法,还存在一种时间复杂度为O(nlogn)的算法,即官方题解内提到的前缀和+二分查找算法。虽然时间复杂度上并没有更优,但前缀和在数组的算法问题上也是一种比较重要的算法,故在博客里记录这种方法的学习笔记。
由于题目内强调了数组的所有元素均为正数,因此数组的前n项和所构成的新数组(下称前缀和数组pre)一定是有序递增的,且不会出现重复元素,符合使用二分法查找的条件。设我们需要找的子数组的最左侧和最右侧下标为i
和j
,我们需要找到的就是在这个前缀和数组中满足pre[j] - pre[i] >= target
的,有最小差值的i和j。式子变形后得target + pre[i] <= pre[j]
,所以我们只需要使用一个for循环不断更新i的位置,确定在前缀和数组里进行二分查找需要用到的新的target,使用二分查找找到j,即可确定该子数组的长度。因为需要遍历整个数组,每移动一次i就要进行一次新的二分查找,因此这个算法的时间复杂度为O(nlogn)
。
因为今天时间不够了,没能实现这个算法。查看了官方题解后发现官方使用了Array.binarySearch(),该函数能够实现查找一个元素在函数中的位置或应该插入的位置,与昨天的题目35.搜索插入位置相关。如何自己实现这个函数以满足题目要求可以作为之后的一个小的学习点之一。
59.螺旋矩阵II
如教程所言,螺旋矩阵只需要使用代码模拟出这个转圈的过程即可。但是,这个转圈的过程如何模拟本身就是容易出错的点。
一开始实现的时候因为没有明确边界的定义,怎么调试怎么改都是出错。查看教程后发现,转圈的时候如果出现了一个for循环填满了某条边的情况,就意味着这段代码没有严格遵守边界的定义。对于填边的过程而言,这个区间是左闭右开的。这条边最开始的格子需要填充,但最后的格子无需填充,因为需要留给下一条边作为开始的格子。因此,一个for循环绝对不会完整填充一条边。
明确区间边界定义之后,模拟的过程也就清晰了。每一次循环就意味着一个圈的完整填充,那么对于偶数n而言,需要转的圈数等于n/2
,奇数n同理,只需要将中心的格子单独拿出来处理即可,因此使用int loop = n/2
确定循环的次数。每一次循环的起始点相较于上一个循环而言,右移1个单位,下移1个单位,因此设置startLeft
和startUp
记录每一次循环填充的起始位置。因为每多一次循环,需要填充的最右侧格子和最下侧格子都会左移/上移1个单位,因此使用offset
控制循环里填充格子的位置。
明确了需要使用哪些变量之后,进行代码实现。
class Solution {
public int[][] generateMatrix(int n) {
int loop = n / 2; // 需要转多少圈
int insertNum = 1; // 用于插入数组的数
int offset = 1; // 偏移量,控制循环中填充格子的位置向内缩
int i; // 循环中用于控制填充格子的下标,控制行
int j; // 循环中用于控制填充格子的下表,控制列
int startLeft = 0; // 控制每一次循环起始位置(行)
int startUp = 0; // 控制每一次循环起始位置(列)
int[][] result = new int[n][n]; // 目标二维数组
while (loop-- > 0) {
i = startLeft;
j = startUp;
for (; j < n - offset; j++) {
result[i][j] = insertNum++; // 向右填充,控制左闭右开,最右侧格子作为下一个for循环的起始格子不能填充
}
for (; i < n - offset; i++) {
result[i][j] = insertNum++; // 向下填充
}
for (; j > startLeft; j--) {
result[i][j] = insertNum++; // 向左填充
}
for (; i > startUp; i--) {
result[i][j] = insertNum++; // 向右填充
}
offset++; // 填充圈向内收缩1个单位
startLeft++; // 下一个循环的填充位置右移1个单位
startUp++; // 下一个循环的填充位置下移1个单位
}
// 奇数n的中央格子单独处理
if (n % 2 == 1) {
result[n / 2][n / 2] = n*n;
}
return result;
}
}
数组篇总结由于时间问题今天无法输出,列入需要完成的后续博客之一。