将矩阵按对角线排序 图解题解
这道题到底在问什么
- 输入
- mat = [[3,3,1,1],[2,2,1,2],[1,1,1,2]]
- 输出
- [[1,1,1,1],[1,2,2,2],[1,2,3,3]]
- 输入
- 主对角线 (0,0)(1,1)(2,2) 上的数 3、2、1
- 输出
- 升序排成 1、2、3,再写回这三格
- 输入
- 怎么判断两格在同一条对角线
- 输出
- 看 i-j 是否相等,相等就在同一条
最优解:为什么这么做
一句话答案:LeetCode 1329 将矩阵按对角线排序:同一条右下对角线上 i-j 是常数,就按 i-j 把数分组、组内升序再原位写回,时间 O(m·n·log(min(m,n)))。
对角线各自排成升序,最后返回的是什么
给一个 m 行 n 列的矩阵 mat,它的对角线是沿右下方向的斜线,从最上一行或最左一列的某格出发一路走到底。题目要把每条对角线单独拎出来,线上的数从小到大排成升序,其余位置不动,返回整个矩阵。题面 mat=[[3,3,1,1],[2,2,1,2],[1,1,1,2]] 处理完是 [[1,1,1,1],[1,2,2,2],[1,2,3,3]]。
同一条对角线上的格子,凭什么一眼认出来
认对角线是这题的门槛:怎么知道 (0,0) 和 (1,1)、(2,2) 是一伙的?沿右下走一步,行号加一、列号也加一,i-j 一减一加正好不变。所以同一条右下对角线上的格子 i-j 全相等——主对角线三格的 i-j 都是 0,右上那条 (0,1)、(1,2)、(2,3) 都是 -1。而 i+j 相等的是右上的反对角线,方向拧着,别认错。格子归哪条线,减一下 i-j 就知道。
按 i-j 分组、排序、写回,负下标怎么办
剩下是三步:先按 i-j 把每个数扔进对应的桶,再把每个桶单独升序排好,最后沿右下从上往下写回,最小的落最左上的格子。有个小坑,i 小 j 大时 i-j 是负的,直接当数组下标会越界。办法是偏移一个常数,比如 i-j+n 或参考代码里的 m-i+j,把编号压进 0 到 m+n-1 这段非负区间;或者用哈希表按 i-j 归桶,键是负数也无妨。
参考代码明明排的是降序,怎么写回却成了升序
参考代码有个反直觉处:每个桶排的是降序(e.sort(reverse=True)),矩阵却成了升序。诀窍在写回用的是 pop()——从桶尾弹数,降序数组的末尾恰好是最小值。写回按行列顺序遍历,同一条对角线总先碰到最左上的格子,弹出的又是当前最小的数,于是一路填下来就是升序,省了一次反转。
主对角线先走一趟,看 3×4 矩阵怎么排
盯住 i-j 等于 0 这条主对角线,三格原始值 mat[0][0]=3、mat[1][1]=2、mat[2][2]=1,沿右下收集得 3、2、1,升序排好是 1、2、3。再从上往下写回:第 1 小的 1 填进 (0,0),第 2 小的 2 填进 (1,1),最大的 3 填进 (2,2)。
别的对角线同理:i-j 等于 -1 那条 (0,1)、(1,2)、(2,3) 收到 3、1、2,排成 1、2、3;四个角上只有一格的短对角线本身就升序,不动。六条线处理完,就是开头那个升序矩阵。
方向一记反,右上那组斜线就被你排了
最容易栽的是方向:判据记成 i+j 相等,排的就成了右上那组斜线,答案整片错位。其次是负下标,i-j 为负时不偏移就越界。还有写回方向,得沿右下从上往下把最小值放左上;从右下往左上写、或降序填回,对角线就反了。
复杂度上,收集和写回各扫一遍是 O(m·n),开销主要在排序:所有对角线共 m·n 个数分段排,总量 O(m·n·log(min(m,n)))。空间上参考代码用桶存全部元素占 O(m·n),逐条处理可压到 O(min(m,n))。
两个边界顺手验:单行或单列时每条对角线只剩一格,原样返回;矩阵每条对角线本就升序时,排完不变,也原样返回。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3三步走:按 i-j 把格子分到各条对角线,每条单独升序,再沿右下从上往下把数从小到大写回。i-j 可能为负,记得偏移成非负下标。
- 4这是要处理的 3 行 4 列矩阵,行号 i 从上到下 0、1、2,列号 j 从左到右 0 到 3。接下来一条一条对角线来处理:每条线先整体点亮、收集线上的数,再升序排好,然后从最上面那格起把数从小到大写回。处理完的格子会标成蓝色定稿,正在处理的标橙色。先看主对角线。
- 5点亮的是 i-j 等于 0 这条对角线,经过 (0,0)、(1,1)、(2,2)。沿右下从上往下把数收集起来,得到 3、2、1。右边面板就是这条线现在的原始排列,还没排序。
- 6把刚收集的 3、2、1 单独升序排一下,得到 1、2、3。注意只在这条对角线内部排,不和别的对角线混。排好后就该把它们从上往下写回去,最小的写在最靠左上的那格。
- 7把这条线第 1 小的数 1 写进格子 (0,0)。它是从上往下第 1 个位置,正好放第 1 小的数。右边面板里还剩 2、3,等着填进更靠右下的格子。
- 8把这条线第 2 小的数 2 写进格子 (1,1)。它是从上往下第 2 个位置,正好放第 2 小的数。右边面板里还剩 3,等着填进更靠右下的格子。
- 9把这条线最大的数 3 写进格子 (2,2)。它是从上往下第 3 个位置,正好放第 3 小的数。这条对角线已经全部就位。
- 10点亮的是 i-j 等于 -1 这条对角线,经过 (0,1)、(1,2)、(2,3)。沿右下从上往下把数收集起来,得到 3、1、2。右边面板就是这条线现在的原始排列,还没排序。
- 11把刚收集的 3、1、2 单独升序排一下,得到 1、2、3。注意只在这条对角线内部排,不和别的对角线混。排好后就该把它们从上往下写回去,最小的写在最靠左上的那格。
- 12把这条线第 1 小的数 1 写进格子 (0,1)。它是从上往下第 1 个位置,正好放第 1 小的数。右边面板里还剩 2、3,等着填进更靠右下的格子。
- 13把这条线第 2 小的数 2 写进格子 (1,2)。它是从上往下第 2 个位置,正好放第 2 小的数。右边面板里还剩 3,等着填进更靠右下的格子。
- 14把这条线最大的数 3 写进格子 (2,3)。它是从上往下第 3 个位置,正好放第 3 小的数。这条对角线已经全部就位。
- 15点亮的是 i-j 等于 -2 这条对角线,经过 (0,2)、(1,3)。沿右下从上往下把数收集起来,得到 1、2。右边面板就是这条线现在的原始排列,还没排序。
- 16把刚收集的 1、2 单独升序排一下,得到 1、2。注意只在这条对角线内部排,不和别的对角线混。排好后就该把它们从上往下写回去,最小的写在最靠左上的那格。
- 17把这条线第 1 小的数 1 写进格子 (0,2)。它是从上往下第 1 个位置,正好放第 1 小的数。右边面板里还剩 2,等着填进更靠右下的格子。
- 18把这条线最大的数 2 写进格子 (1,3)。它是从上往下第 2 个位置,正好放第 2 小的数。这条对角线已经全部就位。
- 19这条对角线只有一个格子 (0,3),值是 1。单个元素本身就是升序,不用排也不用动,直接定稿。
- 20点亮的是 i-j 等于 1 这条对角线,经过 (1,0)、(2,1)。沿右下从上往下把数收集起来,得到 2、1。右边面板就是这条线现在的原始排列,还没排序。
- 21把刚收集的 2、1 单独升序排一下,得到 1、2。注意只在这条对角线内部排,不和别的对角线混。排好后就该把它们从上往下写回去,最小的写在最靠左上的那格。
- 22把这条线第 1 小的数 1 写进格子 (1,0)。它是从上往下第 1 个位置,正好放第 1 小的数。右边面板里还剩 2,等着填进更靠右下的格子。
- 23把这条线最大的数 2 写进格子 (2,1)。它是从上往下第 2 个位置,正好放第 2 小的数。这条对角线已经全部就位。
- 24这条对角线只有一个格子 (2,0),值是 1。单个元素本身就是升序,不用排也不用动,直接定稿。
- 25六条对角线全部处理完。现在每一条从左上到右下的对角线都是升序的:主对角线是 1、2、3,它上面那条是 1、2、3,顶上和最左边那些短对角线也都各自升序。拼起来就是答案,第一行 1、1、1、1,第二行 1、2、2、2,第三行 1、2、3、3。整个过程就是分组、排序、写回这三步反复做。
⚠️ 容易写错的地方
✗ 错:把对角线判成 i+j 相等
✓ 对:右下方向的对角线是 i-j 相等;i+j 相等是右上的反对角线,方向相反
题目明确是从上或从左出发、朝右下走的对角线,只有 i-j 在这条线上保持不变;用 i+j 会排错成另一组斜线
✗ 错:i-j 当数组下标直接用,负数越界
✓ 对:用 m-i+j 或 i-j+n 之类的偏移挪成非负,或干脆用哈希表按 i-j 归桶
i 小 j 大时 i-j 是负的,负下标在数组里越界;偏移一个常数把所有对角线编号压进 0 到 m+n-1 这个区间就安全了
✗ 错:排好后写回的方向或顺序搞反
✓ 对:沿右下从最上面那格往下走,把升序结果从小到大依次填,最小放左上、最大放右下
题目要的是每条对角线升序,左上是这条线的起点、应放最小值;若从右下往左上写、或降序写回,结果就反了
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def diagonalSort(self, mat: List[List[int]]) -> List[List[int]]:
m, n = len(mat), len(mat[0])
g = [[] for _ in range(m + n)]
for i, row in enumerate(mat):
for j, x in enumerate(row):
g[m - i + j].append(x)
for e in g:
e.sort(reverse=True)
for i in range(m):
for j in range(n):
mat[i][j] = g[m - i + j].pop()
return matC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <optional>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
class Solution {
public:
vector<vector<int>> diagonalSort(vector<vector<int>>& mat) {
int m = mat.size(), n = mat[0].size();
vector<int> g[m + n];
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
g[m - i + j].push_back(mat[i][j]);
}
}
for (auto& e : g) {
sort(e.rbegin(), e.rend());
}
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
mat[i][j] = g[m - i + j].back();
g[m - i + j].pop_back();
}
}
return mat;
}
};Java
import java.util.*;
class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(){} TreeNode(int val){this.val=val;} TreeNode(int val, TreeNode left, TreeNode right){this.val=val;this.left=left;this.right=right;} }
class ListNode { int val; ListNode next; ListNode(){} ListNode(int val){this.val=val;} ListNode(int val, ListNode next){this.val=val;this.next=next;} }
class Solution {
public int[][] diagonalSort(int[][] mat) {
int m = mat.length, n = mat[0].length;
List<Integer>[] g = new List[m + n];
Arrays.setAll(g, k -> new ArrayList<>());
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
g[m - i + j].add(mat[i][j]);
}
}
for (List<Integer> e : g) {
Collections.sort(e, (a, b) -> b - a);
}
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
mat[i][j] = g[m - i + j].remove(g[m - i + j].size() - 1);
}
}
return mat;
}
}复杂度
时间
O(m·n·log(min(m,n)))
每个格子被收集和写回各一次,是 O(m·n);真正的开销在排序,每条对角线最长 min(m,n) 个数,所有对角线加起来共 m·n 个数、按对角线分段排序,总排序量是 O(m·n·log(min(m,n)))
空间
O(m·n)
按峰值算:参考代码把全部 m·n 个数都分散存进了各个桶,额外占 O(m·n)。若改成一条对角线处理完再处理下一条,辅助空间可压到一条线的长度 O(min(m,n))。排序自身的栈开销被桶占用量覆盖
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 将矩阵按对角线排序 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么同一条右下对角线上的 i-j 一定是个常数?+
从这条对角线的起点出发,每走一步都是往右下,行号加一、列号也加一。i-j 里 i 加一、j 也加一,一增一减正好抵消,所以整条线上 i-j 保持不变。正因为这样,一条对角线可以用它共同的 i-j 值唯一标识,分组时按这个值归桶就行。至于右上的反对角线,是往右上走、行减列加,变的是 i-j、不变的是 i+j,刚好相反。
i-j 会是负数,直接当数组下标不就越界了吗?+
会越界,所以要么偏移、要么换容器。当 i 小、j 大(右上角那些格子)时 i-j 是负的,负数在多数语言里当数组下标要么报错、要么绕到数组尾部,都是错的。修法是加一个常数把编号搬成非负,比如 i-j+n 或参考代码用的 m-i+j,把所有对角线编号压进 0 到 m+n-1 这段区间;也可以直接用哈希表按 i-j 归桶,键是负数也无所谓,就不必算偏移。
这题和把整个矩阵一起排序,差别到底在哪?复杂度怎么算?+
差在排序的粒度。不是把所有数混在一起排,而是把矩阵切成 m+n-1 条互不相干的对角线,各排各的。每条对角线最长 min(m,n) 个数,所有对角线的数加起来共 m·n 个,分段排序的总量是 O(m·n·log(min(m,n))),比整体排 m·n 个数的 O(m·n·log(m·n)) 略省。空间上参考代码拿桶存下全部元素是 O(m·n);如果一条线处理完再腾出来处理下一条,可以降到一条线的长度 O(min(m,n))。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 将矩阵按对角线排序 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。