代码随想录算法训练营第二天| 209. 长度最小子数组、59. 螺旋矩阵Ⅱ、前缀和

今日内容

  • leetcode.209 长度最小子数组
  • leetcode. 59 螺旋矩阵Ⅱ
  • 前缀和

之前刷的时候还没有区间和,结果不知道什么时候更新了。果然是温故而知新。

Leetcode. 209 长度最小子数组

文章链接:代码随想录 (programmercarl.com)

题目链接:209. 长度最小的子数组 - 力扣(LeetCode)

本题引入了滑动窗口的概念。其实说是说滑动窗口,本质上还是双指针。

此处需要明确的一点就是:当我们循环时,调整的是滑动窗口的起始指针还是终点指针呢?

假设我们在循环里调整的是滑动窗口的起始指针,那么就会发现我们不知道终点指针该在何处停下,这就又需要我们循环遍历符合条件的终点指针。没错,又回到暴力法了。

所以滑动窗口在循环中是调整终点指针。

这样,就有如下步骤:

  1. 循环调整滑动窗口的终点指针。
  2. 当窗口内元素符合条件,记录当前窗口。将起始指针往后移动(相当于把窗口首个元素剔除)。
  3. 若起始指针移动后窗口内元素依然符合条件,则继续重复第2步;否则,再从第1步开始。

本题代码如下:

class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int start = 0; // start:起始指针
        int sum = 0;
        int min = Integer.MAX_VALUE;
        for (int end = 0; end < nums.length; end++){ // end: 终点指针
            sum += nums[end];
            while (sum >= target){ // 满足条件,调整起始指针。注意用的是while
                min = min < (end - start) + 1 ? min : (end - start) + 1;
                sum = sum - nums[start];
                start++; // 移动起始指针
            }
        }
        if (min == Integer.MAX_VALUE){min = 0;} // 如果遍历后min依然为int最大值,说明没找到。赋值为0
        return min;
    }
}
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

Leetcode. 59 螺旋矩阵Ⅱ

文章链接:代码随想录 (programmercarl.com)

题目链接:59. 螺旋矩阵 II - 力扣(LeetCode)

这道题重要的还是循环不变量,确定好整个代码的区间定义就比较清晰了。

当然上面的只是思路,我也知道该这样做,但是思路虽然清晰,我代码写不出来口牙!!!!!代码能力属实弱鸡了。

在本题中,我们确定区间为左闭右开:

这样就不会糊里糊涂了。

代码如下:

class Solution {
    public int[][] generateMatrix(int n) {
        int[][] result = new int[n][n];
        // 起始X和Y
        int startX = 0;
        int startY = 0;
        int offset = 1; // 偏移量
        int count = 1; // 当前填入数字
        int circle = n / 2; // 要旋转的圈数
        int i, j;// 真正要移动的X和Y, i表示行,j表示列
        
        while (circle > 0){
            // 从左到右的循环
            for (j = startY; j < n - offset; j++){
                result[startX][j] = count;
                count++;
            }
            // 从上到下的循环
            for (i = startX; i < n - offset; i++){
                result[i][j] = count; // 此处因为之前的循环使 j 已经到了最右边的列,所以指代列的索引可以使用 j 
                count++;
            }
            // 从右到左的循环
            for (; j > startY; j--){ // 此处不做初始化定义是因为填充列的操作只需要左右移动就好,真正决定要填充哪一行的列依靠的是 i 指向哪一行
                result[i][j] = count; // i 已经指向了最下边的行,可以直接用它当行索引
                count++;
            }
            // 从下到上的循环
            for (; i > startX; i--){ // 与从右到左同理
                result[i][j] = count; // j 已经指向最左边的列,直接用它当列索引
                count++;
            }
            // 跑完一轮,更新参数
            startX++;
            startY++;
            offset++;
            circle--;
        }
        if (n % 2 != 0){
            result[startX][startY] = count;
        }
        return result;
    }
}

感觉主要难点还是在于突然增多的参数量。

下面我按照个人理解,整理整理这段代码中的各个参数:

  •  startX,startY:这两个参数就是划定起点的,它在整个代码中只会在一圈跑完后进行变更。
  • offset:偏移量。因为区间定义为左闭右开,所以offset的作用就是保证区间定义无误。
  • count:这个没什么说的,就是要填的数字。
  • circle:要转的圈数。至于为什么 n / 2 就能得出要转的圈数,个人是这么想的:转完一圈之后,最上面一行和最下面一行就不管了,也就是说一圈之后会少两行。而现在总共有 n 行,也就是说会最多会减少 n / 2 次。所以 n / 2 就是圈数。
  • i,j:前面的 startX,startY 指明了起点,i 和 j 才是真正跑来跑去用来定位的牛马。

前缀和

前缀和非常适合解决区间和问题。

区间和,顾名思义,就是求数组的某一区间的元素和。

面对这种问题,第一反应就是使用暴力法。但假如我们极端一点,我们要查询 m 次,每次查询范围为 0 到 n - 1,那么它的时间复杂度为O(n * m)。当查询次数多起来的话,那耗时已经不敢想象了。

