D02代码随想录算法训练-209长度最小的子数组-59螺旋矩阵II

1. 前置知识与隐性知识

1.1 前置知识(显性知识)

一句话定义

子数组问题 本质是连续区间的动态维护 ,其核心约束为区间和的快速计算与边界移动规则
螺旋矩阵问题 本质是坐标路径的拓扑遍历 ,其核心约束为方向切换的边界触发条件

为什么这样设计

因为CPU缓存局部性原理 (数组连续存储)和方向切换的确定性(螺旋路径),导致:

  1. 滑动窗口比暴力法快 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n 2 ) → O ( n ) O(n^2)→O(n) </math>O(n2)→O(n)
  2. 边界收缩错误会引发重叠/漏填
    于是衍生出常见坑点:
  • 子数组问题:未利用正数单调性导致窗口失效
  • 螺旋矩阵:奇数阶中心点漏处理

高频算法思想

  • 双指针锚定法(滑动窗口):用左右指针动态维护可行解区间
  • 路径模拟法(螺旋矩阵):用方向向量+边界收缩控制遍历轨迹

1.2 隐性知识(专家才知道)

(一)底层机制

  1. 内存优化

    • 行优先存储:squareArray[j][i]squareArray[i][j] 慢 2-3 倍(缓存未命中)
    • 滑动窗口求和时,用 sum -= nums[left++] 而非重新计算,避免 CPU 流水线中断
  2. 边界模式

    java 复制代码
    // 万能循环不变量模板
    while (left <= right) {        // 闭区间
        int mid = left + ((right - left) >> 1);  // 防溢出
        ...
    }

(二)高阶技巧

技巧 一句话解释 典型例题
负值转正 (sum % K + K) % K 处理负数取模 LeetCode 974
方向向量法 dirs={{0,1},{1,0},{0,-1},{-1,0}} 控制螺旋方向 LeetCode 54
乘积窗口 同时维护 minProd/maxProd 处理负数反转 LeetCode 152

(三)思维模式

  • 迁移 checklist

    原结构 新结构 仍成立的条件
    数组 链表 滑动窗口思想 ✓,随机访问 ✗
    矩阵 方向向量 ✓,边界收缩 ✗
  • 测试用例 8 件套

    python 复制代码
    # 子数组问题  
    []                  # 空数组  
    [0,0,0]             # 全零  
    [10, target=5]      # 单元素解  
    [1,1,1,...,1]       # 满数组解  
    [2,3,1,2,4,3]       # 标准用例  
    [10^5, 10^5,...,10^5] # 极大数  
    [1, target=10^9]    # 无解  
    random_array(10000) # 随机数据  
    
    # 螺旋矩阵  
    n=0 → []            # 空矩阵  
    n=1 → [[1]]         # 单点  
    n=2 → [[1,2],[4,3]] # 最小偶阶  
    n=3                 # 最小奇阶(需验证中心点)  
    n=100               # 性能测试  

2. 算法详解

目标:由浅入深给出 3 套代码(暴力→前缀和→滑动窗口)。

2.1 leetcode209长度最小的子数组

markdown 复制代码
* 给定一个含有 n 个正整数的数组和一个正整数 target 。
*
* 找出该数组中满足其总和大于等于 target 的长度最小的 子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
* 示例 1:
* 输入:target = 7, nums = [2,3,1,2,4,3]
* 输出:2
* 解释:子数组 [4,3] 是该条件下的长度最小的子数组。

2.1.1 暴力破解代码(踩坑版)

java 复制代码
public static int minSubArrayLen_Force(int target, int[] nums) {
    int min = -1;
    for (int i = 0; i < nums.length; i++) {
        int left = nums[i];
        for (int j = i+1; j < nums.length; j++) {
            left += nums[j];
            if(left == target){
               int value = j - i +1;
               if(min > 0){
                   if(value<min){
                       min = value;
                   }
               } else {
                   min = value;
               }
            }
        }
    }
    return min;
}
// bug原因:边界条件没想清楚,j其实就是i,所以长度也是小于nums.length,还有差值没有算对,value值少了当前位数一位

2.1.2 滑动窗口解法

