LeetCode 1424 - Diagonal Traverse II
The problem asks us to traverse a jagged 2D integer array nums diagonally. Specifically, elements are accessed by their
Difficulty: 🟡 Medium
Topics: Array, Sorting, Heap (Priority Queue)
Solution
Problem Understanding
The problem asks us to traverse a jagged 2D integer array nums diagonally. Specifically, elements are accessed by their "diagonal level," where each diagonal contains elements with the same sum of row and column indices. Within each diagonal, elements should appear in reverse order of their rows, meaning we start from the bottom-most row of the diagonal and move up.
The input nums is a list of lists of integers. The lists are jagged, meaning each sublist may have a different length. The output should be a single list of integers, representing the diagonal traversal of all elements according to the above rules.
Constraints are significant: the total number of elements across all sublists can be up to 10^5. This rules out any approach that would involve fully flattening or sorting in quadratic time. Each element can be large but the range is not critical for traversal logic.
Edge cases include empty sublists (though nums[i].length >= 1 guarantees at least one element per row), highly jagged arrays (rows of widely differing lengths), and minimal input (nums containing a single row or a single column).
Approaches
The brute-force approach would be to iterate over all possible diagonal levels (sum of indices), collect elements belonging to each diagonal into temporary arrays, reverse each diagonal, and then append them to the result. This works because diagonals are naturally grouped by row + column, but it is cumbersome and may involve many temporary arrays.
The optimal approach uses a hash map (dictionary) where the key is the diagonal level d = row + column. As we iterate over each element, we append it to the list corresponding to its diagonal. After processing all elements, we concatenate the diagonals in order of increasing diagonal level, reversing each diagonal because elements are collected top-to-bottom but must appear bottom-to-top within the same diagonal.
This method works efficiently because it collects diagonals in a single pass and only iterates through all elements once more to produce the final output. It handles jagged arrays naturally, and there is no need for nested iteration over diagonals of varying lengths.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(N + D^2) | O(N + D) | Collect elements by diagonal and reverse each; can be inefficient for jagged arrays with many diagonals |
| Optimal | O(N) | O(N) | Hash map collects diagonals efficiently; single pass over all elements and then over diagonals to form result |
Here, N is the total number of elements (sum(len(row) for row in nums)) and D is the maximum diagonal index.
Algorithm Walkthrough
- Initialize an empty dictionary
diagonalsto map diagonal indexd = row + colto a list of elements. - Initialize a variable
max_diagto track the largest diagonal index seen. - Iterate over each row
rand each columncwithin that row. - Compute the diagonal index
d = r + c. - Append
nums[r][c]todiagonals[d]. - Update
max_diagifdis larger than the current maximum. - Initialize an empty list
resultto store the final diagonal traversal. - Iterate from
d = 0tomax_diag. - For each diagonal list in
diagonals[d], append its elements in reversed order toresult. - Return
result.
Why it works: Each element is assigned to exactly one diagonal key based on row + col, which ensures proper grouping. Reversing each diagonal ensures the bottom-up order required by the problem. Iterating diagonals in increasing order guarantees the correct traversal sequence.
Python Solution
from typing import List
from collections import defaultdict
class Solution:
def findDiagonalOrder(self, nums: List[List[int]]) -> List[int]:
diagonals = defaultdict(list)
max_diag = 0
for r, row in enumerate(nums):
for c, val in enumerate(row):
diagonals[r + c].append(val)
max_diag = max(max_diag, r + c)
result = []
for d in range(max_diag + 1):
result.extend(reversed(diagonals[d]))
return result
This implementation first populates a hash map with elements grouped by their diagonal index. Each diagonal list is then reversed and appended to the final result. Using defaultdict simplifies the logic by avoiding explicit checks for missing keys.
Go Solution
func findDiagonalOrder(nums [][]int) []int {
diagonals := make(map[int][]int)
maxDiag := 0
for r, row := range nums {
for c, val := range row {
diagonals[r+c] = append(diagonals[r+c], val)
if r+c > maxDiag {
maxDiag = r + c
}
}
}
result := []int{}
for d := 0; d <= maxDiag; d++ {
diag := diagonals[d]
for i := len(diag) - 1; i >= 0; i-- {
result = append(result, diag[i])
}
}
return result
}
In Go, a map stores diagonal lists, and elements are appended similarly. Reversing each diagonal during iteration produces the required bottom-to-top order. Nil slices are handled naturally since appending to a nil slice creates a new slice.
Worked Examples
Example 1: [[1,2,3],[4,5,6],[7,8,9]]
| r | c | val | d=r+c | diagonals |
|---|---|---|---|---|
| 0 | 0 | 1 | 0 | {0: [1]} |
| 0 | 1 | 2 | 1 | {0:[1],1:[2]} |
| 0 | 2 | 3 | 2 | {0:[1],1:[2],2:[3]} |
| 1 | 0 | 4 | 1 | {0:[1],1:[2,4],2:[3]} |
| 1 | 1 | 5 | 2 | {0:[1],1:[2,4],2:[3,5]} |
| 1 | 2 | 6 | 3 | {0:[1],1:[2,4],2:[3,5],3:[6]} |
| 2 | 0 | 7 | 2 | {0:[1],1:[2,4],2:[3,5,7],3:[6]} |
| 2 | 1 | 8 | 3 | {0:[1],1:[2,4],2:[3,5,7],3:[6,8]} |
| 2 | 2 | 9 | 4 | {0:[1],1:[2,4],2:[3,5,7],3:[6,8],4:[9]} |
Reversing each diagonal and concatenating: [1,4,2,7,5,3,8,6,9].
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(N) | Each element is processed once to populate diagonals and once when building the result |
| Space | O(N) | Diagonal hash map stores all elements; final result also stores all elements |
The algorithm scales linearly with the total number of elements, making it efficient even for large jagged arrays.
Test Cases
# Provided examples
assert Solution().findDiagonalOrder([[1,2,3],[4,5,6],[7,8,9]]) == [1,4,2,7,5,3,8,6,9] # square matrix
assert Solution().findDiagonalOrder([[1,2,3,4,5],[6,7],[8],[9,10,11],[12,13,14,15,16]]) == [1,6,2,8,7,3,9,4,12,10,5,13,11,14,15,16] # jagged
# Edge cases
assert Solution().findDiagonalOrder([[1]]) == [1] # single element
assert Solution().findDiagonalOrder([[1],[2],[3]]) == [1,2,3] # single column
assert Solution().findDiagonalOrder([[1,2,3]]) == [1,2,3] # single row
assert Solution().findDiagonalOrder([[1,2],[3]]) == [1,3,2] # small jagged
| Test | Why |
|---|---|
[[1,2,3],[4,5,6],[7,8,9]] |
Square matrix traversal correctness |
[[1,2,3,4,5],[6,7],[8],[9,10,11],[12,13,14,15,16]] |
Handles jagged rows and larger input |
[[1]] |
Minimal input |
[[1],[2],[3]] |
Single column |
[[1,2,3]] |
Single row |
[[1,2],[3]] |
Small jagged array, tests diagonal ordering |
Edge Cases
One important edge case is a single-row or single-column array. The algorithm handles this by naturally computing the diagonal indices; no reversal causes issues with single elements.
Another edge case is a highly jagged array,