bigywater.tech
算法与数据结构 · LeetCode 54

LeetCode 54. 螺旋矩阵

给定一个 m × n 的矩阵,按顺时针螺旋的顺序返回所有元素:先走最上面一行从左到右,再走最右边一列从上到下,再走最下面一行从右到左,再走最左边一列从下到上,然后向里缩一圈,重复。这道题没有算法上的窍门,难点是边界:当剩下的区域只有一行或一列时,"下边"和"左边"会重复走已经走过的格子。本文用真实运行展示漏掉这个判断的后果,实测耗时,给出五种语言的实现,并把逐圈走的过程做成分步演示。

5,000
组随机对拍,与暴力解的结果全部一致
7 步
分步演示,每一帧都来自解法的真实执行,另附 5 种语言实现

题意与示例

完整题目见官方页面(链接在文末)。矩阵不一定是方阵。基准解是另一种思路:每次取走第一行,把剩下的矩阵逆时针旋转,再重复。一共 12 组用例,覆盖:官方示例(3×3 和 3×4)、1×1、只有一行、只有一列、2×2、2×3、3×2、方阵:

真实实测 · 本机执行输出
输入 [[[1,2,3],[4,5,6],[7,8,9]]]
输出 [1,2,3,6,9,8,7,4,5]
输入 [[[1,2,3,4],[5,6,7,8],[9,10,11,12]]]
输出 [1,2,3,4,8,12,11,10,9,5,6,7]
输入 [[[1]]]
输出 [1]
输入 [[[1,2,3]]]
输出 [1,2,3]
输入 [[[1],[2],[3]]]
输出 [1,2,3]
输入 [[[1,2],[3,4]]]
输出 [1,2,4,3]

陷阱:只剩一行或一列时,下边和左边会重复

每一圈按上、右、下、左四条边走。当剩下的区域只有一行时,"上边"已经走完了整行,"下边"与"上边"是同一行,再走一遍就重复了;只有一列时,"右边"走完了整列,"左边"与它重复。所以只有区域至少有 2 行、2 列时,才需要走下边和左边。下面是每圈无条件走四条边的写法与 solve 的真实结果:

真实实测 · 本机执行输出
# 每圈无条件走四条边的写法(没有判断是否还有下边、左边)
[[1, 2, 3]]: 无条件四条边 -> [1, 2, 3, 2, 1]    solve -> [1, 2, 3]
[[1], [2], [3]]: 无条件四条边 -> [1, 2, 3, 2]    solve -> [1, 2, 3]
[[1, 2, 3], [4, 5, 6]]: 无条件四条边 -> [1, 2, 3, 6, 5, 4]    solve -> [1, 2, 3, 6, 5, 4]
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]: 无条件四条边 -> [1, 2, 3, 6, 9, 8, 7, 4, 5]    solve -> [1, 2, 3, 6, 9, 8, 7, 4, 5]

[[1, 2, 3]] 无条件走四条边得到 [1, 2, 3, 2, 1]:上边走完 1、2、3,"下边"又从右往左走了一遍 2、1;[[1], [2], [3]] 得到 [1, 2, 3, 2]。而 2×3 和 3×3 的矩阵,没有这个问题(最后一圈区域是完整的至少 2 行 2 列,或者是中间的一个格子),两种写法结果相同。所以这个错误只在只有一行、一列,或者收缩到最后只剩一行、一列时才暴露。

逐圈收缩:五种语言的实现

用 top、bottom、left、right 表示还没走过的矩形区域。每一圈:先走上边(第 top 行,从 left 到 right);再走右边(第 right 列,从 top + 1 到 bottom);如果 top < bottom 并且 left < right(至少 2 行 2 列),再走下边(第 bottom 行,从 right - 1 到 left)和左边(第 left 列,从 bottom - 1 到 top + 1);然后四条边各向里缩一格。Python、Java、Go、JavaScript、C++ 都是真实编译运行的。

