链接: 977. Squares of a Sorted Array
根据给的example可以明显看出,当数组中有负数的时候,经过平方处理,其原有的“非递减排列”会得到改变,从而需要重新排序。如果通过先平方再排序的思路解题,其时间复杂度最小也为O(n+nlog n)
寻求最优解,进行进一步分析。因为绝对值越大的数,平方越大,且此数组条件为非递减,所以数组平方后的最大值,只可能存在于最左侧或最右侧。可以定义两个指针,分别指向数组的最左侧和最右侧,通过比较两个指针指向方向的值的平方,就可以得出数组平方后的最大值。取出最大值后,向中间移动指针,即获得新的子数组,重复上述做法即可一步一步得出每个子数组的最大值,直到原数组取完,得到原数组元素的非递增序列。
但因为题目要求输出的是非递减序列,且我们已知数组长度,即按照从后往前的顺序填入上述获得的元素即可。
class Solution {
public int[] sortedSquares(int[] nums) {
// left为左指针,right为右指针,k是新数组下标(从后往前放入)
int left = 0, right = nums.length - 1, k = nums.length - 1;
int[] sortedNums = new int[nums.length];
// 这里是否要取等号?
// 需要!因为每次是两个元素中通过比较挑一个出来,不取等号,最后一次比较仍然是两个挑一个,剩下的那个元素就被遗漏了
while (left <= right) {
// 这里是否取等号?
// 都可以,因为相等的情况,不管你取左边的数还是右边的数,值一样,而且被剩下的参与下一轮比较的值也一样
if (nums[left] * nums[left] >= nums[right] * nums[right]) {
sortedNums[k--] = nums[left] * nums[left];
left++;
} else {
sortedNums[k--] = nums[right] * nums[right];
right--;
}
}
return sortedNums;
}
}
时间复杂度:O(n)
空间复杂度:O(1)
通过对原数组和返回数组的分析,确定两个指针的初始位置,移动方向和移动条件。注意两个条件的取等含义。
代码随想录文章讲解:传送门
代码随想录视频讲解:传送门
链接: 209. Minimum Size Subarray Sum
此题结合实例理解,可以知道我们最后得到的是输入数组的一个子数组,所以我们需要维护两个指针分别指向子数组的开始位置start和结束位置end。构造出一个滑动的窗口。
如果通过两个循环,遍历所有两个指针可能的所有情况,即一个for循环滑动窗口的起始位置,一个for循环滑动窗口的终止位置,这种暴力解法,时间复杂度为O(n^2)。但是由于我们只需要满足条件的最小长度,有很多无用的子数组可以排除,因而考虑只用一个for循环来解决问题。
那么这个for循环应该应该是循环end(即滑动窗口的终止位置),因为循环start的话,end仍需要通过遍历后续所有元素。
滑动窗口的滑动规则:移动end,计算窗口内元素和,如小于target则继续移动end,否则考虑缩小start以寻求最小值。
且本题需要输出的是所有满足条件里的最小值,所以还需要维护一个结果值,每次获得了满足条件的子数组,通过end-start+1可得到其长度,比较其是否为最小长度,如果是更新结果值,不是则继续寻找直到结束。
class Solution {
public int minSubArrayLen(int target, int[] nums) {
// sum记录当前窗口内的元素和
// len记录滑动过程中的满足条件窗口的最小长度
// len初始值为MAX_VALUE是为了检查是否有len从未被赋值的情况,即无满足条件的子数组(len初始值也可为其他比nums.length大的数)
int start = 0, sum = 0, len = Integer.MAX_VALUE;
for (int end = 0; end < nums.length; end++) {
sum += nums[end];
// 如果窗口满足条件,则记录窗口长度
// 并尝试缩小窗口(移动窗口起始处)看当前是否有更小长度的字数组
while (sum >= target) {
len = Math.min(len, end - start + 1);
sum -= nums[start];
start++;
}
}
if (len < Integer.MAX_VALUE) return len;
else return 0;
}
}
时间复杂度:O(n)(这里每个元素被操作两次,所以是O(2n)即O(n))
空间复杂度:O(1)
我感觉滑动窗口可以算是双指针的一个特例,窗口的两端可以看成是前后指针。因为题目求最小,所以滑动窗口减少了一些冗余情况的遍历,降低了时间复杂度。
代码随想录文章讲解:传送门
代码随想录视频讲解:传送门
链接: 59. Spiral Matrix II
此题没有太多便利的方法,主要是模拟数据填充的循环过程,主要要理清循环的轮次关系、每次循环的边界问题与循环时偏移量的变化。写之前要想好自己定义的变量都是什么意思,遍历的规则,才能更明确上述关系。
比如,遍历每条边的时候,是否包含边的第一个节点和最后一个节点?最好的处理是包含第一个节点,不包含最后一个节点,这样每条边的处理将一样且不会遗漏这一轮的元素。
将每次顺时针转一圈视为一次循环,分别画四条边
class Solution {
public int[][] generateMatrix(int n) {
int i, j; // 移动的坐标
int startX = 0, startY = 0; // 循环的起始点,初识为(0,0)
int round = 0; // 循环轮次
int num = 1; // 进行填充的数字
int offset = 1; // 每次循环边界收缩量
int[][] matrix = new int[n][n];
while (round < n / 2) {
// 每条边遍历左开右闭区间,即包括开始节点,不包括终止节点
// 一开始是向右走,即朝着j++的方向走
for (j = startY; j < n - offset; j++) {
matrix[startX][j] = num++;
}
// 向下走,即朝着i++的方向走
for (i = startX; i < n - offset; i++) {
matrix[i][j] = num++;
}
// 向左走,即朝着j--的方向走
for (; j > startY; j--) {
matrix[i][j] = num++;
}
// 向上走,即朝着i--的方向走
for (; i > startX; i--) {
matrix[i][j] = num++;
}
// 处理下一圈循环变化
// 这里startX和startY的变化其实是同步的,所以可以合成成一个变量start
startX++;
startY++;
// 这里offset的变换更新与round其实也是同步的,只不过offset比round大1,也可以考虑合成一个变量
offset++;
round++;
}
// 如果n为奇数,还要填充最中间的数
if (n % 2 != 0) matrix[n / 2][n / 2] = num;
return matrix;
}
}
与实现一相比,更直接地利用四个变量判断四条边的动态变化。思路其实类似。
class Solution {
public int[][] generateMatrix(int n) {
int[][] matrix = new int[n][n];
int num = 1;
int left = 0, right = n - 1, top = 0, bottom = n - 1;
while (num <= n * n) {
for (int i = left; i <= right; i++) matrix[top][i] = num++;
top++;
for (int i = top; i <= bottom; i++) matrix[i][right] = num++;
right--;
for (int i = right; i >= left; i--) matrix[bottom][i] = num++;
bottom--;
for (int i = bottom; i >= top; i--) matrix[i][left] = num++;
left++;
}
return matrix;
}
}
针对一些写入模式清晰但复杂的题,可以考虑直接模拟过程,但模拟过程中要格外注意各种边界条件。