代码随想录算法训练营Day2|数组part2与数组篇总结

163 阅读4分钟

LeetCode 209 长度最小的子数组

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

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

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

思路

滑动窗口

  • 不断调整子序列的起始和终止位置,是一种特殊的双指针
  • 实现滑动窗口
    • 窗口内要保持什么:总和大于等于 target 的长度最小的 连续子数组
    • 如何移动窗口起始位置:当已满足上述条件,则窗口该缩小了,起始位置+1
    • 如何移动窗口结束位置:当不满足上述条件,则窗口该变大了,结束位置+1
  • 时间复杂度
    • 移动起始位置和移动结束位置仍需要两层循环
    • 但因为每个数组中的元素都只被操作两次(进窗口,出窗口),所以时间复杂度为 O(n)O(n)
  • 滑动窗口本质上是一种不完全的遍历,适用于要找满足某条件的某子序列时,对遍历到的每个序列记录与条件相关的属性

难点

如何处理最小长度的记录:

  • 在从内部循环跳出后需要更新最小长度记录,但是此时的子序列不一定满足题设条件,因为可能窗口右侧已经超出。所以仍需判断sum是否大于target
  • 最小长度的初始值应该设置为比数组长度更大的值,推荐Integer.MAX_VALUE。因为有一种情况是最小长度确实是数组长度,如果初始值也是数组长度,会无法和找不到满足条件的子序列做区分

LeetCode 59 螺旋矩阵2️⃣

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

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

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

思路

既然考察对代码的控制能力,就要选定一个统一的边界: 对于边长为n的正方形,每条边处理n-1个元素,这样处理四次正好处理完最外面一圈 整体思路:

  1. 先初始化一个形状确定的矩阵
  2. 每次处理最外圈后,边长-2
  3. 如果边长迭代为为0或1,单独处理
    1. 0:直接返回
    2. 1:处理中心元素后返回

难点

在每次遍历完最外圈后,指针会回到此圈的起始位置。此时需要把行列索引都+1,才能继续下一圈的遍历

卡码网 58 区间和

题目链接:58区间和

文档讲解:www.programmercarl.com/kamacoder/0…

思路

首先想到暴力:对每一对查询都依次叠加元素求和 但这样对大查询次数,时间复杂度很大,且有多次重复计算 如何避免重复计算?👉前缀和

前缀和

前缀和常用于计算区间和 大致思路:

  1. 统计此数组每个索引对应的前缀和
  2. 对于每个区间查询,只需要用区间右索引的前缀和减去区间左索引前序的前缀和

难点

由于输入的区间是闭区间,做前缀和相减时要考虑索引是否有效

代码

import java.util.Scanner;

public class Main{
    public static void main (String[] args) {
        Scanner scanner = new Scanner(System.in);
        
        int n = scanner.nextInt();
        int[] arr = new int[n];
        int[] presum = new int[n];
        for (int i = 0; i < n; i++) {
            arr[i] = scanner.nextInt();
            if (i == 0) {
                presum[i] = arr[i];
            } 
            else {
                presum[i] = presum[i-1] + arr[i];
            }
        }
        while (scanner.hasNext()) {
            int i = scanner.nextInt();
            int j = scanner.nextInt();
            int sum;
            if (i > 0) {
                sum = presum[j] - presum[i-1];
            }
            else {
                sum = presum[j];
            }
            System.out.println(sum);
        }
    }
}

卡码网 44 开发商购买土地

题目链接:kamacoder.com/problempage…

文档讲解:www.programmercarl.com/kamacoder/0…

思路

第一反应是暴力解法:横向遍历每种解法,纵向遍历每种解法,找到价差最小的 结合上一题的前缀和,可以减少在暴力解法中重复计算 大致思路:

  1. 对横向和纵向分别计算前缀和数组
  2. 遍历每种分割,对每种分割求解价差,保留最小值

难点

尽管这道题思路挺清晰,但在求和的时候也比较考验代码的控制能力。 计算前缀和时我分为了两步:

  1. 读入输入时顺便计算每行每列的和存储在数组中
  2. 全部读完后把行列和依次累加,得到需要的两个前缀和数组

代码

import java.util.*;

public class Main {
    public static void main (String[] args) {
        Scanner scanner = new Scanner(System.in);
        
        int n = scanner.nextInt();
        int m = scanner.nextInt();
        
        int[][] matrix = new int[n][m];
        int[] presumHrz = new int[n];   // 先行和
        int[] presumVtc = new int[m];   // 先列和
        
        for (int i = 0; i < n; i++) {
            // 先记录每行列和
            for (int j = 0;j < m ; j++) {
                matrix[i][j] = scanner.nextInt();
                presumHrz[i] += matrix[i][j];
                presumVtc[j] += matrix[i][j];
            } 
        } 
        // 根据行列和计算前缀和
        // 计算水平分割的前缀和
        for (int i = 1; i < n; i++) {
            presumHrz[i] += presumHrz[i-1];
        } 
        // 计算竖直分割的前缀和
        for (int i = 1; i < m; i++) {
            presumVtc[i] += presumVtc[i-1];
        } 
        
        int minDiff = Integer.MAX_VALUE;
        // 水平分割
        for (int i = 0; i < n-1; i++) {
            int diff = Math.abs(presumHrz[i] * 2 - presumHrz[n-1]);
            if (diff < minDiff) {
                minDiff = diff;
            } 
        }
        // 竖直分割
        for (int i = 0; i < m-1; i++) {
            int diff = Math.abs(presumVtc[i] * 2 - presumVtc[m-1]);
            if (diff < minDiff) {
                minDiff = diff;
            } 
        }
        System.out.println(minDiff);
        
    }

数组专题总结

数组总结.png

今日收获总结

今天算法学习三小时,滑动窗口是复习知识点,边界条件的处理需要注意。前缀和是第一次学习,也是求子序列和的常用方法。此外卡码网的算法题也督促我复习了ACM输入输出模式。