NO.48 旋转图像 —— LeetCode 热题 100 面试导向深度解析

54 阅读6分钟

题目

给定一个 ( n \times n ) 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。

必须在原地旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要使用另一个矩阵来旋转图像。

示例 1:

{DCA58BC2-DCF7-480C-8CAF-0E1D5DFBAE53}.png

输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[[7,4,1],[8,5,2],[9,6,3]]

示例 2:

{FA900CB9-F574-4464-84C1-35A05A0A2C56}.png

输入:matrix = [[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]]

一、读题

一句话复述:将一个 ( n \times n ) 的方阵原地顺时针旋转 90°,不能借助额外矩阵。

关键约束高亮

  • ( n \times n ) 方阵(行数 = 列数),这是旋转能简化的前提;
  • 原地修改Do not return anything),意味着空间复杂度必须为 ( O(1) )(不计递归栈);
  • 矩阵元素类型为整数,无特殊范围限制。

二、前置知识

序号知识点说明
1矩阵转置沿主对角线(左上→右下)翻转,即 matrix[i][j]matrix[j][i]
2水平翻转(行内逆序)每行左右对称交换,即 matrix[i][j]matrix[i][n-1-j]
3原地交换技巧利用 ES6 解构赋值 [a, b] = [b, a] 无需临时变量,简洁且安全

非常见 API 表格

API作用参数返回值使用场景
Math.floor(n / 2)向下取整数值整数行内翻转时确定只需遍历前半列
解构赋值 [a, b] = [b, a]交换两个变量值左右均为数组/类数组undefined原地交换矩阵元素,无需 temp

三、思路推导(核心)

暴力解(不满足题意)

最容易想到的是新建矩阵:观察可知,旋转后 new[j][n-1-i] = old[i][j],即第 i 行第 j 列的元素会跑到第 j 行倒数第 i 列。

var rotate = function(matrix) {
    const n = matrix.length;
    const newMatrix = Array.from({ length: n }, () => Array(n).fill(0));
    for (let i = 0; i < n; i++) {
        for (let j = 0; j < n; j++) {
            newMatrix[j][n - 1 - i] = matrix[i][j];
        }
    }
    // 拷贝回去(但这不算严格原地,且额外 O(n^2) 空间)
    for (let i = 0; i < n; i++) {
        for (let j = 0; j < n; j++) {
            matrix[i][j] = newMatrix[i][j];
        }
    }
};
  • 时间复杂度:( O(n^2) )(两轮遍历,常数倍忽略);
  • 空间复杂度:( O(n^2) )(额外矩阵)。

代入最大数据量(LeetCode 通常 ( n \le 20 ) 或更大),若 ( n=1000 ),则额外需要 100 万个元素的存储空间,内存可能不足,且违背题意


优化动机 —— 为什么能想到"转置 + 水平翻转"?

思维演进路径

  1. 观察旋转前后坐标变化(i, j)(j, n-1-i)
  2. 拆解这个变换:能否分解为两个更简单的操作?
    • 先做转置(i, j)(j, i)
    • 再做水平翻转(j, i)(j, n-1-i)
    • 两步合起来正好是 (i, j)(j, n-1-i)
  3. 为什么这样想:因为转置和翻转都是原地可完成的操作,且逻辑简单——转置只需遍历对角线一侧,翻转只需每行对折交换。这种"将复杂变换拆成两个已知简单操作"是算法设计的常见套路。

核心几何直觉:顺时针旋转 90° = 沿主对角线翻折 + 左右翻折(或者先上下翻折再转置,两种等价)。


手把手模拟(以 3×3 为例)

矩阵初始:

步骤操作矩阵状态
0原始[[1,2,3],[4,5,6],[7,8,9]]
1转置:交换 (0,1)(1,0)[[1,4,3],[2,5,6],[7,8,9]]
2转置:交换 (0,2)(2,0)[[1,4,7],[2,5,6],[3,8,9]]
3转置:交换 (1,2)(2,1)[[1,4,7],[2,5,8],[3,6,9]]
4水平翻转第 0 行:交换 (0,0)(0,2)[[7,4,1],[2,5,8],[3,6,9]]
5水平翻转第 1 行:交换 (1,0)(1,2)[[7,4,1],[8,5,2],[3,6,9]]
6水平翻转第 2 行:交换 (2,0)(2,2)[[7,4,1],[8,5,2],[9,6,3]] ✅

四、代码与避坑

完整代码

/**
 * @param {number[][]} matrix
 * @return {void} Do not return anything, modify matrix in-place instead.
 */
var rotate = function(matrix) {
    const n = matrix.length;

    // 1. 转置(沿主对角线翻转)
    // 为什么 j 从 i+1 开始?因为只需遍历上三角,避免重复交换
    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]];
        }
    }

    // 2. 每行左右翻转
    // 为什么 j < Math.floor(n / 2)?因为只需遍历每行前半部分,避免重复交换
    for (let i = 0; i < n; i++) {
        for (let j = 0; j < Math.floor(n / 2); j++) {
            [matrix[i][j], matrix[i][n - 1 - j]] = [matrix[i][n - 1 - j], matrix[i][j]];
        }
    }
};

边界攻防表

边界场景❌ 错误写法✅ 正确写法原因
n = 1(单元素矩阵)内外层循环正常执行,j < n 进入转置,j < Math.floor(n/2) 不执行转置时 j = i+1 = 1,内层不执行;水平翻转 j < 0 不执行对角线无元素可交换,行内无需翻转
n = 2(最小非平凡)转置时 ji+1 开始,正确交换一次;水平翻转 j < 1 交换一次正确注意双重循环的边界条件
偶数 n(如 n=4)水平翻转时 j < n/2j < 2,交换 (0,3) 和 (1,2),正确覆盖全部正确偶数行中心是两个元素,需交换 n/2 对

五、举一反三

面试追问

  1. 逆时针旋转 90° 怎么做?
    → 先转置,再上下翻转(每列上下交换),即 (i,j)(n-1-j, i)

  2. 旋转 180° 怎么做?
    → 先上下翻转,再左右翻转;或者直接 (i,j)(n-1-i, n-1-j) 原地交换,只需遍历四分之一矩阵。

  3. 如果矩阵不是方阵(( m \times n )),能否原地旋转?
    → 不能严格原地,因为旋转后维度变为 ( n \times m ),形状改变,必须借助新矩阵。

  4. 如果要求旋转后输出新的矩阵,但原矩阵保留(非原地)?
    → 直接用暴力解的新建矩阵即可,代码更简单,无需转置翻转。

相似题型

题目差异点解法变化
LeetCode 54. 螺旋矩阵按螺旋顺序遍历输出,不需要修改原矩阵用四个边界指针收缩,属于模拟题
LeetCode 59. 螺旋矩阵 II生成一个 ( n \times n ) 的螺旋矩阵按顺序填充数字,同样用边界指针
剑指 Offer 29. 顺时针打印矩阵同 54 题,但要求返回一维数组边界模拟,注意空矩阵处理

核心思路迁移:矩阵变换类问题,优先考虑"分解为转置 + 翻转"的组合,因为这两种操作原地成本低,易于实现。


一句话总结:顺时针旋转 90° = 转置 + 水平翻转,原地完成,时间复杂度 ( O(n^2) ),空间复杂度 ( O(1) )。遇到类似矩阵变换,先想能否拆解为简单操作的组合。