【代码随想录|刷题记录Day2】977.有序数组的平方、209.长度最小的子数组、59.螺旋矩阵II

115 阅读6分钟

题目列表

  977.有序数组的平方

  209.长度最小的子数组

  59.螺旋矩阵II

  904.水果成篮

  76.最小覆盖子串

解题过程

1、977.有序数组的平方

  给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。

思路:

这道题有两种解法:
第一种是直接计算数字的平方,然后对整个数组进行快速排序,时间复杂度为O(nlogn)。

代码实现

class Solution {
    public int[] sortedSquares(int[] nums) {
        for(int i = 0; i < nums.length; i++) {
            nums[i] = nums[i] * nums[i];
        }
        Arrays.sort(nums);
        return nums;
    }
}
第二种解法就是使用双指针,观察发现,数字平方后,最大值一定由原数组两边的数字平方后产生,因此可以使用双指针,从两头到中间进行数字平方再比较,得到从大到小的有序数组。
考虑到题目要求输出从小到大的顺序,因此可以从数组最后一个位置开始赋值,从后向前插入从大到小的数字(已平方),时间复杂度为O(n)。

代码实现

class Solution {
    public int[] sortedSquares(int[] nums) {
        int left = 0, right = nums.length - 1;
        //定义一个数组,存储平方排序后的结果
        int[] result = new int[nums.length];
        //从后往前赋值
        int index = nums.length - 1; 
        //进入循环
        while (left <= right) {
            int lefts = nums[left] * nums[left];
            int rights = nums[right] * nums[right];
            if (lefts >= rights) {
                result[index--] = lefts;
                left++;
            } else {
                result[index--] = rights;
                right--;
            }
        }
        return result;
    }
}

注意

  • 从后向前进行赋值可以得到原题目要求的数组顺序。

2、209.长度最小的子数组

  给定一个含有 n 个正整数的数组和一个正整数 target

  找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0

思路:

利用滑动窗口的思路,和双指针类似,但这里考察的是两个指针中间的范围数组。
所谓滑动窗口,就是不断地调节子序列的起始位置和终止位置,从而得出我们要想的结果。
用一个for循环代替暴力所需要的两个for循环,for循环中的参数j代表的是子数组的终止位置,起始位置初始化为i=0位置,当j遍历到该子数组的和≥target时,移动起始位置,收缩子数组的长度。

代码实现

class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int left = 0, sum = 0, ans = Integer.MAX_VALUE;
        for (int right = 0; right < nums.length; right++) {
            sum += nums[right];
            while (sum >= target) {
                //更新ans的值
                ans = Math.min(ans, right -  left + 1);
                //起始位置向后移动,sum值更新,left++
                sum -= nums[left++];
            }
        }
        //是否存在这样的子数组
        return ans == Integer.MAX_VALUE ? 0 : ans;
    }
}

注意

  • for 循环中的 j 代表子数组的终止位置;
  • 终止位置向后移动时,sum 的值加上终止位置指向的值;
  • 起始位置将要向后移动时,sum 的值减去起始位置指向的值;
  • 在这个过程中,滑动窗口一直在滑动和变化。

3、59.螺旋矩阵II

  给你一个正整数 n ,生成一个包含 1 到 n^2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix

思路:

要遵循循环不变量的原则,防止陷入死循环。这里遵循 `左闭右开` 的规则进行遍历,把最后一个结点留给下一条边进行处理。

代码实现

class Solution {
    public int[][] generateMatrix(int n) {
        //while循环的次数
        int loop = 0;
        int[][] result = new int[n][n];
        //起始位置
        int start = 0;
        //当前处理位置
        int i, j;
        //定义填充数字值
        int count = 1;
        while (loop++ < n/2) {
            //模拟上侧从左往右,j能到达的最右的地方和循环次数loop有关
            for (j = start; j < n - loop; j++) { 
                result[start][j] = count++;
            }
            //模拟右侧从上到下
            for (i = start; i < n - loop; i++) {
                result[i][j] = count++;
            }
            //模拟下侧从右往左,j能到达的最左的地方就是循环次数loop
            for (; j >= loop; j--) {
                result[i][j] = count++;
            }
            //模拟左侧从下到上
            for (; i >= loop; i--) {
                result[i][j] = count++;
            }
            start++;
        }
        //边界处理,如果n是奇数,最后对未处理到的格子进行赋值
        if (n % 2 == 1) {
            result[start][start] = count;
        }
        return result;
    }
}

注意

  • 循环不变量的原则;
  • while 循环中是转的总圈数。

