Matrix Traversal Patterns Explained — Row, Column, Diagonal & Spiral
Matrices show up everywhere in software — image pixels, spreadsheet cells, game boards, social network adjacency tables, and pathfinding maps in GPS apps. Every time you apply a filter to a photo on Instagram or your phone auto-rotates an image, some piece of code is walking through a 2D grid in a very deliberate order. Knowing which traversal pattern to reach for isn't just an academic exercise — it's the difference between an O(n) solution and an O(n²) disaster.
The real problem matrix traversal solves is direction and order. A flat array has exactly one way to walk through it — front to back. A matrix has dozens: row-major, column-major, diagonal, anti-diagonal, spiral inward, spiral outward, and more. Choosing the wrong one doesn't just slow your code down — it can produce completely wrong results, like reading a JPEG from the bottom up and getting a scrambled mess instead of a photo.
By the end of this article you'll be able to implement four core traversal patterns from scratch, explain WHY each one exists and WHEN to use it, avoid the index-boundary bugs that crash 80% of first attempts, and walk into any interview question involving a 2D array with genuine confidence.
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. It's the default mental model most developers use, and there's a concrete performance reason for it: in Java (and most languages), a 2D array is stored in row-major order in memory. Accessing elements row-by-row means sequential memory reads, which keeps the CPU cache happy and makes your loop genuinely faster.
Column-major traversal flips the loop order — the outer loop iterates over columns, the inner loop over rows. The traversal visits top-to-bottom, then moves one column to the right. This feels backwards at first, but it's exactly what you need for operations like transposing an image, rotating a matrix 90 degrees, or computing column sums for a spreadsheet.
The critical insight is this: the nested loop structure is identical for both patterns — you're always using row and col as your two indices. The only thing that changes is which variable sits in the outer loop and which sits in the inner loop. That small swap completely changes the order elements are visited.
Know the memory layout. Java stores matrix[row][col] in contiguous row blocks. Column-major traversal causes cache misses on large matrices — this matters in performance-sensitive code like image processing.
public class RowAndColumnTraversal { public static void main(String[] args) { // A 3x4 matrix representing pixel brightness values (0-255) int[][] pixelGrid = { { 120, 200, 45, 180 }, { 30, 255, 90, 110 }, { 170, 60, 210, 15 } }; System.out.println("=== Row-Major Traversal (reading order) ==="); rowMajorScan(pixelGrid); System.out.println("\n=== Column-Major Traversal (column-first order) ==="); columnMajorScan(pixelGrid); System.out.println("\n=== Column Averages (real use-case: spreadsheet column stats) ==="); printColumnAverages(pixelGrid); } // Row-major: outer loop = rows, inner loop = columns // This follows memory layout — fastest for cache performance static void rowMajorScan(int[][] grid) { int totalRows = grid.length; // number of rows in the grid int totalCols = grid[0].length; // number of columns per row for (int row = 0; row < totalRows; row++) { for (int col = 0; col < totalCols; col++) { System.out.printf("%4d", grid[row][col]); // print with padding } System.out.println(); // newline after each row } } // Column-major: outer loop = columns, inner loop = rows // Use this when you need to process one column completely before moving on static void columnMajorScan(int[][] grid) { int totalRows = grid.length; int totalCols = grid[0].length; for (int col = 0; col < totalCols; col++) { // column is now the outer loop for (int row = 0; row < totalRows; row++) { // row is now the inner loop System.out.printf("%4d", grid[row][col]); } System.out.println(); } } // Real-world use: compute the average brightness per column static void printColumnAverages(int[][] grid) { int totalRows = grid.length; int totalCols = grid[0].length; for (int col = 0; col < totalCols; col++) { int columnSum = 0; for (int row = 0; row < totalRows; row++) { columnSum += grid[row][col]; // accumulate values down the column } double average = (double) columnSum / totalRows; System.out.printf("Column %d average brightness: %.1f%n", col, average); } } }
120 200 45 180
30 255 90 110
170 60 210 15
=== Column-Major Traversal (column-first order) ===
120 30 170
200 255 60
45 90 210
180 110 15
=== Column Averages (real use-case: spreadsheet column stats) ===
Column 0 average brightness: 106.7
Column 1 average brightness: 171.7
Column 2 average brightness: 115.0
Column 3 average brightness: 101.7
Diagonal Traversal — How Image Compression and Dynamic Programming Use It
Diagonal traversal isn't something most beginners reach for — but it's everywhere once you know where to look. JPEG compression uses diagonal scans through frequency coefficient blocks (the zigzag scan). Dynamic programming problems like Longest Common Subsequence fill a matrix diagonal-by-diagonal. Even certain matrix rotation algorithms rely on swapping elements along diagonals.
There are two diagonals to understand: the main diagonal (top-left to bottom-right, where row == col) and the anti-diagonal (top-right to bottom-left, where row + col == constant). The key insight is that every element on the same anti-diagonal shares the same row + col value. That single observation unlocks the entire pattern.
For a full zigzag traversal — the kind used in JPEG — you alternate direction on each diagonal. Even-numbered diagonals go bottom-left to top-right, odd-numbered diagonals go top-right to bottom-left. The challenge is keeping your indices within bounds as the diagonal length grows then shrinks across the matrix.
The mental model that makes this click: think of each anti-diagonal as a stripe of paint running from one corner to another. Number each stripe starting at 0. Every cell in stripe d has coordinates where row + col = d. You visit all cells in stripe 0, then stripe 1, and so on.
public class DiagonalTraversal { public static void main(String[] args) { // Simulating a 4x4 matrix of frequency coefficients (like a JPEG block) int[][] frequencyBlock = { { 1, 2, 6, 7 }, { 3, 5, 8, 13 }, { 4, 9, 12, 14 }, { 10, 11, 15, 16 } }; System.out.println("=== Main Diagonal Elements (row == col) ==="); printMainDiagonal(frequencyBlock); System.out.println("\n=== All Anti-Diagonals (row + col = constant) ==="); printAllAntiDiagonals(frequencyBlock); System.out.println("\n=== Zigzag Order (JPEG-style scan) ==="); printZigzagOrder(frequencyBlock); } // Main diagonal: only valid when row index equals column index static void printMainDiagonal(int[][] matrix) { int size = Math.min(matrix.length, matrix[0].length); // handles non-square matrices for (int index = 0; index < size; index++) { System.out.print(matrix[index][index] + " "); // row and col are the same } System.out.println(); } // Anti-diagonals: group elements where (row + col) equals the same value static void printAllAntiDiagonals(int[][] matrix) { int totalRows = matrix.length; int totalCols = matrix[0].length; int totalDiagonals = totalRows + totalCols - 1; // total number of anti-diagonals for (int diagIndex = 0; diagIndex < totalDiagonals; diagIndex++) { System.out.print("Diagonal " + diagIndex + ": "); // Starting row: either 0 (top edge) or overflow into lower rows int startRow = Math.max(0, diagIndex - totalCols + 1); // Ending row: capped at last row or diagIndex itself int endRow = Math.min(diagIndex, totalRows - 1); for (int row = startRow; row <= endRow; row++) { int col = diagIndex - row; // col is derived from the diagonal index System.out.print(matrix[row][col] + " "); } System.out.println(); } } // Zigzag: alternate direction on each anti-diagonal (real JPEG scan pattern) static void printZigzagOrder(int[][] matrix) { int totalRows = matrix.length; int totalCols = matrix[0].length; int totalDiagonals = totalRows + totalCols - 1; for (int diagIndex = 0; diagIndex < totalDiagonals; diagIndex++) { int startRow = Math.max(0, diagIndex - totalCols + 1); int endRow = Math.min(diagIndex, totalRows - 1); if (diagIndex % 2 == 0) { // Even diagonals: scan bottom-left to top-right (row decreasing) for (int row = endRow; row >= startRow; row--) { int col = diagIndex - row; System.out.print(matrix[row][col] + " "); } } else { // Odd diagonals: scan top-right to bottom-left (row increasing) for (int row = startRow; row <= endRow; row++) { int col = diagIndex - row; System.out.print(matrix[row][col] + " "); } } } System.out.println(); } }
1 5 12 16
=== All Anti-Diagonals (row + col = constant) ===
Diagonal 0: 1
Diagonal 1: 2 3
Diagonal 2: 6 5 4
Diagonal 3: 7 8 9 10
Diagonal 4: 13 12 11
Diagonal 5: 14 15
Diagonal 6: 16
=== Zigzag Order (JPEG-style scan) ===
1 2 3 6 5 4 10 9 8 7 13 12 11 14 15 16
Spiral Traversal — The Pattern Behind Matrix Rotation and Layer-by-Layer Processing
Spiral traversal is the pattern that separates candidates who've truly practiced matrix problems from those who haven't. It appears in interview questions constantly — 'print matrix in spiral order', 'generate an NxN spiral matrix', 'rotate matrix 90 degrees'. All of them share the same underlying mechanic: process the outermost layer, shrink the boundaries, repeat.
The boundary-shrinking approach is the key insight. Instead of tracking direction changes with a flag or state machine, you maintain four boundaries — topRow, bottomRow, leftCol, rightCol. Each complete loop around the perimeter moves all four boundaries one step inward. You stop when the boundaries cross.
Think of it like peeling an onion. You remove the outermost ring, then the next ring, then the next, until nothing's left. Each ring is traversed in four legs: left-to-right across the top, top-to-bottom down the right side, right-to-left across the bottom, bottom-to-top up the left side. After each full ring, tighten the boundaries.
The tricky edge case everyone misses: when you finish the top-row pass and move to the right-column pass, you must check that topRow <= bottomRow before proceeding. Similarly, check leftCol <= rightCol before the left-column pass. Without these checks, you'll double-visit elements in matrices with an odd number of rows or columns.
import java.util.ArrayList; import java.util.List; public class SpiralTraversal { public static void main(String[] args) { // Simulating a game board or image thumbnail grid int[][] gameBoard = { { 1, 2, 3, 4, 5 }, { 16, 17, 18, 19, 6 }, { 15, 24, 25, 20, 7 }, { 14, 23, 22, 21, 8 }, { 13, 12, 11, 10, 9 } }; System.out.println("=== Spiral Traversal Order ==="); List<Integer> spiralOrder = spiralTraverse(gameBoard); System.out.println(spiralOrder); System.out.println("\n=== Generate Spiral Matrix (NxN fill) ==="); int[][] spiralMatrix = generateSpiralMatrix(4); printMatrix(spiralMatrix); } // Traverse an existing matrix in spiral order using shrinking boundaries static List<Integer> spiralTraverse(int[][] matrix) { List<Integer> result = new ArrayList<>(); int topRow = 0; // topmost unprocessed row int bottomRow = matrix.length - 1; // bottommost unprocessed row int leftCol = 0; // leftmost unprocessed column int rightCol = matrix[0].length - 1; // rightmost unprocessed column while (topRow <= bottomRow && leftCol <= rightCol) { // Leg 1: traverse LEFT to RIGHT across the top row for (int col = leftCol; col <= rightCol; col++) { result.add(matrix[topRow][col]); } topRow++; // top boundary moves down — we're done with this row // Leg 2: traverse TOP to BOTTOM down the right column for (int row = topRow; row <= bottomRow; row++) { result.add(matrix[row][rightCol]); } rightCol--; // right boundary moves left — done with this column // Leg 3: traverse RIGHT to LEFT across the bottom row // Guard: only do this if there's still a row to process if (topRow <= bottomRow) { for (int col = rightCol; col >= leftCol; col--) { result.add(matrix[bottomRow][col]); } bottomRow--; // bottom boundary moves up } // Leg 4: traverse BOTTOM to TOP up the left column // Guard: only do this if there's still a column to process if (leftCol <= rightCol) { for (int row = bottomRow; row >= topRow; row--) { result.add(matrix[row][leftCol]); } leftCol++; // left boundary moves right } } return result; } // Fill an NxN matrix with numbers 1..N² in spiral order // Real-world use: generating ordered tile IDs, game map indices static int[][] generateSpiralMatrix(int size) { int[][] matrix = new int[size][size]; int topRow = 0; int bottomRow = size - 1; int leftCol = 0; int rightCol = size - 1; int fillValue = 1; // start filling from 1 while (topRow <= bottomRow && leftCol <= rightCol) { for (int col = leftCol; col <= rightCol; col++) matrix[topRow][col] = fillValue++; topRow++; for (int row = topRow; row <= bottomRow; row++) matrix[row][rightCol] = fillValue++; rightCol--; if (topRow <= bottomRow) { for (int col = rightCol; col >= leftCol; col--) matrix[bottomRow][col] = fillValue++; bottomRow--; } if (leftCol <= rightCol) { for (int row = bottomRow; row >= topRow; row--) matrix[row][leftCol] = fillValue++; leftCol++; } } return matrix; } static void printMatrix(int[][] matrix) { for (int[] row : matrix) { for (int value : row) { System.out.printf("%4d", value); } System.out.println(); } } }
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]
=== Generate Spiral Matrix (NxN fill) ===
1 2 3 4
12 13 14 5
11 16 15 6
10 9 8 7
| Traversal Pattern | Loop Structure | Key Index Relationship | Primary Use Case | Boundary Complexity |
|---|---|---|---|---|
| Row-Major | Outer=row, Inner=col | grid[row][col] | Image scanning, DP table fill left-to-right | Low — just rows/cols bounds |
| Column-Major | Outer=col, Inner=row | grid[row][col] | Transpose, column aggregation, 90° rotation | Low — just rows/cols bounds |
| Main Diagonal | Single loop, index=i | grid[i][i] | Trace operations, identity matrix, DP memoization | Low — min(rows,cols) |
| Anti-Diagonal | Outer=diagIndex, Inner=row | col = diagIndex - row | Zigzag scan, JPEG compression, DP anti-diagonal fill | Medium — startRow/endRow clamping |
| Spiral Inward | While loop, 4 boundary vars | topRow, bottomRow, leftCol, rightCol | Layer-by-layer processing, matrix rotation, game board traversal | High — 4 shrinking boundaries + 2 guards |
🎯 Key Takeaways
- Row-major is faster than column-major on large matrices in Java because 2D arrays are stored in contiguous row blocks — iterating row-by-row keeps CPU cache hits high.
- Every element on the same anti-diagonal shares the value
row + col = diagIndex— this single mathematical invariant is the key to diagonal, zigzag, and certain DP traversals. - Spiral traversal is always solved cleanest with four shrinking boundary variables, not with a direction-flag state machine — the boundary approach handles rectangular matrices and edge cases naturally.
- Always test matrix traversal code on at least three shapes: a square (3x3), a wide rectangle (2x5), and a tall rectangle (5x2) — most boundary bugs only surface when the matrix isn't square.
⚠ Common Mistakes to Avoid
- ✕Mistake 1: Off-by-one on boundary conditions in spiral traversal — Symptom: elements near the center are either skipped or printed twice, usually visible when testing a 3x1 or 1x3 matrix — Fix: add the two guard checks
if (topRow <= bottomRow)before the bottom-row leg andif (leftCol <= rightCol)before the left-column leg, and always test with single-row, single-column, and odd-dimension matrices. - ✕Mistake 2: Hardcoding
matrix[0].lengthfor column count instead of checking per-row — Symptom: ArrayIndexOutOfBoundsException on jagged (non-rectangular) arrays likeint[][] grid = {{1,2},{3}}— Fix: usematrix[row].lengthinside the inner loop instead of pre-computingtotalColsonce, or explicitly validate that all rows have equal length before traversal. - ✕Mistake 3: Confusing row and col in the anti-diagonal formula — Symptom: the traversal visits the wrong cells, especially in non-square matrices, with no obvious crash to point to the bug — Fix: anchor on the invariant
row + col = diagIndex, always derivecol = diagIndex - row, and verify with a small hand-traced 3x3 example before coding the full solution.
Interview Questions on This Topic
- QGiven an MxN matrix, print all elements in spiral order. What happens to your algorithm when M ≠ N — walk me through the edge cases your code handles.
- QHow would you rotate an NxN matrix 90 degrees clockwise in-place without allocating a new matrix? Which traversal pattern does this rely on, and what's the time and space complexity?
- QIn a dynamic programming problem where you fill a matrix cell-by-cell and each cell depends only on the cell diagonally above-left, which traversal order should you use and why — and how does the anti-diagonal `row + col = constant` property help you parallelize the computation?
Frequently Asked Questions
What is the time complexity of spiral matrix traversal?
Spiral traversal visits every element in the matrix exactly once, so the time complexity is O(M × N) where M is the number of rows and N is the number of columns. The space complexity is O(1) for the traversal itself, or O(M × N) if you store results in a list.
How do I traverse a matrix diagonally in Java?
Use the anti-diagonal invariant: every element on the same anti-diagonal satisfies row + col = diagIndex. Loop diagIndex from 0 to (rows + cols - 2), then derive valid row indices with startRow = Math.max(0, diagIndex - cols + 1) and endRow = Math.min(diagIndex, rows - 1), and compute col = diagIndex - row inside the inner loop.
Why does my spiral traversal work on a 4x4 matrix but produce duplicates on a 3x4 matrix?
You're missing the boundary guard checks before legs 3 and 4. After completing the top-row pass and incrementing topRow, you must verify topRow <= bottomRow before traversing the bottom row — otherwise a matrix with an odd number of rows causes the middle row to be processed twice. Add the same guard for leftCol <= rightCol before the left-column leg.
Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.