Traverse an m × n matrix in spiral order by maintaining four shrinking boundaries — top, bottom, left, right — and peeling one ring per while-loop iteration.
Given a 2D matrix, return all elements in spiral order — clockwise from the top-left corner. For the matrix [[1,2,3],[4,5,6],[7,8,9]] the answer is [1,2,3,6,9,8,7,4,5]: right across row 0, down column 2, left across row 2, up column 0, then the lone center cell 5. This pattern appears in image rotation, memory layout, and printer rasterization.
top, bottom, left, right — and after each side is traversed, advance the corresponding boundary inward. No direction array, no visited set; the boundaries do all the bookkeeping.top=0, bottom=rows-1, left=0, right=cols-1. Loop while top <= bottom && left <= right.c = left…right along matrix[top][c]. Then top++ to shrink the top boundary.r = top…bottom along matrix[r][right]. Then right--.top <= bottom (prevents re-visiting a single middle row). Walk right-to-left, then bottom--.left <= right (prevents re-visiting a single middle column). Walk bottom-to-top, then left++.top <= bottom check, step 3 would walk it backwards, duplicating values. Always guard the last two directions.Both approaches are O(m·n) time since every cell is visited exactly once. The direction-array approach is more general (works for Spiral Matrix II — generating the spiral) but allocates a full visited matrix. The boundary approach is the space-optimal choice for read-only traversal.
0 bottom=2 left=0 right=2.function spiralOrder(matrix: number[][]): number[] {
const result: number[] = [];
let top = 0, bottom = matrix.length - 1;
let left = 0, right = matrix[0].length - 1;
while (top <= bottom && left <= right) {
// → traverse top row left to right
for (let c = left; c <= right; c++) result.push(matrix[top][c]);
top++;
// ↓ traverse right column top to bottom
for (let r = top; r <= bottom; r++) result.push(matrix[r][right]);
right--;
// ← traverse bottom row right to left (guard: top ≤ bottom)
if (top <= bottom) {
for (let c = right; c >= left; c--) result.push(matrix[bottom][c]);
bottom--;
}
// ↑ traverse left column bottom to top (guard: left ≤ right)
if (left <= right) {
for (let r = bottom; r >= top; r--) result.push(matrix[r][left]);
left++;
}
}
return result;
}top and bottom are the inclusive row bounds; left and right are the inclusive column bounds. The while condition keeps looping as long as there is at least one row and one column left to process.left to right along matrix[top][c]. After the pass, top++ retires that row: it will never be touched again.top (post-increment) means we start below the row we just finished. After the pass, right-- retires the right column.top <= bottomguard catches the case where the remaining "ring" is a single row — it was already consumed in step 1, so we must not walk it again. After the pass, bottom--.left <= right guard catches the single-column degenerate case. After the pass, left++. The while condition re-evaluates: if both bounds have crossed, we exit with every cell collected.1…n² into a freshly allocated matrix using the same boundary loop — same structure, reversed data flow.result.push(matrix[r][c]) to result.push([r, c]) — zero algorithmic change.| "return elements in spiral order" | four-boundary simulation |
| process a 2D grid ring by ring | top/bottom/left/right pointers |
| need O(1) space matrix traversal | boundary pointers (no visited array) |
| "spiral matrix II / rotate image" | same boundary loop, write instead of read |
let top = 0, bottom = matrix.length - 1;
let left = 0, right = matrix[0].length - 1;
const result: number[] = [];
while (top <= bottom && left <= right) {
for (let c = left; c <= right; c++) result.push(matrix[top][c]);
top++;
for (let r = top; r <= bottom; r++) result.push(matrix[r][right]);
right--;
if (top <= bottom) { for (let c = right; c >= left; c--) result.push(matrix[bottom][c]); bottom--; }
if (left <= right) { for (let r = bottom; r >= top; r--) result.push(matrix[r][left]); left++; }
}
return result;[[1,2,3],[4,5,6],[7,8,9]]?if (top <= bottom)?