Python · 解法代码def solve(mat): res = [] top, bottom, left, right = 0, len(mat) - 1, 0, len(mat[0]) - 1 # 还没有走过的矩形区域的四条边 while top <= bottom and left <= right: for j in range(left, right + 1): # 上边,从左到右 res.append(mat[top][j]) for i in range(top + 1, bottom + 1): # 右边,从上到下 res.append(mat[i][right]) if top < bottom and left < right: # 只有区域至少 2 行 2 列时,才有下边和左边 for j in range(right - 1, left - 1, -1): # 下边,从右到左 res.append(mat[bottom][j]) for i in range(bottom - 1, top, -1): # 左边,从下到上 res.append(mat[i][left]) top, bottom, left, right = top + 1, bottom - 1, left + 1, right - 1 # 向里收缩一圈 return res
真实实测 · Python · 前几组用例的输出
$ python3 solution.py   # 统一入口:逐行读取 cases.jsonl,调用上面的 solve,打印 JSON
[[[1,2,3],[4,5,6],[7,8,9]]]  →  [1,2,3,6,9,8,7,4,5]
[[[1,2,3,4],[5,6,7,8],[9,10,11,12]]]  →  [1,2,3,4,8,12,11,10,9,5,6,7]
[[[1]]]  →  [1]
[[[1,2,3]]]  →  [1,2,3]
[[[1],[2],[3]]]  →  [1,2,3]
[[[1,2],[3,4]]]  →  [1,2,4,3]
Java · 解法代码class Solution { public List<Integer> spiralOrder(int[][] matrix) { List<Integer> res = new ArrayList<>(); int top = 0, bottom = matrix.length - 1, left = 0, right = matrix[0].length - 1; // 还没走过的矩形区域的四条边 while (top <= bottom && left <= right) { for (int j = left; j <= right; j++) res.add(matrix[top][j]); // 上边,从左到右 for (int i = top + 1; i <= bottom; i++) res.add(matrix[i][right]); // 右边,从上到下 if (top < bottom && left < right) { // 只有区域至少 2 行 2 列时才有下边和左边 for (int j = right - 1; j >= left; j--) res.add(matrix[bottom][j]); // 下边,从右到左 for (int i = bottom - 1; i > top; i--) res.add(matrix[i][left]); // 左边,从下到上 } top++; bottom--; left++; right--; // 向里收缩一圈 } return res; } }
真实实测 · Java · 前几组用例的输出
$ javac -d build Main.java Solution.java && java -cp build Main < cases.jsonl
[[[1,2,3],[4,5,6],[7,8,9]]]  →  [1,2,3,6,9,8,7,4,5]
[[[1,2,3,4],[5,6,7,8],[9,10,11,12]]]  →  [1,2,3,4,8,12,11,10,9,5,6,7]
[[[1]]]  →  [1]
[[[1,2,3]]]  →  [1,2,3]
[[[1],[2],[3]]]  →  [1,2,3]
[[[1,2],[3,4]]]  →  [1,2,4,3]
Go · 解法代码func spiralOrder(matrix [][]int) []int { res := []int{} top, bottom, left, right := 0, len(matrix)-1, 0, len(matrix[0])-1 // 还没走过的矩形区域的四条边 for top <= bottom && left <= right { for j := left; j <= right; j++ { // 上边,从左到右 res = append(res, matrix[top][j]) } for i := top + 1; i <= bottom; i++ { // 右边,从上到下 res = append(res, matrix[i][right]) } if top < bottom && left < right { // 只有区域至少 2 行 2 列时才有下边和左边 for j := right - 1; j >= left; j-- { // 下边,从右到左 res = append(res, matrix[bottom][j]) } for i := bottom - 1; i > top; i-- { // 左边,从下到上 res = append(res, matrix[i][left]) } } top, bottom, left, right = top+1, bottom-1, left+1, right-1 // 向里收缩一圈 } return res }
真实实测 · Go · 前几组用例的输出
$ go build -o bin . && ./bin < cases.jsonl
[[[1,2,3],[4,5,6],[7,8,9]]]  →  [1,2,3,6,9,8,7,4,5]
[[[1,2,3,4],[5,6,7,8],[9,10,11,12]]]  →  [1,2,3,4,8,12,11,10,9,5,6,7]
[[[1]]]  →  [1]
[[[1,2,3]]]  →  [1,2,3]
[[[1],[2],[3]]]  →  [1,2,3]
[[[1,2],[3,4]]]  →  [1,2,4,3]
JavaScript · 解法代码function spiralOrder(matrix) { const res = []; let top = 0, bottom = matrix.length - 1, left = 0, right = matrix[0].length - 1; // 还没走过的矩形区域的四条边 while (top <= bottom && left <= right) { for (let j = left; j <= right; j++) res.push(matrix[top][j]); // 上边,从左到右 for (let i = top + 1; i <= bottom; i++) res.push(matrix[i][right]); // 右边,从上到下 if (top < bottom && left < right) { // 只有区域至少 2 行 2 列时才有下边和左边 for (let j = right - 1; j >= left; j--) res.push(matrix[bottom][j]); // 下边,从右到左 for (let i = bottom - 1; i > top; i--) res.push(matrix[i][left]); // 左边,从下到上 } top++; bottom--; left++; right--; // 向里收缩一圈 } return res; }
真实实测 · JavaScript · 前几组用例的输出
$ node run.js < cases.jsonl
[[[1,2,3],[4,5,6],[7,8,9]]]  →  [1,2,3,6,9,8,7,4,5]
[[[1,2,3,4],[5,6,7,8],[9,10,11,12]]]  →  [1,2,3,4,8,12,11,10,9,5,6,7]
[[[1]]]  →  [1]
[[[1,2,3]]]  →  [1,2,3]
[[[1],[2],[3]]]  →  [1,2,3]
[[[1,2],[3,4]]]  →  [1,2,4,3]
C++ · 解法代码#include <vector> using namespace std; class Solution { public: vector<int> spiralOrder(vector<vector<int>>& matrix) { vector<int> res; int top = 0, bottom = (int)matrix.size() - 1, left = 0, right = (int)matrix[0].size() - 1; // 还没走过的矩形区域的四条边 while (top <= bottom && left <= right) { for (int j = left; j <= right; j++) res.push_back(matrix[top][j]); // 上边,从左到右 for (int i = top + 1; i <= bottom; i++) res.push_back(matrix[i][right]); // 右边,从上到下 if (top < bottom && left < right) { // 只有区域至少 2 行 2 列时才有下边和左边 for (int j = right - 1; j >= left; j--) res.push_back(matrix[bottom][j]); // 下边,从右到左 for (int i = bottom - 1; i > top; i--) res.push_back(matrix[i][left]); // 左边,从下到上 } top++; bottom--; left++; right--; // 向里收缩一圈 } return res; } };
真实实测 · C++ · 前几组用例的输出
$ clang++ -std=c++17 -O2 -o bin sol.cpp && ./bin < cases.jsonl
[[[1,2,3],[4,5,6],[7,8,9]]]  →  [1,2,3,6,9,8,7,4,5]
[[[1,2,3,4],[5,6,7,8],[9,10,11,12]]]  →  [1,2,3,4,8,12,11,10,9,5,6,7]
[[[1]]]  →  [1]
[[[1,2,3]]]  →  [1,2,3]
[[[1],[2],[3]]]  →  [1,2,3]
[[[1,2],[3,4]]]  →  [1,2,4,3]
真实实测 · 本机执行输出
共 12 组用例(含边界),以暴力解的结果为基准逐组比对:
  Python      通过 12/12
  Java        通过 12/12
  Go          通过 12/12
  JavaScript  通过 12/12
  C++         通过 12/12

耗时实测

矩阵边长 n 分别为 100、300、1000、2000(n × n 个元素),每个规模取三次中最快的一次:

真实实测 · 本机执行输出
边长 n(n×n 矩阵) | 暴力解 | 本文解法
       100 |     4.724 ms | 0.196 ms
       300 |   109.857 ms | 1.432 ms
      1000 |      (未测,太慢) | 30.139 ms
      2000 |      (未测,太慢) | 176.959 ms

本文解法访问每个元素一次,是 O(元素个数),边长每翻一倍元素个数变成 4 倍:从 1000 到 2000,耗时增长约 5.9 倍,比 4 倍略高,从 100 到 300(元素 9 倍)增长约 7.3 倍,从 300 到 1000(元素 11 倍)增长约 21 倍,有波动,我没有细究,可能与大矩阵的缓存行为有关,这只是猜测。基准解每取一行就要把整个剩下的矩阵旋转一次,是 O(n³):从 100 到 300(3 倍)增长约 23 倍(27 倍是立方),所以基准在更大的规模上没有再测。

随机对拍

Python 版本另外和"取一行再旋转"的基准做了随机对拍,行数、列数各随机取 1 到 7:

真实实测 · 本机执行输出
随机对拍 5000 组(行数、列数各随机取 1 到 7 的矩阵(包含只有一行或一列的)),solve 与 brute 的结果全部一致。

另外四种语言只在上面 12 组固定用例上验证过,没有做大规模随机对拍。

分步演示:一圈一圈往里走

下面的演示用 3×4 的矩阵,共两圈:第一圈走外面一圈的 10 个元素,第二圈是中间的一行 [6, 7]。第二圈时只剩一行,所以只走上边,下边和左边被跳过(演示里没有对应的帧)。每一帧都是把上面的解法在这组输入上真实执行记录下来的,运行器还检查了演示的最终结果与 solve 的返回值一致。

螺旋矩阵:[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]
刚访问的格子已访问

    参考与说明

    • 官方题目页:LeetCode 54. 螺旋矩阵。本文只用自己的话概括了题意,完整约束以官方页面为准。
    • 数据来自本机真实运行,五种语言的版本写在页脚;耗时对比只用 Python 测量。
    • 没有覆盖:用"方向数组加已访问标记"的模拟做法;逆时针的变体。
    京ICP备2026067502号-1京公网安备11010502063487号