【LeetCode 每日一题】3446. 按对角线进行矩阵排序——(解法一)分组 - 排序 - 重建
Problem: 3446. 按对角线进行矩阵排序
文章目录
- 整体思路
- 完整代码
- 时空复杂度
- 时间复杂度:O(N^3)
- 空间复杂度:O(N^2)
整体思路
这段代码旨在解决一个非常特殊的矩阵排序问题:对一个 n x n
的方阵 grid
,将其主对角线方向(从左上到右下)的每一条对角线上的元素进行排序,然后用排序后的元素重构矩阵。排序规则是:主对角线(从 (0,0)
到 (n-1,n-1)
)及其下方的所有对角线按降序排序,而主对角线上方的所有对角线按升序排序。
算法采用了一种分步处理的策略:分组 -> 排序 -> 重建。
-
第一步:按对角线分组
- 算法的核心是识别出哪些元素属于同一条对角线。它利用了一个数学特性:对于同一条主对角线方向的对角线上的任意元素
(i, j)
,它们的行索引和列索引之差i - j
是一个常数。 - 代码通过
int p = i - j + n - 1;
这个公式将每个(i, j)
映射到一个对角线索引p
。i - j
的范围是-(n-1)
到n-1
。- 加上
n-1
这个偏移量,使得索引p
的范围变为0
到2n-2
,这正好对应了矩阵中2n-1
条不同的对角线,方便地用作列表的索引。
- 一个
List<List<Integer>> dia
被用来存储分组结果,dia.get(p)
就是第p
条对角线上的所有元素。
- 算法的核心是识别出哪些元素属于同一条对角线。它利用了一个数学特性:对于同一条主对角线方向的对角线上的任意元素
-
第二步:对每条对角线进行排序
- 算法遍历每一条对角线(即
dia
中的每一个列表)。 - 它根据对角线的索引
i
(也就是p
) 来决定排序规则:- 主对角线对应的索引是
p = (i - i) + n - 1 = n - 1
。 - 升序排序:如果
i < n - 1
,说明这是主对角线上方的对角线,使用(a, b) -> a - b
进行升序排序。 - 降序排序:如果
i >= n - 1
,说明这是主对角线或其下方的对角线,使用(a, b) -> b - a
进行降序排序。
- 主对角线对应的索引是
- 算法遍历每一条对角线(即
-
第三步:重建排序后的矩阵
- 最后,算法再次遍历
n x n
矩阵的每一个位置(i, j)
。 - 对于每个位置,它再次使用
p = i - j + n - 1
计算出其所属的对角线索引。 - 然后,它从
dia.get(p)
这个已排序的列表中取出第一个元素(get(0)
),并将其放入新矩阵ans[i][j]
的相应位置。 - 为了确保下一次访问同一条对角线时能取到下一个正确的元素,它将刚刚用过的元素从列表中移除(
remove(0)
)。
- 最后,算法再次遍历
完整代码
import java.util.ArrayList;
import java.util.List;class Solution {/*** 对矩阵的对角线进行特殊排序。* 主对角线上方的对角线升序排序,主对角线及下方的对角线降序排序。* @param grid 输入的 n x n 方阵* @return 排序后的新矩阵*/public int[][] sortMatrix(int[][] grid) {int n = grid.length;int[][] ans = new int[n][n];// dia: 用于存储按对角线分组的元素List<List<Integer>> dia = new ArrayList<>();// 一个 n x n 矩阵共有 2n-1 条主对角线方向的对角线for (int i = 0; i < 2 * n - 1; i++) {dia.add(new ArrayList<>());}// 步骤 1: 遍历原矩阵,将元素按对角线分组for (int i = 0; i < n; i++) {for (int j = 0; j < n; j++) {// 关键公式:i - j 对于同一条对角线是常数。// + (n - 1) 是为了将索引映射到 [0, 2n-2] 的非负区间。int p = i - j + n - 1;dia.get(p).add(grid[i][j]);}}// 步骤 2: 遍历所有对角线列表,并根据规则进行排序for (int i = 0; i < 2 * n - 1; i++) {// 主对角线的索引是 n-1。// i < n-1 表示是主对角线上方的对角线if (i < n - 1) {dia.get(i).sort((a, b) -> a - b); // 升序} else { // 主对角线及下方的对角线dia.get(i).sort((a, b) -> b - a); // 降序}}// 步骤 3: 遍历新矩阵的位置,从排好序的对角线列表中取值填充for (int i = 0; i < n; i++) {for (int j = 0; j < n; j++) {// 再次计算位置 (i, j) 对应的对角线索引 pint p = i - j + n - 1;// 从对角线 p 的列表中取出第一个元素ans[i][j] = dia.get(p).get(0);// 【效率瓶颈】将用过的元素从列表头部移除dia.get(p).remove(0); }}return ans;}
}
时空复杂度
时间复杂度:O(N^3)
- 分组循环:第一个嵌套
for
循环遍历N x N
矩阵一次,内部操作为 O(1)。总时间为 O(N^2)。 - 排序循环:
- 外层循环执行
2N-1
次。 - 内层是对每个对角线列表进行排序。设第
i
条对角线的长度为L_i
,排序时间为O(L_i log L_i)
。 - 所有对角线的总元素数
Σ L_i = N^2
。最长的对角线有N
个元素。 - 总排序时间为
Σ O(L_i log L_i)
,其上界为O(N^2 log N)
。
- 外层循环执行
- 重建循环:
- 第三个嵌套
for
循环遍历N x N
矩阵一次。 - 关键瓶颈:循环内部的
dia.get(p).remove(0)
操作。对于ArrayList
,从头部移除一个元素的时间复杂度是 O(L),其中 L 是列表的当前长度,因为需要将后续所有元素向前移动一位。 - 考虑一条长度为
L
的对角线,在重建过程中,会依次执行remove(0)
共L
次。总操作数约为L + (L-1) + ... + 1
,即 O(L^2)。 - 由于最长的对角线长度为
N
,仅处理这条对角线就需要 O(N^2) 的时间。而所有对角线长度的平方和Σ L_i^2
大致是O(N^3)
级别。因此,重建这一步的时间复杂度是 O(N^3)。
- 第三个嵌套
综合分析:
总时间复杂度由最慢的部分决定:O(N^2) + O(N^2 log N) + O(N^3)。因此,最终的时间复杂度是 O(N^3),瓶颈在于重建矩阵时低效的 remove(0)
操作。
空间复杂度:O(N^2)
- 主要存储开销:
List<List<Integer>> dia
: 这个数据结构需要存储原始矩阵中的所有N^2
个元素。因此,它占用的空间是 O(N^2)。int[][] ans
: 输出矩阵,也占用 O(N^2) 的空间。
综合分析:
算法需要一个与原矩阵同样大小的辅助数据结构来存储分组后的对角线元素。因此,其额外辅助空间复杂度为 O(N^2)。