java 复制代码
public static int minSubArrayLen(int target, int[] nums) {
    int left = 0;
    int sum = 0;//充当滑动窗口管道,右边移为加,左边移为减,和为大小,索引差为长度
    int result = Integer.MAX_VALUE;
    for (int right = 0; right < nums.length ; right++) {
        sum += nums[right];
        while(sum >= target){//bugfix:题目条件没看清,少考虑了包括大于的情况
            int length = right - left +1;
            result = result > length? length : result;
            sum -=nums[left++];//由于满足条件滑动窗口左边移动,进行减值,为下一次匹配做准备,因为方向是往右走所有右侧不能减只能加
        }

    }
    return result == Integer.MAX_VALUE ? 0 : result;
}

2.1.3 二分法+前缀和数组

java 复制代码
// 使用二分法解决
public static int minSubArrayLen_BinarySearch(int target, int[] nums) {
    int n = nums.length;
    // 构建前缀和数组,方便计算子数组和
    int[] prefixSum = new int[n + 1];
    for (int i = 1; i <= n; i++) {
        prefixSum[i] = prefixSum[i - 1] + nums[i - 1];
    }

    // 二分搜索最小子数组长度
    int left = 1, right = n;
    int result = 0;

    while (left <= right) {
        int mid = left + (right - left) / 2;
        boolean found = false;

        // 检查是否存在长度为mid的子数组满足条件
        for (int i = 0; i <= n - mid; i++) {
            // 使用前缀和快速计算子数组和
            if (prefixSum[i + mid] - prefixSum[i] >= target) {
                found = true;
                result = mid;
                break;
            }
        }

        if (found) {
            right = mid - 1; // 尝试寻找更小的长度
        } else {
            left = mid + 1;  // 需要增加长度
        }
    }

    return result;
}

2.2 leetcode59螺旋矩阵II

java 复制代码
抽象正方体的四个顶点进行模拟实现
/**
 * while循环是总个数,for循环起始到结束遍历,每次遍历n-1个元素然后没遍历一次就减少
 * 从左到右&第一行已过top加一,从上到下&最后一列已过right减一,再从右到左&最后一行已过bottom减一,从下到上&第一列行已过left+1
 * @param n
 * @return
 */
public static int[][] getSquare_v1(int n){
    int[][] squareArray = new int[n][n];
    //上下左右各一个位置,每次访问都会减少一行
    int left = 0;
    int right = n - 1;
    int top = 0;
    int bottom = n - 1;
    int k = 1;

    while(k <= n*n) {
        for (int i = left; i <= right; i++,k++) squareArray[top][i] = k;
        top++;
        for (int j = top; j <= bottom; j++,k++) squareArray[j][right] = k;
        right--;
        for (int i = right; i >= left;i--,k++) squareArray[bottom][i] = k;
        bottom--;
        for (int j = bottom; j >= top;j--,k++) squareArray[j][left] = k;
        left++;
    }

    return squareArray;
}
java 复制代码
/**
*把每条边进行抽象,左右,上下,右左,下上;按照左闭右开的区间。对于n为奇数,则会生效一个,因为4条边遍历肯定是偶数,所以还需对中心点再单独处理下。
*/
public static int[][] getSquare(int n) {
    int[][] squareArray = new int[n][n];
    int startX = 0;
    int startY = 0;
    int offset = 1;
    int count = 1;
    int loop = 1;//当前圈数
    int i,j;//i是列,j是行
    while(loop <= n/2){
        //不是特别理解含义
        for( i =startY; i < n - offset; i++){
            squareArray[startX][i]=count++;
        }
        for(j =startX; j < n - offset; j++){
            squareArray[j][i] = count++;
        }
        for(;i>startY;i--){
            squareArray[j][i] = count++;
        }
        for (;j>startX;j--){
            squareArray[j][i] = count++;
        }

        startX++;
        startY++;
        offset++;
        loop++;

    }

    if(n%2 == 1){
        squareArray[startX][startY] = count;
    }
    return squareArray;
}

3. 算法听-想-变-用

把"会做一道题"升级为"会解决一类问题"。

3.1 听(反学习)

专家视角

  • 子数组问题本质是区间操作,核心在于高效计算区间和(前缀和)或动态维护区间属性(滑动窗口)
  • 螺旋矩阵是路径模拟类问题的典型,关键在于建立坐标映射和边界管理

