1. 前置知识与隐性知识
1.1 前置知识(显性知识)
一句话定义
子数组问题 本质是连续区间的动态维护 ,其核心约束为区间和的快速计算与边界移动规则 。
螺旋矩阵问题 本质是坐标路径的拓扑遍历 ,其核心约束为方向切换的边界触发条件。
为什么这样设计
因为CPU缓存局部性原理 (数组连续存储)和方向切换的确定性(螺旋路径),导致:
- 滑动窗口比暴力法快 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n 2 ) → O ( n ) O(n^2)→O(n) </math>O(n2)→O(n)
- 边界收缩错误会引发重叠/漏填
于是衍生出常见坑点:
- 子数组问题:未利用正数单调性导致窗口失效
- 螺旋矩阵:奇数阶中心点漏处理
高频算法思想
- 双指针锚定法(滑动窗口):用左右指针动态维护可行解区间
- 路径模拟法(螺旋矩阵):用方向向量+边界收缩控制遍历轨迹
1.2 隐性知识(专家才知道)
(一)底层机制
-
内存优化
- 行优先存储:
squareArray[j][i]
比squareArray[i][j]
慢 2-3 倍(缓存未命中) - 滑动窗口求和时,用
sum -= nums[left++]
而非重新计算,避免 CPU 流水线中断
- 行优先存储:
-
边界模式
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 听(反学习)
专家视角:
- 子数组问题本质是区间操作,核心在于高效计算区间和(前缀和)或动态维护区间属性(滑动窗口)
- 螺旋矩阵是路径模拟类问题的典型,关键在于建立坐标映射和边界管理
新手误区:
-
暴力解法常犯错误:
- 边界处理错误(如结束条件
j < nums.length
误写为j <= nums.length
) - 忽略子数组和大于等于目标值的情况(只判断相等)
- 未考虑全零数组等边界情况
- 边界处理错误(如结束条件
-
螺旋矩阵常见问题:
- 坐标变换时行列混淆
- 未处理奇数阶矩阵的中心点
- 边界收缩时机错误导致重叠填充
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:
| 基础算术 | 数论进阶
| 位运算 | 图论算法
| 简单几何 | 动态规划优化
| 排列组合基础| 计算几何