Matrix Traversal Patterns Explained — Row, Column, Diagonal & Spiral
- Row-major is your performance champion due to memory layout in Java. Column-major is 5-10x slower on large matrices due to cache thrashing.
- Diagonal traversals are easily solved by anchoring on the 'row + col' invariant. Number of diagonals = rows + cols - 1.
- Spiral traversal requires 4 boundaries and 2 specific guard checks (top <= bottom, left <= right) for non-square grids.
- Row-major: left-to-right, top-to-bottom. Cache-friendly. Default for most operations.
- Column-major: top-to-bottom, left-to-right. 5-10x slower on large matrices due to cache misses.
- Spiral: boundary shrinking (top, bottom, left, right). Used for layer-by-layer processing.
- Diagonal: group by row+col invariant. Used for JPEG zigzag, DP table fill.
- Image processing pipelines use row-major for filters, column-major for rotation.
- Spiral traversal is used in matrix rotation and layer-based compression.
- Missing guard checks (top <= bottom, left <= right) in spiral traversal. Causes duplicate visits on non-square matrices.
Spiral traversal produces duplicates on non-square matrices.
Test with 1x3 matrix [[1,2,3]] — expected [1,2,3], if you get [1,2,3,2,1] the guards are missingTest with 3x1 matrix [[1],[2],[3]] — expected [1,2,3]ArrayIndexOutOfBoundsException on empty or null matrix.
Print: System.out.println("rows=" + (matrix == null ? "null" : matrix.length))Print: System.out.println("cols=" + (matrix.length == 0 ? "empty" : matrix[0].length))Column-major traversal is 10x slower than expected on large matrices.
Profile with perf stat — look for L1 cache miss rate > 50%Swap outer/inner loops to row-major and re-benchmarkDiagonal traversal misses elements on rectangular matrices.
Test with 2x4 matrix — verify all 8 elements appear in outputPrint d, startRow, endRow for each diagonal to trace the boundsProduction Incident
Production Debug GuideSymptom-first investigation path for matrix traversal bugs.
Matrices are the fundamental data structure for image processing, game boards, adjacency tables, pathfinding, and spreadsheet operations. Every image filter, matrix rotation, and grid-based search relies on a specific traversal pattern. Choosing the wrong pattern does not just slow your code — it produces wrong results.
The core problem is direction and order. A flat array has one traversal: front to back. A matrix has dozens: row-major, column-major, diagonal, anti-diagonal, spiral inward, spiral outward, and BFS/DFS flood fill. Row-major traversal is 5-10x faster than column-major on large matrices due to CPU cache locality. Spiral traversal requires careful boundary management to avoid duplicate visits on non-square grids.
The common misconception is that all traversal patterns have the same performance. Row-major reads contiguous memory — cache-friendly. Column-major jumps across rows — cache-hostile. On a 4096x4096 image, the difference is 200ms vs 2 seconds. Understanding memory layout is what separates a correct solution from a production-grade one.
Worked Example — Spiral Order Traversal of a 3x3 Matrix
Matrix M = [[1,2,3],[4,5,6],[7,8,9]]. Spiral order traversal:
- Initialize: top=0, bottom=2, left=0, right=2, result=[].
- Traverse top row left-to-right: [1,2,3]. top becomes 1.
- Traverse right column top-to-bottom: [6,9]. right becomes 1.
- Traverse bottom row right-to-left: [8,7]. bottom becomes 1.
- Traverse left column bottom-to-top: [4]. left becomes 1.
- top>bottom (1>1 is false) but top==bottom: traverse remaining top row [5]. top becomes 2.
- top>bottom → stop. Result: [1,2,3,6,9,8,7,4,5].
DFS on grid for number of islands (1=land, 0=water) in [[1,1,0],[0,1,0],[0,0,1]]: 1. (0,0)=1: DFS marks (0,0),(0,1),(1,1) as visited. Count=1. 2. (0,2)=0: skip. 3. Continue scanning... (2,2)=1: DFS marks (2,2). Count=2. 4. Total islands = 2.
package io.thecodeforge.traversal; import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class WorkedExamples { /** Spiral order traversal. O(m*n) time, O(1) space (excluding result list). */ public static List<Integer> spiralOrder(int[][] matrix) { List<Integer> result = new ArrayList<>(); if (matrix == null || matrix.length == 0) return result; int top = 0, bottom = matrix.length - 1; int left = 0, right = matrix[0].length - 1; while (top <= bottom && left <= right) { for (int i = left; i <= right; i++) result.add(matrix[top][i]); top++; for (int i = top; i <= bottom; i++) result.add(matrix[i][right]); right--; if (top <= bottom) { for (int i = right; i >= left; i--) result.add(matrix[bottom][i]); bottom--; } if (left <= right) { for (int i = bottom; i >= top; i--) result.add(matrix[i][left]); left++; } } return result; } /** Number of islands using DFS. O(m*n) time, O(m*n) space for visited array. */ public static int numIslands(int[][] grid) { if (grid == null || grid.length == 0) return 0; int count = 0; for (int r = 0; r < grid.length; r++) { for (int c = 0; c < grid[0].length; c++) { if (grid[r][c] == 1) { dfs(grid, r, c); count++; } } } return count; } private static void dfs(int[][] grid, int r, int c) { if (r < 0 || r >= grid.length || c < 0 || c >= grid[0].length || grid[r][c] == 0) return; grid[r][c] = 0; // mark visited dfs(grid, r + 1, c); dfs(grid, r - 1, c); dfs(grid, r, c + 1); dfs(grid, r, c - 1); } public static void main(String[] args) { int[][] matrix = {{1,2,3},{4,5,6},{7,8,9}}; System.out.println("Spiral: " + spiralOrder(matrix)); int[][] islands = {{1,1,0},{0,1,0},{0,0,1}}; System.out.println("Islands: " + numIslands(islands)); } }
Islands: 2
- Sequential scan: row-major or column-major. Default for most operations.
- Boundary shrinking: spiral. Four boundaries converge inward.
- Invariant grouping: diagonal. Cells grouped by row+col = constant.
- Connected component: DFS/BFS. Flood fill, number of islands.
- Most interview problems combine two building blocks.
Matrix Traversal Patterns — Plain English
Matrix traversal problems require visiting cells in a specific order. Three main patterns:
Pattern 1 — Spiral traversal: 1. Maintain boundaries: top, bottom, left, right. 2. Traverse right along top row; shrink top. 3. Traverse down right column; shrink right. 4. Traverse left along bottom row; shrink bottom. 5. Traverse up left column; shrink left. 6. Repeat while top<=bottom and left<=right.
Worked example — spiral of [[1,2,3],[4,5,6],[7,8,9]]: Right: 1,2,3. top=1. Down: 6,9. right=1. Left: 8,7. bottom=1. Up: 4. left=1. Right: 5. top(2)>bottom(1). Stop. Result: [1,2,3,6,9,8,7,4,5].
Pattern 2 — DFS/BFS flood fill (number of islands): For each cell matching condition: run DFS marking all connected cells visited. Count DFS starts = number of islands.
Pattern 3 — Diagonal traversal: Cells on the same anti-diagonal share i+j. Group by i+j to traverse diagonals.
package io.thecodeforge.traversal; import java.util.ArrayList; import java.util.List; public class TraversalPatterns { /** Pattern 1: Spiral traversal. */ public static List<Integer> spiral(int[][] m) { List<Integer> res = new ArrayList<>(); if (m == null || m.length == 0) return res; int top = 0, bottom = m.length - 1, left = 0, right = m[0].length - 1; while (top <= bottom && left <= right) { for (int i = left; i <= right; i++) res.add(m[top][i]); top++; for (int i = top; i <= bottom; i++) res.add(m[i][right]); right--; if (top <= bottom) { for (int i = right; i >= left; i--) res.add(m[bottom][i]); bottom--; } if (left <= right) { for (int i = bottom; i >= top; i--) res.add(m[i][left]); left++; } } return res; } /** Pattern 3: Diagonal traversal grouped by row+col. */ public static List<Integer> diagonalOrder(int[][] m) { List<Integer> res = new ArrayList<>(); if (m == null || m.length == 0) return res; int rows = m.length, cols = m[0].length; for (int d = 0; d < rows + cols - 1; d++) { int r = Math.max(0, d - cols + 1); int rEnd = Math.min(d, rows - 1); for (int i = r; i <= rEnd; i++) { res.add(m[i][d - i]); } } return res; } public static void main(String[] args) { int[][] m = {{1,2,3},{4,5,6},{7,8,9}}; System.out.println("Spiral: " + spiral(m)); System.out.println("Diagonal: " + diagonalOrder(m)); } }
Diagonal: [1, 2, 4, 7, 5, 3, 6, 8, 9]
- Specific order required → spiral (boundary shrinking) or diagonal (invariant grouping).
- Connectivity required → DFS/BFS flood fill.
- Process every cell → row-major for cache performance.
- Transpose/rotate → column-major logic within row-major tiles.
- Most interview problems combine two patterns.
Row-by-Row and Column-by-Column — The Foundation Every Pattern Builds On
Row-major traversal visits every element left-to-right, top-to-bottom — exactly how English speakers read a page. In Java, a 2D array is technically an 'array of arrays.' These sub-arrays (rows) are generally stored in contiguous blocks of memory.
Accessing elements row-by-row means sequential memory reads, which keeps the CPU cache happy (spatial locality) and makes your loop significantly faster. Column-major traversal flips this—the outer loop iterates over columns, the inner loop over rows. This is essential for operations like transposing an image or rotating a matrix 90 degrees, but it comes with a performance penalty on massive datasets because it forces the CPU to jump across memory addresses.
package io.thecodeforge.traversal; public class RowAndColumnTraversal { public static void main(String[] args) { int[][] pixelGrid = { { 120, 200, 45, 180 }, { 30, 255, 90, 110 }, { 170, 60, 210, 15 } }; System.out.println("=== Row-Major Traversal ==="); rowMajorScan(pixelGrid); System.out.println("\n=== Column-Major Traversal ==="); columnMajorScan(pixelGrid); } static void rowMajorScan(int[][] grid) { for (int row = 0; row < grid.length; row++) { for (int col = 0; col < grid[row].length; col++) { System.out.print(grid[row][col] + " "); } System.out.println(); } } static void columnMajorScan(int[][] grid) { if (grid == null || grid.length == 0) return; int totalCols = grid[0].length; for (int col = 0; col < totalCols; col++) { for (int row = 0; row < grid.length; row++) { if (col < grid[row].length) { System.out.print(grid[row][col] + " "); } } System.out.println(); } } }
120 200 45 180
30 255 90 110
170 60 210 15
=== Column-Major Traversal ===
120 30 170
200 255 60
45 90 210
180 110 15
Diagonal Traversal — Using Mathematical Invariants
Diagonal traversal is used heavily in JPEG compression (zigzag scan) and Dynamic Programming. The key to mastering this without confusing your indices is the anti-diagonal invariant: every element on the same anti-diagonal shares a constant value for row + col.
By iterating through the sum of indices (from 0 to rows + cols - 2), you can systematically visit every diagonal. For non-square matrices, we use Math.max and Math.min to 'clamp' our indices so we don't wander outside the grid boundaries.
package io.thecodeforge.traversal; import java.util.ArrayList; import java.util.List; public class DiagonalTraversal { public static List<Integer> diagonalZigzag(int[][] matrix) { List<Integer> result = new default for cache performance. Column-major is 5- ArrayList<>(); if (matrix == null || matrix.length == 0) return result; int rows = matrix.length; int cols = matrix[0].length; for (int d = 0; d < rows + cols - 1; d++) { int startRow = Math.max(0, d - cols + 1); int endRow = Math.min(d, rows - 1); if (d % 2 == 0) { for (int r = endRow; r >= startRow; r--) { result.add(matrix[r][d - r]); } } else { for (int r = startRow; r <= endRow; r++) { result.add(matrix[r][d - r]); } } } return result; } public static void main(String[] args) { int[][] m = {{1,2,3},{4,5,6},{7,8,9}}; System.out.println("Diagonal zigzag: " + diagonalZigzag(m)); } }
- Anti-diagonal: row + col = d (constant). Group cells by their diagonal sum.
- Main diagonal: row - col = d (constant). Used in matrix operations.
- Number of anti-diagonals: rows + cols - 1.
- startRow = max(0, d - cols + 1), endRow = min(d, rows - 1).
- Zigzag: alternate direction on each diagonal. d % 2 == 0 → reverse order.
Spiral Traversal — The Boundary Shrinking Technique
Spiral traversal is the ultimate test of boundary management. Instead of complex state machines, maintain four boundaries: top, bottom, left, and right. As you complete a pass (e.g., left to right), increment the top boundary. The loop terminates naturally when boundaries cross.
package io.thecodeforge.traversal; import java.util.ArrayList; import java.util.List; public class SpiralTraversal { public List<Integer> getSpiralOrder(int[][] matrix) { List<Integer> result = new ArrayList<>(); if (matrix == null || matrix.length == 0) return result; int top = 0, bottom = matrix.length - 1; int left = 0, right = matrix[0].length - 1; while (top <= bottom && left <= right) { for (int i = left; i <= right; i++) result.add(matrix[top][i]); top++; for (int i = top; i <= bottom; i++) result.add(matrix[i][right]); right--; if (top <= bottom) { for (int i = right; i >= left; i--) result.add(matrix[bottom][i]); bottom--; } if (left <= right) { for (int i = bottom; i >= top; i--) result.add(matrix[i][left]); left++; } } return result; } }
if (top <= bottom) and if (left <= right) checks are mandatory for non-square matrices to prevent re-processing the middle row/column twice. Without these checks, a 1x3 matrix [[1,2,3]] produces [1,2,3,2,1] instead of [1,2,3].DFS and BFS on Grids — Connected Component Traversal
DFS and BFS on grids solve connectivity problems: number of islands, flood fill, shortest path in unweighted grids, and surrounded regions. The key insight is that each cell has at most 4 neighbors (up, down, left, right), and the traversal visits each cell at most once.
DFS is simpler to implement recursively but risks StackOverflowError on large grids (rec an explicit queue and is safer for production systems. Both are O(m*n) time.
The boundary check before each recursive call is critical: 0 <= r < rows AND 0 <= c < cols AND cell is unvisited AND cell matches condition. Missing any of these checks causes ArrayIndexOutOfBoundsException or infinite recursion.
package io.thecodeforge.traversal; import java.util.LinkedList; import java.util.Queue; public class GridBFS { /** * BFS shortest path in unweighted grid. O(m*n) time, O(m*n) space. * Returns the distance from (sr,sc) to (dr,dc), orursion depth equals the grid size). BFS uses -1 if unreachable. */ public static int shortestPath(int[][] grid, int sr, int sc, int dr, int dc) { if (grid == null || grid.length == 0) return -1; int rows = grid.length, cols = grid[0].length; boolean[][] visited = new boolean[rows][cols]; Queue<int[]> queue = new LinkedList<>(); queue.offer(new int[]{sr, sc, 0}); visited[sr][sc] = true; int[][] dirs = {{0,1},{0,-1},{1,0},{-1,0}}; while (!queue.isEmpty()) { int[] curr = queue.poll(); int r = curr[0], c = curr[1], dist = curr[2]; if (r == dr && c == dc) return dist; for (int[] d : dirs) { int nr = r + d[0], nc = c + d[1]; if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && !visited[nr][nc] && grid[nr][nc] == 0) { visited[nr][nc] = true; queue.offer(new int[]{nr, nc, dist + 1}); } } } return -1; // unreachable } public static void main(String[] args) { int[][] grid = { {0, 0, 1, 0}, {1, 0, 1, 0}, {0, 0, 0, 0}, {0, 1, 1, 0} }; System.out.println("Shortest path (0,0) to (3,3): " + shortestPath(grid, 0, 0, 3, 3)); } }
- DFS: connectivity problems. Recursive. O(mn) time, O(mn) space for visited array.
- BFS: shortest path in unweighted grids. Explicit queue. O(mn) time, O(mn) space.
- DFS recursion depth = grid size. StackOverflowError on grids > 10,000 cells.
- BFS is always production-safe. Use it unless the problem specifically requires DFS.
- Boundary check: 0 <= r < rows AND 0 <= c < cols AND unvisited AND condition.
| Traversal Pattern | Loop Structure | Key Invariant | Primary Use Case | Complexity |
|---|---|---|---|---|
| Row-Major | Outer=row, Inner=col | grid[row][col] | Default scanning, Cache optimization | O(M*N) Time, O(1) Space |
| Column-Major | Outer=col, Inner=row | grid[row][col] | Transpose, Column sums | O(M*N) Time, O(1) Space |
| Diagonal | Outer=sum, Inner=row | row + col = const | JPEG Zigzag, DP table fill | O(M*N) Time, O(1) Space |
| Spiral | While(boundaries) | Boundary shrinking | Layer processing, Rotation | O(M*N) Time, O(1) Space |
| DFS | Recursive or stack | Connected components | Number of islands, Flood fill | O(MN) Time, O(MN) Space |
| BFS | Queue-based | Level-order expansion | Shortest path in unweighted grid | O(MN) Time, O(MN) Space |
| Tiled (row-major blocks) | Block outer, row-major inner | L1 cache block size | Transpose, Large matrix operations | O(M*N) Time, O(1) Space |
🎯 Key Takeaways
- Row-major is your performance champion due to memory layout in Java. Column-major is 5-10x slower on large matrices due to cache thrashing.
- Diagonal traversals are easily solved by anchoring on the 'row + col' invariant. Number of diagonals = rows + cols - 1.
- Spiral traversal requires 4 boundaries and 2 specific guard checks (top <= bottom, left <= right) for non-square grids.
- Always validate for null, empty, and jagged rows before beginning any matrix traversal.
- DFS for connectivity, BFS for shortest path. DFS risks StackOverflowError on large grids — use BFS for production.
- For matrix transpose on large matrices, use tiled traversal (64x64 blocks in row-major) to keep data in L1 cache.
- Rotation = transpose + reverse rows (clockwise). Forgetting the reversal is a common bug.
- Test spiral traversal with 1xN and Nx1 matrices. These single-row/column cases expose missing guard checks.
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QHow do you rotate an NxN matrix 90 degrees clockwise in-place? What is the time and space complexity?
- QWhat happens to your spiral algorithm if the matrix is 100x1? How do the guard checks prevent duplicate visits?
- QWhy is row-major traversal typically faster than column-major in Java? What role does CPU cache locality play?
- QHow would you find the number of islands in a binary matrix? Compare DFS and BFS approaches.
- QExplain the row+col invariant for diagonal traversal. How does it help you traverse all diagonals without nested loops?
- QHow would you implement a shortest path algorithm on an unweighted grid? What data structure do you use and why?
- QYour transpose operation is 10x slower than expected on 8K images. What is the most likely cause and how do you fix it?
- QHow would you generate a spiral matrix of size NxN filled with numbers 1 to N^2?
- QDescribe a production scenario where matrix traversal choice impacted performance. What pattern did you use and what was the cache impact?
- QHow would you handle DFS on a grid with 100 million cells without causing a StackOverflowError?
Frequently Asked Questions
What is the time complexity of spiral matrix traversal?
It is O(M x N) where M is rows and N is columns, as every cell is visited exactly once.
How do I traverse a matrix diagonally in Java?
Iterate through the sum of indices (row + col). Use Math.max and Math.min to ensure your derived indices stay within the bounds of the rows and columns.
Does row-major order matter for small matrices?
For small matrices that fit in L1 cache, the difference is negligible. However, for large matrices (1000x1000+), row-major is 5-10x faster due to CPU cache locality. Always benchmark on production-sized data.
How do you handle boundary conditions in matrix DFS?
Before recursing into neighbour (r,c), check: 0<=r<rows and 0<=c<cols and cell is unvisited and matches your condition. Combine all checks before any processing to avoid index-out-of-range errors.
What is the time complexity of a matrix DFS/BFS?
O(mn) — each cell is visited at most once. Each cell has at most 4 neighbours, so total edge traversals are O(4mn) = O(mn).
How do I rotate a matrix 90 degrees in-place?
For clockwise rotation: first transpose the matrix (swap M[i][j] with M[j][i] for all i<j), then reverse each row. For counter-clockwise: transpose then reverse each column (or reverse each row then transpose). Both are O(n^2) time and O(1) extra space.
What is the time complexity of diagonal traversal for an n x m matrix?
Each cell is visited exactly once, so diagonal traversal is O(n*m) time and O(min(n,m)) space for the current diagonal. The number of diagonals is n+m-1, but total work is still proportional to the total number of cells.
Why does column-major traversal cause cache thrashing on large matrices?
Java 2D arrays are stored row-major: each row is a contiguous block of memory. Column-major traversal accesses matrix[0][col], matrix[1][col], matrix[2][col] — each access is width x element_size bytes apart. For a 4096-wide matrix with 4-byte ints, that is 16KB between consecutive accesses. The L1 cache (32KB) can hold only 2 rows. Every column access beyond the first 2 rows is a cache miss.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.