@author LemonCat
@time 2023/7/24
@target 第一章 数组 part02 (qq.com)
今日任务
- 977.有序数组的平方、209.长度最小的子数组、59.螺旋矩阵 II
977.有序数组的平方
题目建议: 本题关键在于理解双指针思想
思路
- 暴力法 - 将每个数平方。然后快排进行排序 -> 时间复杂度 O(nlogn)
- 双指针法 - 时间复杂度为 O(n)
方法一 - 暴力法
- for 循环遍历 将每个数平方 - n
- 快速排序 logn
方法二 - 双指针法
-
数组是正序 - 负数平方之后可能成为最大数
- 数组平方的最大值就在数组的两端,不是最左边就是最右边,不可能是中间
-
通过设置双指针 - left 指向起始位置,right 指向终止位置。
-
定义一个新数组 newArr,和 A(原始数组) 数组一样的大小,让 index(每次数组放置的位置) 指向 newArr 数组终止位置
- 如果 A[left] * A[left] < A[right] * A[right] -> newArr[index--] = A[right] * A[right];
- 如果 A[left] * A[left] >= A[right] * A[right] -> newArr[index--] = A[left] * A[left];
-
即比较两个指针所对应值平方大小的比较 - 哪个大就把他放到新数组里
-
-
注意:下面的循环也可以用 while 更加的直观 这里为了保证代码简洁性 选取了 for 循环
/**
* 方法二 - 双指针法
*/
class Solution {
public int[] sortedSquares(int[] nums) {
int[] newArr = new int[nums.length]; // 返回的新数组
// i 是左指针 j 是右指针 k 为每次遍历插入新数组的索引
for (int i = 0, j = nums.length - 1, k = nums.length - 1; i <= j; k--) { // 注意这里要i <= j,因为当 i = j 时正好是最后一个元素
if (nums[i] * nums[i] < nums[j] * nums[j]) {
newArr[k] = nums[j] * nums[j--]; // 当右指针做对应值的平方大时 -> 将其放入新数组中 并将右指针向前移动一位
} else {
// else 就包含 左边大和相等的情况 - 相等时任意取出一个就好(这里就放到了选左边)
newArr[k] = nums[i] * nums[i++];// 当左指针做对应值的平方大或相等时 -> 将其放入新数组中 并将左指针向前移动一位
}
}
return newArr;
}
}
209.长度最小的子数组
题目建议: 本题关键在于理解滑动窗口,这个滑动窗口看文字讲解 还挺难理解的,建议大家先看视频讲解。 拓展题目可以先不做。
方法一 - 暴力法
- 两个 for 循环,然后不断的寻找符合条件的子序列,时间复杂度 O(n^2)
- 已经超时 -> o.O
方法二 - 滑动窗口
-
就是不断的调节子序列的 起始位置 和 终止位置,从而得出我们要想的结果
- 就是用一个 for 循环 -> 去替代原来的双层 for 循环
-
暴力解法:
- 是一个 for 循环滑动窗口的起始位置
- 一个 for 循环为滑动窗口的终止位置
- 用两个 for 循环 完成了一个不断搜索区间的过程
-
一个 for 循环遍历的是 滑动窗口的起始位置,还是终止位置?
- 滑动窗口的起始位置 - 那么如何遍历剩下的终止位置?此时难免再次陷入暴力解法的怪圈
- 所以一定是表示 滑动窗口的终止位置
-
滑动窗口,主要确定如下三点:
- 窗口:满足条件的长度最小的 连续子数组
- 如何移动窗口的起始位置:如果当前窗口的值大于 s 了,窗口就要向前移动了(也就是该缩小了) -> start++
- 如何移动窗口的结束位置:就是遍历数组的指针 -> end++
/**
* 方法二 - 滑动窗口
*/
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int sum = 0; // 计算和
int start = 0; // start - 滑动窗口起始位置
int res = Integer.MAX_VALUE; // res - 是用返回的最小窗口长度的 这里先赋值一个最大值 用与判断是否有这样的滑动窗口
// 选用 end 作为遍历原因 -> 反证法:若选用 start作为遍历 -> 仔细一想好像跟双层for循环没啥区别了
for (int end = 0; end < nums.length; end++) { // end - 滑动窗口终止位置
sum += nums[end];
while (sum >= target) { // 找到满足条件的滑动窗口
if (res > (end - start)) { // 若找到的这个新窗口 小于之前找到的 则进行更新替换
res = end - start + 1;
}
sum -= nums[start++]; // 缩小滑动窗口大小 -> start 左移
}
}
return res == Integer.MAX_VALUE ? 0 : res; // 若res依旧是最大值 则就证明 没有符合条件的窗口 返回 0 否则返回res
}
}
- 滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。
- 时间复杂度:从而将 O(n^2)暴力解法降为 O(n)
59.螺旋矩阵 II
题目建议: 本题关键还是在转圈的逻辑,在二分搜索中提到的区间定义,在这里又用上了。
方法一 - 螺旋矩阵
-
坚持循环不变量原则 - 每画一条边都要坚持一致的左闭右开原则
- 这样这一圈才能按照统一的规则画下来
-
模拟顺时针画矩阵的过程:
- 填充上行从左到右
- 填充右列从上到下
- 填充下行从右到左
- 填充左列从下到上
- 由外向内一圈一圈这么画下去
-
最后要注意 - 若边长为奇数 -> 按照上面的处理方式就会漏掉最后一个中心点
- 结束返回前 记得 n % 2 == 1 判断一下 -> 奇数就赋值
class Solution {
public int[][] generateMatrix(int n) {
// 循环次数
int loop = 0;
// 定义螺旋矩阵
int[][] matrix = new int[n][n];
// 定义每次起始坐标
int startX = 0;
int startY = 0;
// 定义 结束偏移量
int setOff = 1;
// 填入矩阵的数值
int count = 1;
while (loop++ < n / 2) {
// 注意:我们定义每个边遍历都是左闭右开
// 这样可以保证循环不变量 -> 即每个边处理逻辑是相同的
int i, j;
// 从左到右
for (j = startY; j < n - setOff; j++) {
matrix[startX][j] = count++;
}
// 从上到下
for (i = startX; i < n - setOff; i++) {
matrix[i][j] = count++;
}
// 从右到左
for (j = n - setOff; j >= setOff; j--) {
matrix[i][j] = count++;
}
// 从下到上
for (i = n - setOff; i >= setOff; i--) {
matrix[i][j] = count++;
}
startX++; // 起始点 +1
startY++; // 起始点 +1
setOff++; // 偏移量 +1
}
if (n % 2 == 1) { // 解决边长为奇数情况 -> 给中心点赋值
// matrix[n / 2][n / 2] = count;
matrix[startX][startY] = count; // 遍历到结束时就是最中心的那个点
}
return matrix;
}
}
- 时间复杂度 O(n^2): 模拟遍历二维矩阵的时间
- 空间复杂度 O(1)
方法二 - 螺旋矩阵简洁版
- 循环次数 loop 和 偏移量 setoff 本质上是相同的 整合到一起
- 起始点 startx 和 starty 逻辑处理过也是相同的 整合到一起
- 从右到左 和 从下到上 循环起始值 i 和 j 可以不用赋值 -> 其本身已经达到了目标值
class Solution {
public int[][] generateMatrix(int n) {
int loop = 0; // 循环次数 也是结束偏移量
int[][] matrix = new int[n][n];// 定义螺旋矩阵
int start = 0; // 定义起始坐标
int count = 1; // 填入矩阵的数值
while (loop++ < n / 2) {
// 注意:我们定义每个边遍历都是左闭右开 - 这样可以保证循环不变量 -> 即每个边处理逻辑是相同的
int i, j;
// 从左到右
for (j = start; j < n - loop; j++) {
matrix[start][j] = count++;
}
// 从上到下
for (i = start; i < n - loop; i++) {
matrix[i][j] = count++;
}
// 从右到左
for (; j >= loop; j--) {
matrix[i][j] = count++;
}
// 从下到上
for (; i >= loop; i--) {
matrix[i][j] = count++;
}
// 第二圈开始的时候,起始位置要各自加1, 例如:第一圈起始位置是(0, 0),第二圈起始位置是(1, 1)
start++; // 起始点 +1
}
if (n % 2 == 1) { // 解决边长为奇数情况 -> 给中心点赋值
// matrix[n / 2][n / 2] = count;
matrix[start][start] = count; // 遍历到结束时就是最中心的那个点
}
return matrix;
}
}
总结
- 二分法
- 双指针法
- 滑动矩阵
- 循环不变量原则
- 结束 - 原神启动 ~ 明天继续加油!