LeetCode 每日一题笔记
0. 前言
- 日期:2025.03.20
- 题目:3567.子矩阵的最小绝对差
- 难度:中等
- 标签:数组 矩阵 排序 滑动窗口
1. 题目理解
问题描述 :
给你一个 m x n 的整数矩阵 grid 和一个整数 k。对于矩阵 grid 中的每个连续的 k x k 子矩阵,计算其中任意两个不同值之间的最小绝对差。返回一个大小为 (m - k + 1) x (n - k + 1) 的二维数组 ans,其中 ans[i][j] 表示以 grid 中坐标 (i, j) 为左上角的子矩阵的最小绝对差。
注意:如果子矩阵中的所有元素都相同,则答案为 0。
示例:
输入: grid = [[1,8],[3,-2]], k = 2
输出: [[2]]
解释:
只有一个可能的 k x k 子矩阵:[[1, 8], [3, -2]]。
子矩阵中的不同值为 [1, 8, 3, -2],排序后为 [-2, 1, 3, 8]。
最小绝对差为 |1 - 3| = 2,因此答案为 [[2]]。
2. 解题思路
核心观察
- 任意数组中最小绝对差 一定出现在排序后相邻的元素之间,无需计算所有元素两两之间的差值;
- 每个 k×k 子矩阵需要遍历其内部所有元素,提取后排序,再计算相邻元素的最小差值;
- 当 k=1 时,子矩阵仅有一个元素,无"不同值",直接返回 0;当子矩阵所有元素相同时,最小绝对差也为 0。
算法步骤
- 确定结果矩阵大小 :结果矩阵的行数为
m - k + 1,列数为n - k + 1; - 遍历所有 k×k 子矩阵 :
- 以 (i,j) 为左上角,遍历所有合法的 k×k 子矩阵;
- 提取当前子矩阵的所有元素,存入一维数组;
- 计算最小绝对差 :
- 对提取的数组排序;
- 若 k=1,直接赋值最小差值为 0;
- 否则遍历排序后的数组,计算相邻元素的绝对差,记录最小值;
- 若所有元素相同(最小值未更新),赋值为 0;
- 填充结果矩阵:将每个子矩阵的最小绝对差存入结果矩阵对应位置。
3. 代码实现
java
package com.sheeta1998.lec.lc3567;
import java.util.Arrays;
public class Solution {
public int[][] minAbsDiff(int[][] grid, int k) {
int m = grid.length;
int n = grid[0].length;
int[][] ans = new int[m + 1 - k][n + 1 - k];
for (int i = 0; i < m + 1 - k; i++) {
for (int j = 0; j < n + 1 - k; j++) {
int res = Integer.MAX_VALUE;
int[] ints = new int[k * k];
int count = 0;
for (int l = i; l < k + i; l++) {
for (int o = j; o < k + j; o++) {
ints[count++] = grid[l][o];
}
}
int[] sortedInts = Arrays.stream(ints).sorted().toArray();
if (k == 1) {
res = 0;
} else {
for (int l = 1; l < k * k; l++) {
if (sortedInts[l - 1] != sortedInts[l]) {
res = Math.min(res, Math.abs(sortedInts[l] - sortedInts[l-1]));
}
}
}
if (res == Integer.MAX_VALUE) {
res = 0;
}
ans[i][j] = res;
}
}
return ans;
}
}
4. 代码优化说明
优化点1:替换流式排序为普通排序(提升性能)
Arrays.stream(ints).sorted().toArray() 会产生额外的流对象和数组拷贝,替换为 Arrays.sort(ints) 直接排序原数组,减少内存开销:
java
// 优化后
Arrays.sort(ints);
// 后续直接使用ints,无需sortedInts变量
for (int l = 1; l < k * k; l++) {
if (ints[l - 1] != ints[l]) {
res = Math.min(res, Math.abs(ints[l] - ints[l-1]));
}
}
优化点2:提前终止差值计算(剪枝)
若遍历过程中找到差值为 0 的情况,可直接终止当前子矩阵的差值计算(0 已是最小绝对差):
java
for (int l = 1; l < k * k; l++) {
int diff = Math.abs(ints[l] - ints[l-1]);
if (diff == 0) {
res = 0;
break; // 0是最小值,无需继续计算
}
res = Math.min(res, diff);
}
优化点3:滑动窗口优化(降低时间复杂度)
对于大矩阵+大k的场景,上述暴力法会重复遍历元素,可通过滑动窗口+有序集合(如TreeSet)优化:
- 先预处理每行的滑动窗口,再处理列的滑动窗口;
- 每次窗口滑动时,仅移除离开的元素、添加进入的元素,无需重新提取所有元素;
- 该优化可将时间复杂度从 (O((m-k+1)(n-k+1)k^2\log k)) 降至 (O(mn k \log k))。
5. 复杂度分析
-
时间复杂度:(O((m-k+1)(n-k+1) \times (k^2 + k^2\log k)) = O((m-k+1)(n-k+1)k^2\log k))
- 遍历所有 k×k 子矩阵:((m-k+1)(n-k+1)) 次循环;
- 提取子矩阵元素:每次 (O(k^2));
- 排序子矩阵元素:每次 (O(k^2\log k));
- 计算最小差值:每次 (O(k^2))(可忽略,远小于排序耗时)。
-
空间复杂度:(O(k^2))
- 存储子矩阵元素的一维数组占用 (k^2) 空间;
- 结果矩阵空间为 (O((m-k+1)(n-k+1))),属于输出空间,不计入算法额外空间。
6. 总结
- 核心思路是暴力枚举 + 排序求最小相邻差:通过枚举所有 k×k 子矩阵,利用"排序后相邻元素最小差"的特性简化计算;
- 关键技巧:无需计算所有元素两两差值,仅需计算排序后相邻元素的差值,大幅减少计算量;
- 优化方向:替换流式排序提升性能,或通过滑动窗口+有序集合优化大矩阵场景下的时间复杂度。
关键点回顾
- 数组最小绝对差的核心性质:排序后相邻元素的最小差值即为整个数组的最小绝对差;
- 边界处理:k=1 或子矩阵元素全相同的情况,最小绝对差为 0;
- 性能优化:避免流式排序的额外开销,大矩阵场景可采用滑动窗口减少重复计算。