代码随想录算法训练营第二天 | 977.有序数组的平方、209.长度最小的子数组、59.螺旋矩阵 II

214 阅读6分钟

@author LemonCat

@time 2023/7/24

@target 第一章 数组 part02 (qq.com)

今日任务

  • 977.有序数组的平方、209.长度最小的子数组、59.螺旋矩阵 II

977.有序数组的平方

题目建议: 本题关键在于理解双指针思想

题目链接:leetcode.cn/problems/sq…

文章讲解:代码随想录 (programmercarl.com)

视频讲解: www.bilibili.com/video/BV1QB…

思路

  1. 暴力法 - 将每个数平方。然后快排进行排序 -> 时间复杂度 O(nlogn)
  2. 双指针法 - 时间复杂度为 O(n)

方法一 - 暴力法

  1. for 循环遍历 将每个数平方 - n
  2. 快速排序 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.长度最小的子数组

题目建议: 本题关键在于理解滑动窗口,这个滑动窗口看文字讲解 还挺难理解的,建议大家先看视频讲解。 拓展题目可以先不做。

题目链接:leetcode.cn/problems/mi…

文章讲解:programmercarl.com/0209.%E9%95…

视频讲解:www.bilibili.com/video/BV1tZ…

方法一 - 暴力法

  • 两个 for 循环,然后不断的寻找符合条件的子序列,时间复杂度 O(n^2)
  • 已经超时 -> o.O

方法二 - 滑动窗口

  • 就是不断的调节子序列的 起始位置 和 终止位置,从而得出我们要想的结果

    • 就是用一个 for 循环 -> 去替代原来的双层 for 循环
  • 暴力解法:

    • 是一个 for 循环滑动窗口的起始位置
    • 一个 for 循环为滑动窗口的终止位置
    • 用两个 for 循环 完成了一个不断搜索区间的过程
  • 一个 for 循环遍历的是 滑动窗口的起始位置,还是终止位置?

    1. 滑动窗口的起始位置 - 那么如何遍历剩下的终止位置?此时难免再次陷入暴力解法的怪圈
    2. 所以一定是表示 滑动窗口的终止位置
  • 滑动窗口,主要确定如下三点:

    • 窗口:满足条件的长度最小的 连续子数组
    • 如何移动窗口的起始位置:如果当前窗口的值大于 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

题目建议: 本题关键还是在转圈的逻辑,在二分搜索中提到的区间定义,在这里又用上了。

题目链接:leetcode.cn/problems/sp…

文章讲解:programmercarl.com/0059.%E8%9E…

视频讲解:www.bilibili.com/video/BV1SL…

方法一 - 螺旋矩阵

  • 坚持循环不变量原则 - 每画一条边都要坚持一致的左闭右开原则

    • 这样这一圈才能按照统一的规则画下来
  • 模拟顺时针画矩阵的过程:

    • 填充上行从左到右
    • 填充右列从上到下
    • 填充下行从右到左
    • 填充左列从下到上
    • 由外向内一圈一圈这么画下去

image.png

  • 最后要注意 - 若边长为奇数 -> 按照上面的处理方式就会漏掉最后一个中心点

    • 结束返回前 记得 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)

方法二 - 螺旋矩阵简洁版

  1. 循环次数 loop 和 偏移量 setoff 本质上是相同的 整合到一起
  2. 起始点 startx 和 starty 逻辑处理过也是相同的 整合到一起
  3. 从右到左 和 从下到上 循环起始值 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;
        }
    }
 

总结

  • 二分法
  • 双指针法
  • 滑动矩阵
  • 循环不变量原则

文章链接:programmercarl.com/%E6%95%B0%E…

  • 结束 - 原神启动 ~ 明天继续加油!