4、904.水果成篮

  你正在探访一家农场,农场从左到右种植了一排果树。这些树用一个整数数组 fruits 表示,其中 fruits[i] 是第 i 棵树上的水果 种类

  你想要尽可能多地收集水果。然而,农场的主人设定了一些严格的规矩,你必须按照要求采摘水果:

  • 你只有 两个 篮子,并且每个篮子只能装 单一类型 的水果。每个篮子能够装的水果总量没有限制。
  • 你可以选择任意一棵树开始采摘,你必须从 每棵 树(包括开始采摘的树)上 恰好摘一个水果 。采摘的水果应当符合篮子中的水果类型。每采摘一次,你将会向右移动到下一棵树,并继续采摘。
  • 一旦你走到某棵树前,但水果不符合篮子的水果类型,那么就必须停止采摘。

  给你一个整数数组 fruits ,返回你可以收集的水果的 最大 数目。

思路:

首先要理解题目的意思是“找至多包含两种元素的最长子串,返回其长度”。
看到最长子串,考虑滑动窗口,for循环中的终止位置向后移动,当出现的类型大于2种时,起始位置移动,然后终止位置再继续向后移动,过程中比较和更新子串的长度。

代码实现

class Solution {
    public int totalFruit(int[] fruits) {
        //声明起始位置,种类数,和最后返回的水果数
        int left = 0, total = 0, ans = 0;
        //cnts数组存储被选中的水果的种类,>=1代表被选中了
        int[] cnts = new int[fruits.length + 10]; 
        for (int right = 0; right < fruits.length; right++) {
            cnts[fruits[right]]++;
            if (cnts[fruits[right]] == 1) {
                total++;
            }
            //到了要移动起始位置的时候
            while (total > 2) { 
                //将最先放入的水果种类对应的水果全部移除
                if(--cnts[fruits[left++]] == 0) {
                    total--;
                }
            }
            //更新ans的值
            ans = Math.max(ans, right -  left + 1);
        }
        return ans;
    }
}

注意

  • 声明一个数组 cnts 代表被选中水果的个数;
  • 利用滑动窗口。

5、76.最小覆盖子串

  给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 ""

思路: 引用他人题解

1.在字符串 S 中使用双指针中的左右指针技巧,初始化 left = right = 0,把索引左闭右开区间 [left, right) 称为一个「窗口」;
2.不断地增加 right 的值扩大窗口 [left, right),直到窗口中的字符串符合要求(包含了 T 中的所有字符);
3.此时,我们停止增加 right,转而不断增加 left 指针缩小窗口 [left, right),直到窗口中的字符串不再符合要求(不包含 T 中的所有字符了)。同时,每次增加 left,我们都要更新一轮结果;
4.重复第 2 和第 3 步,直到 right 到达字符串 S 的尽头。

代码实现

class Solution {
    public String minWindow(String source, String target) {
        char[] s = source.toCharArray();
        char[] t = target.toCharArray();
        // needs是需要的字符和数量,window记录窗口中有效的字符和数量
        HashMap<Character, Integer> needs = new HashMap<>();
        HashMap<Character, Integer> window = new HashMap<>();
        // valid 变量表示窗口中满足 need 条件的字符个数
        int valid = 0;
        int left = 0, right = 0;
        // 记录最小覆盖子串的起始索引及长度
        int start = 0, len = Integer.MAX_VALUE;
        for(char c : t){
            needs.put(c, needs.getOrDefault(c, 0) + 1);
        }
        while(right < s.length){
            // c 是将移入窗口的字符
            char c = s[right];
            // 扩大窗口
            right++;
            // 进行窗口内数据的一系列更新
            if(needs.containsKey(c)){
                window.put(c, window.getOrDefault(c, 0) + 1);
                // ⭐注意:两个Integer类型的数据不能直接用< == >判断
                if(window.get(c).equals(needs.get(c))){
                    valid++;
                }
            }
            // 判断左侧窗口是否要收缩
            while(valid == needs.size()){
                // 在这里更新最小覆盖子串(更新最终结果)
                if(right - left < len){
                    start = left;
                    len = right - left;
                }
                // d 是将移要出窗口的字符
                char d = s[left];
                // 缩小窗口
                left++;
                // 进行窗口内数据的一系列更新
                if(needs.containsKey(d)){
                    window.put(d, window.get(d) - 1);
                    // window窗口内的数据无法满足,不再有效
                    // ⭐注意:两个Integer类型的数据不能直接用< == >判断
                    if(window.get(d) < Integer.valueOf(needs.get(d))){
                        valid--;
                    }
                }
            }
        }
        // 返回最小覆盖子串
        return len == Integer.MAX_VALUE ? "" : source.substring(start, start + len);
    }
}

总结

  今天的题目(2023.3.16)主要涉及双指针、滑动窗口和循环不变量的原则,难度比昨天大一点,拓展题目 76.最小覆盖子串 还需要花时间好好复习一下。