1329. Sort the Matrix Diagonally
Problem Description
You are given an m x n
matrix of integers and need to sort each diagonal of the matrix in ascending order.
A matrix diagonal is a line of cells that starts from either:
- Any cell in the topmost row (row 0), OR
- Any cell in the leftmost column (column 0)
And proceeds diagonally down-right until it reaches the edge of the matrix.
For example, in a 6 x 3
matrix, the diagonal starting from mat[2][0]
would include the cells mat[2][0]
, mat[3][1]
, and mat[4][2]
.
Your task is to:
- Identify all diagonals in the matrix
- Sort each diagonal independently in ascending order
- Return the modified matrix with all diagonals sorted
The key insight is that elements on the same diagonal share a common property: for any two elements (iβ, jβ)
and (iβ, jβ)
on the same diagonal, the difference j - i
is constant. This property helps identify which elements belong to the same diagonal.
The solution uses this property by grouping elements with the same m - i + j
value (where m
is added as an offset to ensure positive indices), sorting each group, and then placing the sorted elements back into their diagonal positions in the matrix.
Intuition
The first observation is that we need to identify which elements belong to the same diagonal. If we look at any diagonal, we notice that as we move down-right (incrementing row by 1 and column by 1), the difference between column index and row index remains constant.
For example, consider elements on a diagonal:
mat[0][2]
: difference is2 - 0 = 2
mat[1][3]
: difference is3 - 1 = 2
mat[2][4]
: difference is4 - 2 = 2
This pattern holds for every diagonal! Each diagonal can be uniquely identified by its j - i
value.
Now, instead of sorting each diagonal in-place (which would be complex to implement), we can:
- Extract all elements from each diagonal into separate arrays
- Sort these arrays
- Put the sorted elements back
To group elements by diagonal, we use the formula m - i + j
where m
is the number of rows. The addition of m
ensures all indices are positive (since j - i
can be negative when i > j
).
The clever part of the solution is using a list of lists g
where g[k]
contains all elements from the diagonal with identifier k
. After collecting all elements:
- We sort each diagonal's elements
- We iterate through the matrix again and refill each position with the sorted values
- Using
pop()
ensures we place elements in the correct order as we traverse the matrix
This approach transforms a complex 2D sorting problem into multiple simple 1D sorting problems, making the solution both elegant and efficient.
Learn more about Sorting patterns.
Solution Approach
The implementation follows these steps:
-
Initialize data structures: Create a list
g
withm + n
empty lists. Each list will store elements from one diagonal. The sizem + n
is sufficient because there are at mostm + n - 1
diagonals in anm x n
matrix. -
Group elements by diagonal: Iterate through the matrix and add each element to its corresponding diagonal group:
for i, row in enumerate(mat): for j, x in enumerate(row): g[m - i + j].append(x)
The index
m - i + j
uniquely identifies each diagonal. Elements with the samem - i + j
value belong to the same diagonal. -
Sort each diagonal: Sort each list in
g
in reverse order (descending):for e in g: e.sort(reverse=True)
We sort in reverse because we'll use
pop()
later, which removes from the end of the list. This ensures we get elements in ascending order when popping. -
Refill the matrix: Traverse the matrix again in the same order and replace each element with the sorted values:
for i in range(m): for j in range(n): mat[i][j] = g[m - i + j].pop()
Since we traverse in the same order as we collected elements, and we're popping from lists sorted in reverse, each diagonal gets filled with its elements in ascending order.
The time complexity is O(m * n * log(min(m, n)))
because each element is part of a diagonal of maximum length min(m, n)
, and sorting each diagonal takes O(k * log(k))
where k
is the diagonal length. The space complexity is O(m * n)
for storing all elements in the auxiliary lists.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with a 3 x 4
matrix:
Initial matrix: [3, 3, 1, 1] [2, 2, 1, 2] [1, 1, 1, 2]
Step 1: Identify the diagonals
Each diagonal is identified by m - i + j
(where m = 3):
- Position [0][0]: 3 - 0 + 0 = 3, value = 3
- Position [0][1]: 3 - 0 + 1 = 4, value = 3
- Position [0][2]: 3 - 0 + 2 = 5, value = 1
- Position [0][3]: 3 - 0 + 3 = 6, value = 1
- Position [1][0]: 3 - 1 + 0 = 2, value = 2
- Position [1][1]: 3 - 1 + 1 = 3, value = 2
- Position [1][2]: 3 - 1 + 2 = 4, value = 1
- Position [1][3]: 3 - 1 + 3 = 5, value = 2
- Position [2][0]: 3 - 2 + 0 = 1, value = 1
- Position [2][1]: 3 - 2 + 1 = 2, value = 1
- Position [2][2]: 3 - 2 + 2 = 3, value = 1
- Position [2][3]: 3 - 2 + 3 = 4, value = 2
Step 2: Group elements by diagonal
After grouping, our g
array looks like:
- g[1]: [1] (diagonal starting at [2][0])
- g[2]: [2, 1] (diagonal starting at [1][0])
- g[3]: [3, 2, 1] (diagonal starting at [0][0])
- g[4]: [3, 1, 2] (diagonal starting at [0][1])
- g[5]: [1, 2] (diagonal starting at [0][2])
- g[6]: [1] (diagonal starting at [0][3])
Step 3: Sort each diagonal in reverse
After sorting in reverse order:
- g[1]: [1]
- g[2]: [2, 1]
- g[3]: [3, 2, 1]
- g[4]: [3, 2, 1]
- g[5]: [2, 1]
- g[6]: [1]
Step 4: Refill the matrix
Traverse the matrix in the same order and pop from each diagonal:
- [0][0]: pop from g[3] β 1 (g[3] becomes [3, 2])
- [0][1]: pop from g[4] β 1 (g[4] becomes [3, 2])
- [0][2]: pop from g[5] β 1 (g[5] becomes [2])
- [0][3]: pop from g[6] β 1 (g[6] becomes [])
- [1][0]: pop from g[2] β 1 (g[2] becomes [2])
- [1][1]: pop from g[3] β 2 (g[3] becomes [3])
- [1][2]: pop from g[4] β 2 (g[4] becomes [3])
- [1][3]: pop from g[5] β 2 (g[5] becomes [])
- [2][0]: pop from g[1] β 1 (g[1] becomes [])
- [2][1]: pop from g[2] β 2 (g[2] becomes [])
- [2][2]: pop from g[3] β 3 (g[3] becomes [])
- [2][3]: pop from g[4] β 3 (g[4] becomes [])
Final sorted matrix:
[1, 1, 1, 1] [1, 2, 2, 2] [1, 2, 3, 3]
Each diagonal is now sorted in ascending order!
Solution Implementation
1class Solution:
2 def diagonalSort(self, mat: List[List[int]]) -> List[List[int]]:
3 # Get matrix dimensions
4 rows, cols = len(mat), len(mat[0])
5
6 # Create buckets to store elements from each diagonal
7 # Each diagonal is identified by (row - col) value, but we offset by rows to avoid negative indices
8 # Total possible diagonals: rows + cols - 1
9 diagonal_buckets = [[] for _ in range(rows + cols)]
10
11 # Collect all elements and group them by their diagonal
12 # Elements on the same diagonal have the same (rows - row_idx + col_idx) value
13 for row_idx, row in enumerate(mat):
14 for col_idx, value in enumerate(row):
15 diagonal_id = rows - row_idx + col_idx
16 diagonal_buckets[diagonal_id].append(value)
17
18 # Sort each diagonal in descending order for efficient pop() operation
19 # pop() removes from the end, so we'll get smallest elements first
20 for diagonal in diagonal_buckets:
21 diagonal.sort(reverse=True)
22
23 # Place sorted elements back into the matrix
24 # Pop from each diagonal bucket to get elements in ascending order
25 for row_idx in range(rows):
26 for col_idx in range(cols):
27 diagonal_id = rows - row_idx + col_idx
28 mat[row_idx][col_idx] = diagonal_buckets[diagonal_id].pop()
29
30 return mat
31
1class Solution {
2 public int[][] diagonalSort(int[][] mat) {
3 int rows = mat.length;
4 int cols = mat[0].length;
5
6 // Create an array of lists to store elements from each diagonal
7 // Total number of diagonals = rows + cols - 1, but we allocate rows + cols for simplicity
8 // Each diagonal is identified by the expression: rows - rowIndex + colIndex
9 List<Integer>[] diagonals = new List[rows + cols];
10
11 // Initialize each list in the array
12 Arrays.setAll(diagonals, index -> new ArrayList<>());
13
14 // Collect all elements from the matrix and group them by their diagonal
15 // Elements on the same diagonal have the same value of (rows - i + j)
16 for (int i = 0; i < rows; i++) {
17 for (int j = 0; j < cols; j++) {
18 int diagonalIndex = rows - i + j;
19 diagonals[diagonalIndex].add(mat[i][j]);
20 }
21 }
22
23 // Sort each diagonal in descending order
24 // We use descending order because we'll remove elements from the end later (removeLast)
25 for (List<Integer> diagonal : diagonals) {
26 Collections.sort(diagonal, (a, b) -> b - a);
27 }
28
29 // Place the sorted elements back into the matrix
30 // removeLast() gets the smallest element (since we sorted in descending order)
31 // This ensures elements are placed back in ascending order along each diagonal
32 for (int i = 0; i < rows; i++) {
33 for (int j = 0; j < cols; j++) {
34 int diagonalIndex = rows - i + j;
35 mat[i][j] = diagonals[diagonalIndex].removeLast();
36 }
37 }
38
39 return mat;
40 }
41}
42
1class Solution {
2public:
3 vector<vector<int>> diagonalSort(vector<vector<int>>& mat) {
4 int rows = mat.size();
5 int cols = mat[0].size();
6
7 // Create buckets for each diagonal
8 // Each diagonal is identified by (row - col) value, but we offset it
9 // by rows to ensure non-negative indices
10 vector<vector<int>> diagonals(rows + cols);
11
12 // Collect all elements from each diagonal
13 // Elements on the same diagonal have the same (rows - i + j) value
14 for (int i = 0; i < rows; ++i) {
15 for (int j = 0; j < cols; ++j) {
16 int diagonalIndex = rows - i + j - 1; // Adjusted to 0-based indexing
17 diagonals[diagonalIndex].push_back(mat[i][j]);
18 }
19 }
20
21 // Sort each diagonal in descending order
22 // We use descending order because we'll pop from the back later
23 for (auto& diagonal : diagonals) {
24 sort(diagonal.rbegin(), diagonal.rend());
25 }
26
27 // Place sorted elements back into the matrix
28 // Pop from the back to get elements in ascending order
29 for (int i = 0; i < rows; ++i) {
30 for (int j = 0; j < cols; ++j) {
31 int diagonalIndex = rows - i + j - 1;
32 mat[i][j] = diagonals[diagonalIndex].back();
33 diagonals[diagonalIndex].pop_back();
34 }
35 }
36
37 return mat;
38 }
39};
40
1/**
2 * Sorts each diagonal of a matrix in ascending order
3 * @param mat - The input matrix to be diagonal sorted
4 * @returns The matrix with sorted diagonals
5 */
6function diagonalSort(mat: number[][]): number[][] {
7 // Get matrix dimensions
8 const rows: number = mat.length;
9 const cols: number = mat[0].length;
10
11 // Create array to store all diagonals
12 // Each diagonal is identified by (row - col + cols) to ensure positive indices
13 const diagonals: number[][] = Array.from({ length: rows + cols }, () => []);
14
15 // Collect all elements by their diagonal index
16 // Elements on the same diagonal have the same (rows - i + j) value
17 for (let row = 0; row < rows; row++) {
18 for (let col = 0; col < cols; col++) {
19 const diagonalIndex = rows - row + col;
20 diagonals[diagonalIndex].push(mat[row][col]);
21 }
22 }
23
24 // Sort each diagonal in descending order
25 // We sort descending because we'll use pop() later which removes from the end
26 for (const diagonal of diagonals) {
27 diagonal.sort((a, b) => b - a);
28 }
29
30 // Place sorted elements back into the matrix
31 // pop() retrieves the smallest element (since array is sorted descending)
32 for (let row = 0; row < rows; row++) {
33 for (let col = 0; col < cols; col++) {
34 const diagonalIndex = rows - row + col;
35 mat[row][col] = diagonals[diagonalIndex].pop()!;
36 }
37 }
38
39 return mat;
40}
41
Time and Space Complexity
Time Complexity: O(m Γ n Γ log(min(m, n)))
The algorithm works by grouping elements along each diagonal, sorting them, and then placing them back. Breaking down the time complexity:
-
First nested loop: Iterating through all
m Γ n
elements to group them by diagonal takesO(m Γ n)
time. -
Sorting phase: There are
m + n - 1
diagonals in total. Each diagonal contains at mostmin(m, n)
elements. Sorting each diagonal takesO(k Γ log k)
wherek
is the diagonal length. The total sorting time across all diagonals is dominated byO(m Γ n Γ log(min(m, n)))
since the sum of all diagonal lengths equalsm Γ n
. -
Second nested loop: Redistributing the sorted elements back to the matrix takes
O(m Γ n)
time.
The overall time complexity is O(m Γ n) + O(m Γ n Γ log(min(m, n))) + O(m Γ n) = O(m Γ n Γ log(min(m, n)))
.
Space Complexity: O(m Γ n)
The algorithm uses a list g
containing m + n
sublists that collectively store all m Γ n
elements from the matrix. This requires O(m Γ n)
additional space to store all the matrix elements in the diagonal groups.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Diagonal Indexing Formula
A critical pitfall is using the wrong formula to identify diagonals. Many developers intuitively try i + j
or i - j
as the diagonal identifier, but these don't correctly group all diagonals.
Why it fails:
- Using
i - j
alone produces negative indices for diagonals below the main diagonal - Using
i + j
groups anti-diagonals (top-right to bottom-left) instead of the required top-left to bottom-right diagonals
Correct approach:
The formula m - i + j
(or alternatively i - j + offset
) ensures:
- All indices are positive
- Elements on the same diagonal share the same identifier
- The offset prevents array index out of bounds errors
2. Forgetting to Handle the Pop Order
When using pop()
to retrieve elements, forgetting to sort in reverse order is a common mistake:
# WRONG: Sorting in ascending order for diagonal in diagonal_buckets: diagonal.sort() # This will cause elements to be placed in descending order! # CORRECT: Sorting in descending order for pop() for diagonal in diagonal_buckets: diagonal.sort(reverse=True) # Elements will be popped in ascending order
Alternative solution without reverse sorting:
# Use deque for efficient popleft()
from collections import deque
# After sorting normally:
for diagonal in diagonal_buckets:
diagonal.sort()
# Convert to deque
diagonal_buckets = [deque(d) for d in diagonal_buckets]
# Pop from the left instead
for row_idx in range(rows):
for col_idx in range(cols):
diagonal_id = rows - row_idx + col_idx
mat[row_idx][col_idx] = diagonal_buckets[diagonal_id].popleft()
3. Incorrect Bucket Array Size
Allocating too few buckets causes index out of bounds errors:
# WRONG: Not enough buckets
diagonal_buckets = [[] for _ in range(rows)] # Missing diagonals!
# CORRECT: Maximum possible diagonals is rows + cols - 1
diagonal_buckets = [[] for _ in range(rows + cols)] # Safe allocation
The actual number of diagonals is rows + cols - 1
, but allocating rows + cols
provides a safety margin without significant overhead.
In a binary min heap, the minimum element can be found in:
Recommended Readings
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!