新手误区

  1. 暴力解法常犯错误:

    • 边界处理错误(如结束条件 j < nums.length 误写为 j <= nums.length
    • 忽略子数组和大于等于目标值的情况(只判断相等)
    • 未考虑全零数组等边界情况
  2. 螺旋矩阵常见问题:

    • 坐标变换时行列混淆
    • 未处理奇数阶矩阵的中心点
    • 边界收缩时机错误导致重叠填充

3.2 想(参考答案思维)

条件穷举表

条件 已用/未用 优化可能性
数组元素均为正整数 已用 滑动窗口成立的核心前提
子数组连续性要求 已用 前缀和优化的基础
和>=target的约束 已用 滑动窗口收缩条件
最小长度要求 已用 驱动结果值动态更新

数据结构映射表

物理概念 数学抽象 代码表示
子数组和 区间和 prefixSum[j]-prefixSum[i]
滑动窗口 动态区间 left/right双指针
螺旋路径 坐标变换序列 (x,y) + 方向向量
边界收缩 规模递减 left++/right--

3.3 变(深层迁移)

模式提炼 4 层模型

层次 案例 迁移提问
物理结构 一维数组 树状数组/链表能否适用?
逻辑操作 区间和计算 如何迁移到区间极值/区间乘积问题?
数学抽象 前缀和单调性 能否用于解决众数统计问题?
系统思维 边界收缩策略 如何迁移到分形图形生成问题?

验证矩阵

原场景 新场景 匹配度 需调整
子数组和(209) 乘积最小子数组(152) 85% 需维护最大/最小值
螺旋矩阵II(59) 螺旋矩阵I(54) 95% 遍历方向逻辑复用
滑动窗口 无重复字符子串(3) 90% 改为哈希表维护字符
前缀和+二分 和可被K整除子数组(974) 75% 需处理负数取模

3.4 用(聚焦 + 模式化)

聚焦:不变 & 经典

无论题目如何变形,连续子数组的区间可加性螺旋矩阵的边界对称性永不改变

模式化:代码模板 & 触发器

模式化:模板与触发器

java 复制代码
// ================= 滑动窗口模板 =================
int slidingWindowTemplate(int[] nums, int target) {
    int left = 0, sum = 0, res = Integer.MAX_VALUE;
    for (int right = 0; right < nums.length; right++) {
        sum += nums[right];                // 1. 扩展右边界
        while (满足触发条件) {              // 2. 触发收缩
            res = Math.min(res, right-left+1); // 3. 更新结果
            sum -= nums[left++];           // 4. 收缩左边界
        }
    }
    return res == MAX_VALUE ? 0 : res;
}

// 触发器伪代码
if (problem.contains("连续子数组") 
 && problem.contains("和/积") 
 && problem.contains("最值")) {
    调用滑动窗口模板;
    调整触发条件(和/积/字符统计);
}
java 复制代码
// 触发器伪代码
if (problem.contains("螺旋遍历") 
 || problem.contains("回型路径")
 || problem.contains("层状填充")) {
    调用螺旋矩阵模板;
    调整方向顺序(顺时针/逆时针);
}

4. 额外补充

用MECE法则对编程的数学知识分类,然后四象限归类识别实用类的知识点,然后攻克难点

高实用&易掌握 | 高实用&难掌握

markdown 复制代码
        |  象限1:      |  象限2:
        |  基础算术    |  数论进阶
        |  位运算      |  图论算法
        |  简单几何    |  动态规划优化
        |  排列组合基础|  计算几何
相关推荐
程序员爱钓鱼几秒前
Go语言实战案例-括号匹配算法
后端·google·go
程序员爱钓鱼5 分钟前
Go语言实战案例-判断字符串是否由另一个字符串的字母组成
后端·google·go
郝学胜-神的一滴2 小时前
SpringBoot实战指南:从快速入门到生产级部署(2025最新版)
java·spring boot·后端·程序人生
爷_7 小时前
字节跳动震撼开源Coze平台!手把手教你本地搭建AI智能体开发环境
前端·人工智能·后端
不过普通话一乙不改名10 小时前
第一章:Go语言基础入门之函数
开发语言·后端·golang
豌豆花下猫11 小时前
Python 潮流周刊#112:欢迎 AI 时代的编程新人
后端·python·ai
Electrolux11 小时前
你敢信,不会点算法没准你赛尔号都玩不明白
前端·后端·算法
whhhhhhhhhw11 小时前
Go语言-fmt包中Print、Println与Printf的区别
开发语言·后端·golang
ん贤12 小时前
Zap日志库指南
后端·go
Spliceㅤ12 小时前
Spring框架
java·服务器·后端·spring·servlet·java-ee·tomcat