给定一个 n × n 的二维矩阵,把它原地顺时针旋转 90 度,不允许另外开一个二维数组。最直接的想法是找出每个位置旋转后的落点,但落点的对应关系((i, j) 到 (j, n-1-i))很容易写错。更简洁的做法是把旋转拆成两次翻转:先沿主对角线转置,再把每一行左右翻转。本文用真实数据展示只做转置会得到什么,验证另一种"四个位置循环交换"的原地做法与它结果一致并比较耗时,给出五种语言的实现,并把两步做成分步演示。
完整题目见官方页面(链接在文末)。矩阵是方阵。基准解是 zip(*matrix[::-1]):先上下翻转,再转置,这是一个不原地的、与"转置再左右翻转"不同的组合方式。一共 8 组用例,覆盖:官方示例(3×3 和 4×4)、1×1、2×2、全是 0、5×5、含负数:
输入 [[[1]]] 输出 [[1]] 输入 [[[1,2],[3,4]]] 输出 [[3,1],[4,2]] 输入 [[[1,2,3],[4,5,6],[7,8,9]]] 输出 [[7,4,1],[8,5,2],[9,6,3]] 输入 [[[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]] 输出 [[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]] 输入 [[[0,0],[0,0]]] 输出 [[0,0],[0,0]] 输入 [[[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]]] 输出 [[21,16,11,6,1],[22,17,12,7,2],[23,18,13,8,3],[24,19,14,9,4],[25,20,15,10,5]]
下面在同一个矩阵上对比:只做转置(不翻转)、solve(转置加翻转)、四个位置循环交换。并验证:连续旋转 4 次是否回到原样;随机 3000 个方阵上两种原地做法是否一致;以及各自的耗时:
输入 [[1, 2, 3], [4, 5, 6], [7, 8, 9]] 只转置(不翻转): [[1, 4, 7], [2, 5, 8], [3, 6, 9]] solve(转置 + 每行翻转): [[7, 4, 1], [8, 5, 2], [9, 6, 3]] 四个位置循环交换: [[7, 4, 1], [8, 5, 2], [9, 6, 3]] 随机 3000 个方阵,两种原地做法结果一致:True 连续旋转 4 次回到原样:True 耗时(每个规模取三次中最快):转置 + 翻转 vs 四个位置循环交换 n= 200: 转置+翻转 0.59 ms 四位置循环 1.25 ms n= 400: 转置+翻转 3.05 ms 四位置循环 7.58 ms n= 800: 转置+翻转 12.86 ms 四位置循环 37.16 ms
只做转置得到的是沿主对角线的镜像 [[1, 4, 7], [2, 5, 8], [3, 6, 9]],不是旋转,需要再把每行翻转才是顺时针旋转 90° 的结果 [[7, 4, 1], [8, 5, 2], [9, 6, 3]]。四个位置循环交换的做法:每次把 (i, j)、(j, n-1-i)、(n-1-i, n-1-j)、(n-1-j, i) 这四个位置循环换一遍,只遍历矩阵的四分之一,但在 Python 里每次要做四元组的赋值,实测比"转置加翻转"慢(n = 800 时 37.2ms 对 12.9ms,约 2.9 倍)。随机 3000 个方阵上两种做法结果一致,连续旋转 4 次也确实回到了原样。
转置:对每个 i 和 j > i,交换 matrix[i][j] 和 matrix[j][i]。翻转:每一行原地左右翻转。顺时针旋转 90° 等价于这两步的组合。Python、Java、Go、JavaScript、C++ 都是真实编译运行的;Java、Go、JavaScript、C++ 都是真正原地修改,不额外分配矩阵。
Python · 解法代码def solve(matrix): n = len(matrix) for i in range(n): # 第一步:沿主对角线转置 for j in range(i + 1, n): matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j] for row in matrix: # 第二步:每一行左右翻转 row.reverse() return matrix
$ python3 solution.py # 统一入口:逐行读取 cases.jsonl,调用上面的 solve,打印 JSON [[[1]]] → [[1]] [[[1,2],[3,4]]] → [[3,1],[4,2]] [[[1,2,3],[4,5,6],[7,8,9]]] → [[7,4,1],[8,5,2],[9,6,3]] [[[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]] → [[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]] [[[0,0],[0,0]]] → [[0,0],[0,0]] [[[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]]] → [[21,16,11,6,1],[22,17,12,7,2],[23,18,13,8,3],[24,19,14,9,4],[25,20,15,10,5]]
Java · 解法代码class Solution { public void rotate(int[][] matrix) { int n = matrix.length; for (int i = 0; i < n; i++) { // 第一步:沿主对角线转置 for (int j = i + 1; j < n; j++) { int t = matrix[i][j]; matrix[i][j] = matrix[j][i]; matrix[j][i] = t; } } for (int[] row : matrix) { // 第二步:每一行左右翻转 for (int l = 0, r = n - 1; l < r; l++, r--) { int t = row[l]; row[l] = row[r]; row[r] = t; } } } }
$ javac -d build Main.java Solution.java && java -cp build Main < cases.jsonl [[[1]]] → [[1]] [[[1,2],[3,4]]] → [[3,1],[4,2]] [[[1,2,3],[4,5,6],[7,8,9]]] → [[7,4,1],[8,5,2],[9,6,3]] [[[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]] → [[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]] [[[0,0],[0,0]]] → [[0,0],[0,0]] [[[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]]] → [[21,16,11,6,1],[22,17,12,7,2],[23,18,13,8,3],[24,19,14,9,4],[25,20,15,10,5]]
Go · 解法代码func rotate(matrix [][]int) { n := len(matrix) for i := 0; i < n; i++ { // 第一步:沿主对角线转置 for j := i + 1; j < n; j++ { matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j] } } for _, row := range matrix { // 第二步:每一行左右翻转 for l, r := 0, n-1; l < r; l, r = l+1, r-1 { row[l], row[r] = row[r], row[l] } } }
$ go build -o bin . && ./bin < cases.jsonl [[[1]]] → [[1]] [[[1,2],[3,4]]] → [[3,1],[4,2]] [[[1,2,3],[4,5,6],[7,8,9]]] → [[7,4,1],[8,5,2],[9,6,3]] [[[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]] → [[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]] [[[0,0],[0,0]]] → [[0,0],[0,0]] [[[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]]] → [[21,16,11,6,1],[22,17,12,7,2],[23,18,13,8,3],[24,19,14,9,4],[25,20,15,10,5]]
JavaScript · 解法代码function rotate(matrix) { const n = matrix.length; for (let i = 0; i < n; i++) { // 第一步:沿主对角线转置 for (let j = i + 1; j < n; j++) { [matrix[i][j], matrix[j][i]] = [matrix[j][i], matrix[i][j]]; } } for (const row of matrix) row.reverse(); // 第二步:每一行左右翻转 }
$ node run.js < cases.jsonl [[[1]]] → [[1]] [[[1,2],[3,4]]] → [[3,1],[4,2]] [[[1,2,3],[4,5,6],[7,8,9]]] → [[7,4,1],[8,5,2],[9,6,3]] [[[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]] → [[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]] [[[0,0],[0,0]]] → [[0,0],[0,0]] [[[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]]] → [[21,16,11,6,1],[22,17,12,7,2],[23,18,13,8,3],[24,19,14,9,4],[25,20,15,10,5]]
C++ · 解法代码#include <algorithm> #include <vector> using namespace std; class Solution { public: void rotate(vector<vector<int>>& matrix) { int n = matrix.size(); for (int i = 0; i < n; i++) { // 第一步:沿主对角线转置 for (int j = i + 1; j < n; j++) swap(matrix[i][j], matrix[j][i]); } for (auto& row : matrix) reverse(row.begin(), row.end()); // 第二步:每一行左右翻转 } };
$ clang++ -std=c++17 -O2 -o bin sol.cpp && ./bin < cases.jsonl [[[1]]] → [[1]] [[[1,2],[3,4]]] → [[3,1],[4,2]] [[[1,2,3],[4,5,6],[7,8,9]]] → [[7,4,1],[8,5,2],[9,6,3]] [[[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]] → [[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]] [[[0,0],[0,0]]] → [[0,0],[0,0]] [[[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]]] → [[21,16,11,6,1],[22,17,12,7,2],[23,18,13,8,3],[24,19,14,9,4],[25,20,15,10,5]]
共 8 组用例(含边界),以暴力解的结果为基准逐组比对: Python 通过 8/8 Java 通过 8/8 Go 通过 8/8 JavaScript 通过 8/8 C++ 通过 8/8
边长 n 分别为 100、200、400、800(元素个数是 n²),每个规模取三次中最快的一次,基准解是 zip 版本(会新建一个矩阵):
边长 n | 暴力解 | 本文解法
100 | 0.063 ms | 0.148 ms
200 | 0.591 ms | 0.606 ms
400 | 1.718 ms | 2.946 ms
800 | 9.717 ms | 12.701 ms两种做法都是 O(n²),边长每翻一倍,理论上耗时增长 4 倍:原地版本实测 4.1 倍、4.9 倍、4.3 倍,比较接近;zip 版本 9.4 倍、2.9 倍、5.7 倍,波动比较大,小规模上计时的噪声占比更高。这里 zip 版本(C 层循环)与原地版本(Python 层循环)耗时接近,并不能说明原地更慢或更快,原地的价值是不需要额外的 O(n²) 空间,这里没有测量内存。
Python 版本另外和 zip 版本做了随机对拍,边长 1 到 8,元素取 -50 到 50:
随机对拍 5000 组(边长 1 到 8 的随机方阵,元素取 -50 到 50),solve 与 brute 的结果全部一致。
另外四种语言只在上面 8 组固定用例上验证过,没有做大规模随机对拍。
下面的演示用 [[1, 2, 3], [4, 5, 6], [7, 8, 9]]。每一帧都是把上面的解法在这组输入上真实执行记录下来的,运行器还检查了演示的最终矩阵与 solve 的返回值一致。转置阶段每一步交换一对关于主对角线对称的格子(蓝色),翻转阶段绿色的部分是已经翻转完成的行。