3446. Sort Matrix by Diagonals
Problem Description
You are given an n x n
square matrix of integers called grid
. Your task is to sort the elements along the diagonals of this matrix according to specific rules and return the modified matrix.
The matrix needs to be sorted as follows:
-
Bottom-left triangle (including the main diagonal): All diagonals in this region should be sorted in non-increasing order (from largest to smallest as you move along each diagonal).
-
Top-right triangle: All diagonals in this region should be sorted in non-decreasing order (from smallest to largest as you move along each diagonal).
To clarify the regions:
- The bottom-left triangle includes all positions
(i, j)
wherei >= j
(row index is greater than or equal to column index) - The top-right triangle includes all positions
(i, j)
wherei < j
(row index is less than column index) - The main diagonal (where
i == j
) belongs to the bottom-left triangle
For example, in a 3x3 matrix:
- The bottom-left triangle consists of positions:
(0,0)
,(1,0)
,(1,1)
,(2,0)
,(2,1)
,(2,2)
- The top-right triangle consists of positions:
(0,1)
,(0,2)
,(1,2)
Each diagonal should be sorted independently. A diagonal is a line of elements where the difference between row and column indices remains constant. After sorting all diagonals according to their respective rules, return the resulting matrix.
Intuition
The key insight is to recognize that we need to process diagonals independently, and each diagonal can be identified by a unique pattern. In any diagonal, as we move from one element to the next, both the row index and column index increase by 1. This means elements (i, j)
, (i+1, j+1)
, (i+2, j+2)
, etc., all belong to the same diagonal.
For the bottom-left triangle, we can start from each element in the leftmost column (where j = 0
) and traverse diagonally upward and to the right. Each starting position (k, 0)
where k
ranges from n-1
down to 0
gives us a different diagonal. As we collect elements along each diagonal, we can sort them and then place them back in reverse order (using pop()
) to achieve non-increasing order.
For the top-right triangle, we can start from each element in the rightmost column (where j = n-1
) and traverse diagonally upward and to the left. Each starting position (k, n-1)
where k
ranges from n-2
down to 1
gives us a different diagonal. We collect elements, sort them, and place them back in reverse order to achieve non-decreasing order when traversing from top-right to bottom-left.
The reason we use pop()
after sorting is clever: when we sort the array in ascending order and then pop elements (which removes from the end), we get elements in descending order. This works perfectly for the bottom-left triangle where we want non-increasing order. For the top-right triangle, since we're traversing in the opposite direction (from bottom-right to top-left), popping sorted elements also gives us the desired non-decreasing order when read from top-right to bottom-left.
The solution elegantly handles the main diagonal as part of the bottom-left triangle processing, avoiding any special cases or duplicate processing.
Learn more about Sorting patterns.
Solution Approach
The implementation follows a simulation approach with sorting, processing diagonals in two phases:
Phase 1: Process Bottom-Left Triangle (including main diagonal)
We iterate through starting positions along the left column, from bottom to top using k
from n-2
down to -1
:
- For each
k
, we start at position(k, 0)
- Initialize empty list
t
to collect diagonal elements - Traverse diagonally using
i
andj
, where both increment by 1 in each step - Continue while both
i < n
andj < n
to stay within bounds - Collect all elements along this diagonal into list
t
After collecting elements:
- Sort
t
in ascending order usingt.sort()
- Reset to starting position
(k, 0)
- Traverse the same diagonal again
- Place elements back using
t.pop()
, which removes elements from the end - Since
t
is sorted ascending and we're popping from the end, we place elements in descending order along the diagonal
Phase 2: Process Top-Right Triangle
We iterate through starting positions along the right column, from second-to-last row down to first row using k
from n-2
down to 1
:
- For each
k
, we start at position(k, n-1)
- Initialize empty list
t
to collect diagonal elements - Traverse diagonally using
i
andj
, where both decrement by 1 in each step - Continue while both
i >= 0
andj >= 0
to stay within bounds - Collect all elements along this diagonal into list
t
After collecting elements:
- Sort
t
in ascending order usingt.sort()
- Reset to starting position
(k, n-1)
- Traverse the same diagonal again (from bottom-right to top-left)
- Place elements back using
t.pop()
- Since we're traversing from bottom-right to top-left and popping sorted elements, the diagonal ends up in non-decreasing order when read from top-left to bottom-right
Time Complexity: O(nΒ² log n)
- We visit each element twice (once to collect, once to place back), and sorting each diagonal takes O(m log m)
where m
is the diagonal length. In the worst case, the main diagonal has n
elements.
Space Complexity: O(n)
- The temporary list t
can hold at most n
elements (for the main diagonal).
The solution modifies the grid in-place and returns it after all diagonals have been properly sorted according to their region's requirements.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small 3Γ3 example to illustrate the solution approach.
Initial Grid:
grid = [[3, 9, 4], [7, 2, 8], [5, 6, 1]]
Phase 1: Process Bottom-Left Triangle (including main diagonal)
We start with k = 1 (n-2), then k = 0, then k = -1.
When k = 1:
- Start at position (1, 0) which has value 7
- Traverse diagonally: (1,0) β (2,1)
- Collect elements: t = [7, 6]
- Sort t: [6, 7]
- Place back using pop():
- Position (1,0) gets t.pop() = 7
- Position (2,1) gets t.pop() = 6
- Grid remains: [[3,9,4], [7,2,8], [5,6,1]]
When k = 0:
- Start at position (0, 0) which has value 3
- Traverse diagonally: (0,0) β (1,1) β (2,2)
- Collect elements: t = [3, 2, 1]
- Sort t: [1, 2, 3]
- Place back using pop():
- Position (0,0) gets t.pop() = 3
- Position (1,1) gets t.pop() = 2
- Position (2,2) gets t.pop() = 1
- Grid becomes: [[3,9,4], [7,2,8], [5,6,1]]
When k = -1:
- Start at position (-1, 0) which is out of bounds
- When adjusted: start at (0, 1) after incrementing both i and j
- Traverse diagonally: (0,1) β (1,2)
- But wait, (0,1) is in the top-right triangle, not bottom-left
- Actually, this diagonal only has position (2,0) = 5 in the bottom-left region
- Since it's a single element, sorting doesn't change it
- Grid remains: [[3,9,4], [7,2,8], [5,6,1]]
Actually, let me reconsider the traversal pattern. Looking at the code more carefully:
When k = 1:
- Start at (1, 0), traverse to (2, 1)
- Elements: [7, 6] β sorted: [6, 7] β placed back as [7, 6]
When k = 0:
- Start at (0, 0), traverse to (1, 1), then (2, 2)
- Elements: [3, 2, 1] β sorted: [1, 2, 3] β placed back as [3, 2, 1]
When k = -1:
- Start would be at (-1, 0), but after incrementing, we start at (0, 1)
- This is actually processing a different diagonal in the top section, so nothing happens here for bottom-left
After Phase 1: [[3,9,4], [7,2,8], [5,6,1]]
Phase 2: Process Top-Right Triangle
We process k = 1 (since k goes from n-2 down to 1).
When k = 1:
- Start at position (1, 2) which has value 8
- Traverse diagonally backwards: (1,2) β (0,1)
- Collect elements: t = [8, 9]
- Sort t: [8, 9]
- Place back using pop():
- Position (1,2) gets t.pop() = 9
- Position (0,1) gets t.pop() = 8
- Grid becomes: [[3,8,4], [7,2,9], [5,6,1]]
Now we need to process the remaining diagonal in top-right:
- The diagonal containing just (0,2) = 4 remains unchanged (single element)
Final Result:
grid = [[3, 8, 4], [7, 2, 9], [5, 6, 1]]
Let's verify:
- Bottom-left diagonals (read along each diagonal):
- (2,0): [5] - trivially sorted β
- (1,0)β(2,1): [7, 6] - non-increasing β
- (0,0)β(1,1)β(2,2): [3, 2, 1] - non-increasing β
- Top-right diagonals (read along each diagonal):
- (0,1)β(1,2): [8, 9] - non-decreasing β
- (0,2): [4] - trivially sorted β
The solution correctly sorts each diagonal according to its region's requirements.
Solution Implementation
1class Solution:
2 def sortMatrix(self, grid: List[List[int]]) -> List[List[int]]:
3 n = len(grid)
4
5 # Process diagonals starting from the left edge (column 0)
6 # Starting from row n-2 down to row 0
7 for start_row in range(n - 2, -1, -1):
8 row, col = start_row, 0
9 diagonal_values = []
10
11 # Collect all values along the diagonal (moving down-right)
12 while row < n and col < n:
13 diagonal_values.append(grid[row][col])
14 row += 1
15 col += 1
16
17 # Sort the diagonal values in ascending order
18 diagonal_values.sort()
19
20 # Place sorted values back along the diagonal in descending order
21 # (popping from sorted list gives largest values first)
22 row, col = start_row, 0
23 while row < n and col < n:
24 grid[row][col] = diagonal_values.pop()
25 row += 1
26 col += 1
27
28 # Process diagonals starting from the bottom edge (row n-1)
29 # Starting from column n-2 down to column 1
30 for start_col in range(n - 2, 0, -1):
31 row, col = start_col, n - 1
32 diagonal_values = []
33
34 # Collect all values along the diagonal (moving up-left)
35 while row >= 0 and col >= 0:
36 diagonal_values.append(grid[row][col])
37 row -= 1
38 col -= 1
39
40 # Sort the diagonal values in ascending order
41 diagonal_values.sort()
42
43 # Place sorted values back along the diagonal in descending order
44 # (popping from sorted list gives largest values first)
45 row, col = start_col, n - 1
46 while row >= 0 and col >= 0:
47 grid[row][col] = diagonal_values.pop()
48 row -= 1
49 col -= 1
50
51 return grid
52
1class Solution {
2 public int[][] sortMatrix(int[][] grid) {
3 int n = grid.length;
4
5 // Sort all diagonals starting from the left column (bottom to top)
6 // Each diagonal goes from (k, 0) to the bottom-right
7 for (int startRow = n - 2; startRow >= 0; startRow--) {
8 int row = startRow;
9 int col = 0;
10 List<Integer> diagonalElements = new ArrayList<>();
11
12 // Collect all elements in the current diagonal
13 while (row < n && col < n) {
14 diagonalElements.add(grid[row][col]);
15 row++;
16 col++;
17 }
18
19 // Sort the diagonal elements
20 Collections.sort(diagonalElements);
21
22 // Place sorted elements back to the diagonal
23 // Move back to the last valid position and fill backwards
24 row--;
25 col--;
26 for (int element : diagonalElements) {
27 grid[row][col] = element;
28 row--;
29 col--;
30 }
31 }
32
33 // Sort all diagonals starting from the top row (right to left)
34 // Each diagonal goes from (0, k) to the bottom-right
35 // Skip the main diagonal (k=0) as it was already sorted
36 for (int startCol = 1; startCol < n; startCol++) {
37 int row = 0;
38 int col = startCol;
39 List<Integer> diagonalElements = new ArrayList<>();
40
41 // Collect all elements in the current diagonal
42 while (row < n && col < n) {
43 diagonalElements.add(grid[row][col]);
44 row++;
45 col++;
46 }
47
48 // Sort the diagonal elements
49 Collections.sort(diagonalElements);
50
51 // Place sorted elements back to the diagonal
52 // Move back to the last valid position and fill backwards
53 row--;
54 col--;
55 for (int element : diagonalElements) {
56 grid[row][col] = element;
57 row--;
58 col--;
59 }
60 }
61
62 return grid;
63 }
64}
65
1class Solution {
2public:
3 vector<vector<int>> sortMatrix(vector<vector<int>>& grid) {
4 int n = grid.size();
5
6 // Sort diagonals starting from the left column (bottom-left to top-right diagonals)
7 // Start from second-to-last row and move upward to row 0
8 for (int startRow = n - 2; startRow >= 0; --startRow) {
9 int row = startRow;
10 int col = 0;
11 vector<int> diagonalElements;
12
13 // Collect all elements along the diagonal
14 while (row < n && col < n) {
15 diagonalElements.push_back(grid[row][col]);
16 row++;
17 col++;
18 }
19
20 // Sort the diagonal elements
21 ranges::sort(diagonalElements);
22
23 // Place sorted elements back to their diagonal positions
24 // Note: row and col are now out of bounds, so we decrement first
25 for (int element : diagonalElements) {
26 --row;
27 --col;
28 grid[row][col] = element;
29 }
30 }
31
32 // Sort diagonals starting from the top row (top-right to bottom-left diagonals)
33 // Start from column index n-2 and move leftward to column 1
34 for (int startCol = n - 2; startCol > 0; --startCol) {
35 int row = startCol;
36 int col = n - 1;
37 vector<int> diagonalElements;
38
39 // Collect all elements along the diagonal
40 while (row >= 0 && col >= 0) {
41 diagonalElements.push_back(grid[row][col]);
42 row--;
43 col--;
44 }
45
46 // Sort the diagonal elements
47 ranges::sort(diagonalElements);
48
49 // Place sorted elements back to their diagonal positions
50 // Note: row and col are now at -1, so we increment first
51 for (int element : diagonalElements) {
52 ++row;
53 ++col;
54 grid[row][col] = element;
55 }
56 }
57
58 return grid;
59 }
60};
61
1/**
2 * Sorts the diagonals of a square matrix in ascending order
3 * @param grid - The n x n matrix to be sorted diagonally
4 * @returns The matrix with sorted diagonals
5 */
6function sortMatrix(grid: number[][]): number[][] {
7 const matrixSize: number = grid.length;
8
9 // Process diagonals starting from the left column (bottom-left to top-right direction)
10 // Start from second-to-last row and move upward
11 for (let startRow: number = matrixSize - 2; startRow >= 0; startRow--) {
12 let currentRow: number = startRow;
13 let currentCol: number = 0;
14 const diagonalElements: number[] = [];
15
16 // Collect all elements along the current diagonal
17 while (currentRow < matrixSize && currentCol < matrixSize) {
18 diagonalElements.push(grid[currentRow][currentCol]);
19 currentRow++;
20 currentCol++;
21 }
22
23 // Sort the diagonal elements in ascending order
24 diagonalElements.sort((a: number, b: number) => a - b);
25
26 // Place sorted elements back into the diagonal
27 // Move back to the starting position by decrementing indices
28 for (const element of diagonalElements) {
29 currentRow--;
30 currentCol--;
31 grid[currentRow][currentCol] = element;
32 }
33 }
34
35 // Process diagonals starting from the top row (top-right to bottom-left direction)
36 // Start from second-to-last column and move leftward
37 for (let startCol: number = matrixSize - 2; startCol > 0; startCol--) {
38 let currentRow: number = startCol;
39 let currentCol: number = matrixSize - 1;
40 const diagonalElements: number[] = [];
41
42 // Collect all elements along the current diagonal
43 while (currentRow >= 0 && currentCol >= 0) {
44 diagonalElements.push(grid[currentRow][currentCol]);
45 currentRow--;
46 currentCol--;
47 }
48
49 // Sort the diagonal elements in ascending order
50 diagonalElements.sort((a: number, b: number) => a - b);
51
52 // Place sorted elements back into the diagonal
53 // Move forward to the starting position by incrementing indices
54 for (const element of diagonalElements) {
55 currentRow++;
56 currentCol++;
57 grid[currentRow][currentCol] = element;
58 }
59 }
60
61 return grid;
62}
63
Time and Space Complexity
The time complexity is O(n^2 log n)
, and the space complexity is O(n)
.
Time Complexity Analysis:
- The code processes diagonals of the matrix in two separate loops
- First loop: processes diagonals starting from the leftmost column (from row
n-2
to row0
) - Second loop: processes anti-diagonals starting from the rightmost column (from row
n-2
to row1
) - For each diagonal/anti-diagonal:
- Collecting elements takes
O(d)
time whered
is the diagonal length - Sorting takes
O(d log d)
time - Placing sorted elements back takes
O(d)
time
- Collecting elements takes
- The longest diagonal has length
n
, and there are approximately2n
diagonals total - The sum of all diagonal lengths is approximately
n^2
- The dominant operation is sorting, giving us:
Ξ£(d log d)
for all diagonals - Since the maximum diagonal length is
n
and we processO(n)
diagonals with average lengthO(n)
, the total complexity isO(n^2 log n)
Space Complexity Analysis:
- The temporary array
t
stores elements from one diagonal at a time - The maximum diagonal length is
n
(the main diagonal) - Therefore, the space complexity is
O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Diagonal Coverage
Pitfall: The current implementation doesn't correctly cover all diagonals in the matrix. It processes diagonals starting from the left edge (column 0) and bottom edge (row n-1), but this misses several important diagonals and incorrectly processes others.
Issues:
- The first loop starts from row
n-2
and column0
, missing the bottom-left corner diagonal starting at(n-1, 0)
- The second loop processes diagonals from the bottom edge instead of the right edge, which doesn't align with the top-right triangle requirement
- Some diagonals in the top-right triangle are never processed
Solution: Process diagonals systematically by their starting positions:
- For bottom-left triangle: Start from the left edge (column 0) and then the bottom edge
- For top-right triangle: Start from the top edge (row 0) excluding the top-left corner
2. Incorrect Sorting Direction Application
Pitfall: The code uses pop()
to place elements in descending order for both regions, but the top-right triangle should be in ascending order.
Issues:
- The second phase incorrectly places elements in descending order when it should place them in ascending order
- The traversal direction and popping strategy don't align with the required sorting order
Solution: Use different placement strategies for each region:
- Bottom-left: Use
pop()
to place in descending order - Top-right: Use index-based placement or
pop(0)
to place in ascending order
Corrected Implementation:
class Solution:
def sortMatrix(self, grid: List[List[int]]) -> List[List[int]]:
n = len(grid)
# Process bottom-left triangle (including main diagonal)
# Start from left edge (column 0)
for start_row in range(n):
row, col = start_row, 0
diagonal_values = []
# Collect diagonal elements
while row < n and col < n and row >= col: # Stay in bottom-left triangle
diagonal_values.append(grid[row][col])
row += 1
col += 1
# Sort and place back in descending order
diagonal_values.sort()
row, col = start_row, 0
while row < n and col < n and row >= col:
grid[row][col] = diagonal_values.pop() # Pop gives largest first
row += 1
col += 1
# Process bottom-left triangle diagonals starting from bottom edge
for start_col in range(1, n):
row, col = n - 1, start_col
diagonal_values = []
# Collect diagonal elements
while row >= 0 and col < n and row >= col:
diagonal_values.append(grid[row][col])
row -= 1
col += 1
# Sort and place back in descending order
diagonal_values.sort()
row, col = n - 1, start_col
while row >= 0 and col < n and row >= col:
grid[row][col] = diagonal_values.pop()
row -= 1
col += 1
# Process top-right triangle
# Start from top edge (row 0), excluding (0,0)
for start_col in range(1, n):
row, col = 0, start_col
diagonal_values = []
# Collect diagonal elements
while row < n and col < n:
diagonal_values.append(grid[row][col])
row += 1
col += 1
# Sort and place back in ascending order
diagonal_values.sort()
row, col = 0, start_col
idx = 0
while row < n and col < n:
grid[row][col] = diagonal_values[idx] # Place in ascending order
row += 1
col += 1
idx += 1
# Process top-right triangle diagonals starting from right edge
for start_row in range(1, n - 1):
row, col = start_row, n - 1
diagonal_values = []
# Collect diagonal elements
while row < n and col >= 0 and row < col:
diagonal_values.append(grid[row][col])
row += 1
col -= 1
# Sort and place back in ascending order
diagonal_values.sort()
row, col = start_row, n - 1
idx = 0
while row < n and col >= 0 and row < col:
grid[row][col] = diagonal_values[idx]
row += 1
col -= 1
idx += 1
return grid
What data structure does Breadth-first search typically uses to store intermediate states?
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!