54. Spiral Matrix
題目 / Problem
中文: 給定一個 m x n 的矩陣 matrix,請按照「螺旋順序」回傳矩陣中的所有元素。螺旋順序就是:從左上角開始,先往右走完第一列,再往下走完最右行,接著往左走完最後一列,再往上走完最左行,然後往內縮一圈重複這個動作,直到所有元素都被讀完為止。
English: Given an m x n matrix, return all of its elements in spiral order. Spiral order means: start at the top-left, go right across the top row, then down the right column, then left across the bottom row, then up the left column, then shrink inward by one ring and repeat until every element has been visited.
Constraints / 限制條件:
- m == matrix.length(列數 / number of rows)
- n == matrix[i].length(行數 / number of columns)
- 1 <= m, n <= 10
- -100 <= matrix[i][j] <= 100
Worked example / 範例:
Input: matrix = [[1,2,3],[4,5,6],[7,8,9]]
Output: [1,2,3,6,9,8,7,4,5]
從 1 出發 → 往右 1,2,3 → 往下 6,9 → 往左 8,7 → 往上 4 → 往內剩下 5。
名詞解釋 / Glossary
- 矩陣 / matrix:一個由列(row)和行(column)組成的二維表格。
matrix[i][j]表示第i列、第j行的數字。A 2D table of numbers indexed by rowiand columnj. - 二維陣列 / 2D array:陣列裡面再裝陣列。在 C 裡用
int**(指向「指向 int 的指標」的指標)表示,外層選列、內層選行。An array of arrays; in C it's anint**where the outer pointer picks a row and the inner one picks a column. - 邊界指標 / boundary pointers:四個變數
top, bottom, left, right,標記目前「還沒讀過的區塊」的上下左右界線。每讀完一條邊就把對應的界線往內收一格。Four variables marking the still-unread region; after finishing one side we move that boundary inward by one. - 模擬 / simulation:不找數學公式,而是「照著題目描述一步步走」的解法。本題就是老老實實地按螺旋路線走一圈。Solving by literally following the described motion step by step, rather than a formula.
- 原地 / in-place vs 額外空間:這裡我們需要一個輸出陣列來放結果,但不需要修改原矩陣。We need an output array, but we never modify the input matrix itself.
malloc/ 動態配置記憶體:C 語言在執行期間向系統要一塊記憶體的函式,回傳指向那塊記憶體的指標;用完理論上要free,但 LeetCode 會幫你回收。A C function that requests memory at runtime and returns a pointer to it.
思路
最直覺的「暴力」想法是:開四個方向(右、下、左、上),用一個 visited 標記陣列記住哪些格子走過了,每走一步就看看「前面那一格是不是出界或已經走過」,如果是就轉 90 度。這種寫法可行,但需要額外一個和矩陣一樣大的布林陣列,而且轉向的判斷有點繞。我們可以做得更乾淨。更好的方法是「一層一層剝洋蔥」:用 top、bottom、left、right 四條邊界框住目前還沒讀的矩形區域。每一圈固定做四件事:(1) 沿著 top 這一列從 left 走到 right,然後 top++;(2) 沿著 right 這一行從 top 走到 bottom,然後 right--;(3) 沿著 bottom 這一列從 right 走回 left,然後 bottom--;(4) 沿著 left 這一行從 bottom 走回 top,然後 left++。每做完一個方向就把對應邊界往內收,這樣自然不會重複也不會出界。關鍵的「不變量(invariant)」是:四條邊界永遠圍住「尚未讀取」的格子;當 top > bottom 或 left > right 時,代表沒東西可讀了,迴圈結束。要特別小心的是步驟 (3) 和 (4):只有在「還有多於一列 / 多於一行」時才該執行,否則在單列或單行的情況會把同一排數字讀第二次,所以每次往左 / 往上之前都要再檢查一次 top <= bottom 和 left <= right。
The brute-force idea is to keep a visited array the same size as the matrix, walk in the current direction, and turn 90° whenever the next cell is out of bounds or already visited. It works but costs extra space and the turning logic is fiddly. The cleaner approach is to peel the matrix like an onion using four boundary pointers — top, bottom, left, right — that enclose the rectangle of not-yet-read cells. Each ring does exactly four passes: left→right along top (then top++), top→bottom along right (then right--), right→left along bottom (then bottom--), and bottom→top along left (then left++). Shrinking the matching boundary after each pass guarantees we never revisit a cell or step out of bounds. The invariant is that the four boundaries always frame the unread region; we stop as soon as top > bottom or left > right. The subtle part is the third and fourth passes: they must be guarded by re-checking top <= bottom and left <= right, otherwise a single remaining row or column would be read a second time. With those guards the simulation handles every shape — square, wide, tall, single row, single column — correctly.
逐步走查 / Walkthrough
Input: [[1,2,3],[4,5,6],[7,8,9]],初始 top=0, bottom=2, left=0, right=2,輸出陣列 out,寫入位置 k=0。
| 步驟 Step | 動作 Action | 讀到的值 Values | 邊界更新 Boundary update | out 目前內容 |
|---|---|---|---|---|
| 1 | top 列:j 從 0→2 | 1, 2, 3 | top=1 |
[1,2,3] |
| 2 | right 行:i 從 1→2 | 6, 9 | right=1 |
[1,2,3,6,9] |
| 3 | bottom 列:j 從 1→0(檢查 top≤bottom ✓) | 8, 7 | bottom=1 |
[1,2,3,6,9,8,7] |
| 4 | left 行:i 從 1→1(檢查 left≤right ✓) | 4 | left=1 |
[1,2,3,6,9,8,7,4] |
| 5 | top 列:j 從 1→1(此時 top=1,bottom=1,left=1,right=1) | 5 | top=2 |
[1,2,3,6,9,8,7,4,5] |
| 6 | 迴圈條件:top(2) > bottom(1) 成立 → 結束 |
— | — | done |
最終輸出 [1,2,3,6,9,8,7,4,5],與預期相符。注意第 5 步只讀了單一格 5,然後第 2 個方向因為 top(2) > bottom(1) 不會執行,迴圈也隨即停止 —— 這就是邊界檢查在發揮作用。
Solution — C
// 演算法 / Algorithm:
// 用 top/bottom/left/right 四條邊界框住未讀區域,每圈依序讀 上排→右行→下排→左行,
// 讀完一條就把該邊界往內收一格,直到邊界交錯為止。
// Frame the unread region with four boundaries; each ring reads top→right→bottom→left,
// shrinking the matching boundary after every pass, until the boundaries cross.
#include <stdlib.h> // malloc 在這裡宣告 / malloc is declared here
/* LeetCode 給的簽名:matrix 是 int**,matrixSize 是列數 m,
* matrixColSize[i] 是第 i 列的行數 n,returnSize 要寫回結果長度。
* LeetCode signature: matrix is int**, matrixSize = m,
* matrixColSize[i] = n for row i, write the result length into *returnSize. */
int* spiralOrder(int** matrix, int matrixSize, int* matrixColSize, int* returnSize) {
int m = matrixSize; // 列數 / number of rows
int n = matrixColSize[0]; // 行數(每列等長)/ number of columns (all rows equal)
int total = m * n; // 元素總數 / total number of elements
*returnSize = total; // 透過指標把長度回傳給呼叫者 / report length to caller via pointer
// 向系統要一塊能裝 total 個 int 的記憶體;回傳的指標就是輸出陣列
// Ask the OS for room for `total` ints; the returned pointer is our output array
int* out = (int*)malloc(sizeof(int) * total);
int k = 0; // out 的下一個寫入位置 / next write slot in out
int top = 0, bottom = m - 1; // 上下邊界(含端點)/ top & bottom row boundaries (inclusive)
int left = 0, right = n - 1; // 左右邊界(含端點)/ left & right column boundaries (inclusive)
// 只要還有未讀的格子就繼續 / keep going while an unread rectangle still exists
while (top <= bottom && left <= right) {
// (1) 沿 top 這一列,由左到右 / top row, left → right
for (int j = left; j <= right; j++)
out[k++] = matrix[top][j]; // matrix[top][j] 取第 top 列第 j 行 / cell at row top, col j
top++; // 這一列讀完,邊界下移 / done with this row, move boundary down
// (2) 沿 right 這一行,由上到下 / right column, top → bottom
for (int i = top; i <= bottom; i++)
out[k++] = matrix[i][right];
right--; // 最右行讀完,邊界左移 / done, move boundary left
// (3) 沿 bottom 這一列,由右到左;先確認還有列可讀,避免單列重讀
// bottom row, right → left; guard against re-reading a single leftover row
if (top <= bottom) {
for (int j = right; j >= left; j--)
out[k++] = matrix[bottom][j];
bottom--; // 最底列讀完,邊界上移 / move boundary up
}
// (4) 沿 left 這一行,由下到上;先確認還有行可讀,避免單行重讀
// left column, bottom → top; guard against re-reading a single leftover column
if (left <= right) {
for (int i = bottom; i >= top; i--)
out[k++] = matrix[i][left];
left++; // 最左行讀完,邊界右移 / move boundary right
}
}
return out; // 回傳結果陣列;LeetCode 會負責 free / return result; LeetCode frees it
}
Solution — C++
// 演算法 / Algorithm:
// 與 C 版相同:四條邊界 top/bottom/left/right 框住未讀區域,
// 每圈讀 上→右→下→左 並把對應邊界往內收,直到邊界交錯。
// Same as the C version: four boundaries enclose the unread region; each ring reads
// top→right→bottom→left and shrinks the matching boundary until they cross.
#include <vector>
using namespace std;
class Solution {
public:
// vector<vector<int>> 是「裝著 int 向量的向量」,即二維動態陣列
// vector<vector<int>> is a vector of int-vectors, i.e. a dynamic 2D array
vector<int> spiralOrder(vector<vector<int>>& matrix) {
int m = matrix.size(); // 列數 / number of rows
int n = matrix[0].size(); // 行數 / number of columns
vector<int> out; // 結果向量,會自動成長 / result vector, grows automatically
out.reserve(m * n); // 預先配置空間,避免反覆重新配置 / pre-allocate to avoid reallocations
int top = 0, bottom = m - 1; // 上下邊界 / top & bottom boundaries (inclusive)
int left = 0, right = n - 1; // 左右邊界 / left & right boundaries (inclusive)
while (top <= bottom && left <= right) {
// (1) top 列由左到右 / top row, left → right
for (int j = left; j <= right; ++j)
out.push_back(matrix[top][j]); // push_back 把元素加到尾端 / append to the end
++top;
// (2) right 行由上到下 / right column, top → bottom
for (int i = top; i <= bottom; ++i)
out.push_back(matrix[i][right]);
--right;
// (3) bottom 列由右到左;守門避免單列重讀 / bottom row, guarded against single-row reread
if (top <= bottom) {
for (int j = right; j >= left; --j)
out.push_back(matrix[bottom][j]);
--bottom;
}
// (4) left 行由下到上;守門避免單行重讀 / left column, guarded against single-col reread
if (left <= right) {
for (int i = bottom; i >= top; --i)
out.push_back(matrix[i][left]);
++left;
}
}
return out; // 回傳結果 / return the spiral order
}
};
複雜度 / Complexity
- Time: O(m × n) — 每個格子只被讀取(寫入 out)剛好一次,總工作量正比於元素總數
m × n。邊界收縮的判斷都是 O(1),不影響整體。Each cell is visited exactly once, so total work is proportional to the number of elementsm × n. - Space: O(1) 額外空間 — 除了必須回傳的輸出陣列外,只用了四個邊界變數與幾個計數器,不隨輸入大小成長。若把回傳陣列也算進來則是 O(m × n),但那是題目要求的輸出,不算「額外」。Beyond the required output array we use only a handful of integer variables, independent of input size.
Pitfalls & Edge Cases
- 單列或單行 / single row or single column:例如
[[1,2,3]](一列)或[[1],[2],[3]](一行)。如果第 (3)、(4) 個方向沒有if (top <= bottom)/if (left <= right)守門,就會把同一排數字讀第二次,輸出多出重複值。守門條件正是為此存在。Without the boundary guards, a lone leftover row/column gets read twice. *returnSize沒設定 / forgetting to set*returnSize(C):LeetCode 靠這個值知道你的陣列有多長;忘了寫會讀到垃圾長度或被判錯。Must write the result length through thereturnSizepointer.- 行數要用
matrixColSize[0]而非自己猜 / column count comes frommatrixColSize[0](C):題目保證每列等長,所以取第 0 列的行數即可;直接寫死數字會出錯。Readnfrom the harness, don't hardcode it. - 邊界用「含端點」要一致 / inclusive boundaries must stay consistent:本解所有邊界都是閉區間(
<=、>=),迴圈條件與收縮方向都要跟著一致,混用開閉區間是常見的 off-by-one 來源。Mixing inclusive/exclusive conventions is a classic off-by-one bug. - 空輸入 / empty input:本題限制
m, n >= 1,所以不會出現空矩陣;但若移植到別處,matrix[0].size()在空矩陣上會崩潰,需先檢查。Constraints rule out empty input here, but thematrix[0]access would crash on a truly empty matrix elsewhere.