基于此就引入前缀和的思想。

前缀和:重复利用计算过的子数组之和,从而降低区间查询需要累加计算的次数。

以下图为例,简单解释一下何为前缀和:

vec[i] 表示数组,p[i] 表示从下标 0 到下标 i 的累加和

所以当我们想要求区间 2 到 5 的区间和时,就可以使用 p[5] - p[1]得到,因为:

p[1] = vec[0] + vec[1];

p[5] = vec[0] + vec[1] + vec[2] + vec[3] + vec[4] + vec[5];

p[5] - p[1] = vec[2] + vec[3] + vec[4] + vec[5];

  这样仅需要时间复杂度O(1)就可以获得区间和了。

卡码网. 58 区间和

文章链接:58. 区间和 | 代码随想录 (programmercarl.com)

题目链接:58. 区间和(第九期模拟笔试) (kamacoder.com)

本题直接用前缀和就好,代码如下:

import java.util.Scanner;

class Main{
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        
        int n = sc.nextInt(); // 数组长度
        int[] array = new int[n]; // 整数数组
        int[] prefix = new int[n]; // 前缀和数组
        int sum = 0; // 前缀和
        
        for (int i = 0; i < n; i++){
            array[i] = sc.nextInt();
            sum += array[i];
            prefix[i] = sum;
        }
        
        while (sc.hasNextInt()){
            int a = sc.nextInt();
            int b = sc.nextInt();
            
            if (a > b){
                continue;
            } else if (a == 0){
                int output = prefix[b];
                System.out.println(output);
            } else {
                int output = prefix[b] - prefix[a - 1];
                System.out.println(output);
            }
        }
        sc.close();
    }
}
  • 时间复杂度:O(1)
  • 空间复杂度:O(n)

卡码网. 44 开发商购买土地

文章链接:44. 开发商购买土地 | 代码随想录 (programmercarl.com)

题目链接:44. 开发商购买土地(第五期模拟笔试) (kamacoder.com)

这道题一开始看代码随想录上的文字描述没怎么读懂,到题目链接上看到题之后才看懂....主要看不懂的地方在于“提示信息”那一块。

看懂之后,本题总结起来就是说:有一块 n * m 的区域,可以按照横向或纵向划分为两个子区域,我们需要找到两个子区域间差距最小的划分。

此题本质上就是要比较若干行(或者列)之间的数值和。一看到数值和,直接联系上前缀和。我们就做一个行的前缀和数组,一个列的前缀和数组,然后进行比较。

代码如下:

import java.util.Scanner;

class Main{
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        // 区域地块的行列数
        int n = sc.nextInt();
        int m = sc.nextInt();
        // 区域地块二维数组
        int[][] allArea = new int[n][m];
        int sum = 0;
        
        for (int i = 0; i < n; i++){
            for (int j = 0; j < m; j++){
                allArea[i][j] = sc.nextInt();
                sum += allArea[i][j]; // 区域地块的总体权值
            }
        }
        
        int[] horizon = new int[n]; // 行的前缀和数组
        for (int i = 0; i < n; i++){
            for (int j = 0; j < m; j++){
                if (i == 0){
                    horizon[i] += allArea[i][j];
                }else{
                    horizon[i] = horizon[i - 1] + allArea[i][j];
                }
            }
        }
        
        int[] vertical = new int[m]; // 列的前缀和数组
        for (int j = 0; j < m; j++){
            for (int i = 0; i < n; i++){
                if (j == 0){
                    vertical[j] += allArea[i][j];
                }else{
                    vertical[j] = vertical[j - 1] + allArea[i][j];
                }
            }
        }
        
        int result = Integer.MAX_VALUE;
        for (int i = 0; i < horizon.length; i++){
            int elem = sum - horizon[i]; // horizon[i] 表示前 i 行的土地价值。所以 elem 表示剩下几行的土地价值
            result = result < Math.abs(horizon[i] - elem) ? result : Math.abs(horizon[i] - elem);
        }
        
        for (int i = 0; i < vertical.length; i++){
            int elem = sum - vertical[i]; // vertical[i] 表示前 i 列的土地价值。所以 elem 表示剩下几列的土地价值
            result = result < Math.abs(vertical[i] - elem) ? result : Math.abs(vertical[i] - elem);
        }
        
        System.out.println(result);
        sc.close();
    }
}
  • 时间复杂度:O(n ^ 2)
  • 空间复杂度:O(max(n, m))【算法新手,不确定是不是这样的】

总结

至此,数组相关的问题就解决了。

其中知道了数组是存放在连续内存空间中的相同类型的数据集合,数组元素的删除实际上是覆盖。

之后从数组的经典问题中总结出了几种解决方法:

  • 二分法:适用于 有序、元素不重复 的数组,注意循环不变量
  • 双指针:常见于数组和链表
  • 滑动窗口:本质上也是双指针,注意滑动窗口调整的是终点指针。
  • 前缀和:适用于解决数组区间元素和的问题。

再次刷题收获满满,真的温故而知新吧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

DonciSacer

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值