📖 第106课:旋转图像

4 阅读15分钟

想系统提升编程能力、查看更完整的学习路线,欢迎访问 AI Compass:github.com/tingaicompa… 仓库持续更新刷题题解、Python 基础和 AI 实战内容,适合想高效进阶的你。

📖 第106课:旋转图像

模块:高级技巧 | 难度:Medium ⭐⭐ LeetCode 链接:leetcode.cn/problems/ro… 前置知识:无 预计学习时间:25分钟


🎯 题目描述

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

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

示例:

输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[[7,4,1],[8,5,2],[9,6,3]]
解释:
原矩阵:     旋转90度后:
1 2 3       7 4 1
4 5 6  -->  8 5 2
7 8 9       9 6 3

约束条件:

  • n == matrix.length == matrix[i].length
  • 1 <= n <= 20
  • -1000 <= matrix[i][j] <= 1000
  • 必须原地旋转,空间复杂度 O(1)

🧪 边界用例(面试必考)

用例类型输入期望输出考察点
最小输入[[1]][[1]]1x1矩阵
2x2矩阵[[1,2],[3,4]][[3,1],[4,2]]基本旋转
3x3矩阵[[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]]旋转后处理负数

💡 思路引导

生活化比喻

想象你在整理一叠扑克牌,要把它们顺时针旋转90度摆放到桌面上。

🐌 笨办法:拿一张新桌子,一张一张地按照旋转后的位置放好。但这需要额外的空间,而且浪费时间。

🚀 聪明办法:你发现一个规律——先把牌按对角线翻折(就像翻书一样),然后再把每行牌从左到右反转(就像镜像翻转)。这样只需要在原桌面上操作,不需要额外空间!

关键洞察: 观察旋转规律,顺时针旋转90度 = 先转置(沿对角线翻转) + 再水平翻转(每行反转)

关键洞察

矩阵旋转的数学规律:matrix[i][j] 旋转后到 matrix[j][n-1-i]


🧠 解题思维链

这一节模拟你在面试中"从零开始思考"的过程。

Step 1:理解题目 → 锁定输入输出

  • 输入:n×n 的二维矩阵(方阵)
  • 输出:原地修改,无返回值
  • 限制:必须原地操作,空间复杂度 O(1),不能创建新矩阵

Step 2:先想笨办法(暴力法)

最直接的思路是创建一个新矩阵 result,然后按照旋转规律:

  • 原矩阵第 i 行第 j 列 → 新矩阵第 j 行第 (n-1-i) 列
  • 时间复杂度:O(n²) — 遍历所有元素
  • 瓶颈在哪:需要 O(n²) 的额外空间,违反了原地旋转的要求

Step 3:瓶颈分析 → 优化方向

  • 核心问题:如何在不使用额外矩阵的情况下完成旋转?
  • 优化思路:寻找旋转的等价操作组合
    • 观察:旋转90度可以拆分为两个简单操作的组合
    • 发现:转置(沿主对角线翻转) + 水平翻转(每行反转) = 旋转90度

Step 4:选择武器

  • 选用:矩阵转置 + 行反转
  • 理由:两个操作都可以原地完成,总空间复杂度 O(1)

🔑 模式识别提示:当题目出现"矩阵旋转/翻转"且要求原地操作,优先考虑"拆分为多个简单变换的组合"


🔑 解法一:辅助矩阵法(直觉法)

思路

先用额外空间理解旋转规律,找到坐标变换公式。

图解过程

原矩阵 matrix[i][j]:
  0 1 2
0 1 2 3
1 4 5 6
2 7 8 9

旋转90度后 result[j][n-1-i]:
  0 1 2
0 7 4 1
1 8 5 2
2 9 6 3

坐标映射规律:
(0,0)→(0,2)  (0,1)→(1,2)  (0,2)→(2,2)
(1,0)→(0,1)  (1,1)→(1,1)  (1,2)→(2,1)
(2,0)→(0,0)  (2,1)→(1,0)  (2,2)→(2,0)

规律: matrix[i][j] → result[j][n-1-i]

Python代码

from typing import List


def rotate_with_extra_space(matrix: List[List[int]]) -> None:
    """
    解法一:辅助矩阵法
    思路:用额外空间按旋转公式逐个复制
    """
    n = len(matrix)
    # 创建辅助矩阵
    result = [[0] * n for _ in range(n)]

    # 按照旋转公式复制
    for i in range(n):
        for j in range(n):
            result[j][n - 1 - i] = matrix[i][j]

    # 拷贝回原矩阵
    for i in range(n):
        for j in range(n):
            matrix[i][j] = result[i][j]


# ✅ 测试
matrix1 = [[1,2,3],[4,5,6],[7,8,9]]
rotate_with_extra_space(matrix1)
print(matrix1)  # 期望输出:[[7,4,1],[8,5,2],[9,6,3]]

matrix2 = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]
rotate_with_extra_space(matrix2)
print(matrix2)  # 期望输出:[[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]]

复杂度分析

  • 时间复杂度:O(n²) — 遍历两次 n×n 矩阵
    • 具体地说:如果 n=10,需要 10×10×2 = 200 次赋值操作
  • 空间复杂度:O(n²) — 需要额外的 n×n 辅助矩阵

优缺点

  • ✅ 思路清晰,容易理解坐标变换规律
  • ❌ 违反了原地旋转的要求,需要 O(n²) 额外空间

🏆 解法二:转置+翻转法(最优解)

优化思路

关键洞察:旋转90度可以拆分为两个原地操作的组合:

  1. 转置:沿主对角线(左上到右下)翻转 → matrix[i][j] 与 matrix[j][i] 交换
  2. 水平翻转:每行左右反转 → matrix[i][j] 与 matrix[i][n-1-j] 交换

💡 关键想法:复杂的旋转 = 简单操作的组合,每个操作都可以原地完成!

图解过程

原矩阵:
1 2 3
4 5 6
7 8 9

Step 1: 转置(沿主对角线翻转)
交换 matrix[i][j] 与 matrix[j][i] (i < j)
  交换(0,1)与(1,0): 2↔4
  交换(0,2)与(2,0): 3↔7
  交换(1,2)与(2,1): 6↔8

1 4 7
2 5 8
3 6 9

Step 2: 水平翻转(每行反转)
每行左右对称交换:
  第0行: 1↔7  → 7 4 1
  第1行: 2↔8  → 8 5 2
  第2行: 3↔9  → 9 6 3

最终结果:
7 4 1
8 5 2
9 6 3  ✅ 完成旋转!

再用第二个示例验证:

原矩阵 2x2:
1 2
3 4

转置后:
1 3
2 4

水平翻转后:
3 1
4 2  ✅ 正确!

Python代码

def rotate(matrix: List[List[int]]) -> None:
    """
    解法二:转置+翻转法(最优解)
    思路:旋转90度 = 转置 + 水平翻转
    """
    n = len(matrix)

    # Step 1: 转置矩阵(沿主对角线翻转)
    for i in range(n):
        for j in range(i + 1, n):  # 只遍历上三角,避免重复交换
            matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]

    # Step 2: 水平翻转(每行左右反转)
    for i in range(n):
        for j in range(n // 2):  # 只遍历左半部分
            matrix[i][j], matrix[i][n - 1 - j] = matrix[i][n - 1 - j], matrix[i][j]


# ✅ 测试
matrix3 = [[1,2,3],[4,5,6],[7,8,9]]
rotate(matrix3)
print(matrix3)  # 期望输出:[[7,4,1],[8,5,2],[9,6,3]]

matrix4 = [[1]]
rotate(matrix4)
print(matrix4)  # 期望输出:[[1]]

matrix5 = [[1,2],[3,4]]
rotate(matrix5)
print(matrix5)  # 期望输出:[[3,1],[4,2]]

复杂度分析

  • 时间复杂度:O(n²) — 转置 O(n²) + 翻转 O(n²) = O(n²)
    • 具体地说:n=10 时,转置约 45 次交换 + 翻转 50 次交换 = 95 次操作
  • 空间复杂度:O(1) — 仅用常数个变量,原地修改

⚡ 解法三:四元素环状交换法(巧妙方法)

优化思路

从另一个角度理解旋转:每次同时移动 4 个元素,形成一个环状交换。

  • 把矩阵分为一层层的"框"
  • 对每一层,同时旋转 4 个对应位置的元素

💡 关键想法:旋转不是一个一个移动,而是 4 个位置同时循环交换!

图解过程

3x3矩阵分层:
外层(红色):  中心(蓝色):
1 2 3        5
4 X 6
7 8 9

对于外层,每次处理4个对应位置:
位置1:(0,0)  位置2:(0,2)  位置3:(2,2)  位置4:(2,0)
  1    →      3    →      9    →      7    →  回到1

具体步骤(以3x3外层为例):
第0轮: (0,0)→(0,2)→(2,2)→(2,0)  即 1→3→9→7
第1轮: (0,1)→(1,2)→(2,1)→(1,0)  即 2→6→8→4

4x4矩阵:
外外层 → 外层 → 内层 → 中心
需要处理 n//2 层

Python代码

def rotate_v3(matrix: List[List[int]]) -> None:
    """
    解法三:四元素环状交换法
    思路:分层处理,每层同时旋转4个对应位置
    """
    n = len(matrix)

    # 处理每一层(从外到内)
    for layer in range(n // 2):
        first = layer
        last = n - 1 - layer

        # 对于当前层,旋转每组4个元素
        for i in range(first, last):
            offset = i - first

            # 保存上边
            top = matrix[first][i]

            # 左边 → 上边
            matrix[first][i] = matrix[last - offset][first]

            # 下边 → 左边
            matrix[last - offset][first] = matrix[last][last - offset]

            # 右边 → 下边
            matrix[last][last - offset] = matrix[i][last]

            # 上边(保存的) → 右边
            matrix[i][last] = top


# ✅ 测试
matrix6 = [[1,2,3],[4,5,6],[7,8,9]]
rotate_v3(matrix6)
print(matrix6)  # 期望输出:[[7,4,1],[8,5,2],[9,6,3]]

复杂度分析

  • 时间复杂度:O(n²) — 每个元素移动一次
  • 空间复杂度:O(1) — 仅用一个临时变量

🐍 Pythonic 写法

利用 Python 的解包和切片特性:

# 方法一:转置 + 翻转的简洁版
def rotate_pythonic(matrix: List[List[int]]) -> None:
    """Pythonic写法:利用zip和切片"""
    # 转置:用zip(*matrix)将行变列
    matrix[:] = zip(*matrix[::-1])
    # 等价于:先逆序行,再转置


# 更清晰的分步版本
def rotate_pythonic_v2(matrix: List[List[int]]) -> None:
    """分步Pythonic写法"""
    # Step 1: 转置
    matrix[:] = [list(row) for row in zip(*matrix)]
    # Step 2: 每行反转
    for row in matrix:
        row.reverse()


# ✅ 测试
matrix7 = [[1,2,3],[4,5,6],[7,8,9]]
rotate_pythonic(matrix7)
print([list(row) for row in matrix7])  # 期望输出:[[7,4,1],[8,5,2],[9,6,3]]

解释:

  • zip(*matrix) 实现转置:将行变列
  • matrix[::-1] 逆序行
  • matrix[:] = ... 原地修改(而非创建新对象)

⚠️ 面试建议:先写清晰版本(解法二)展示思路,再提 Pythonic 写法展示语言功底。 面试官更看重你的思考过程,而非代码行数。


📊 解法对比

维度解法一:辅助矩阵🏆 解法二:转置+翻转(最优)解法三:四元素环状交换
时间复杂度O(n²)O(n²)O(n²)
空间复杂度O(n²)O(1) ← 原地最优O(1)
代码难度简单简单 ← 易理解较难
面试推荐⭐⭐⭐ ← 首选⭐⭐
适用场景理解坐标变换面试首选,通用性强展示算法巧思

为什么解法二是最优解:

  • 时间O(n²)已经是理论最优(每个元素至少访问一次)
  • 空间O(1)满足原地旋转的要求
  • 代码简洁清晰,分为两个独立步骤,易于理解和调试
  • 思路直观:旋转 = 转置 + 翻转,容易记忆和复现

面试建议:

  1. 先用30秒口述暴力法思路(辅助矩阵),展示理解坐标变换
  2. 立即优化到🏆最优解(转置+翻转),强调"拆分为简单操作组合"的优化思想
  3. 重点讲解最优解的核心思想:"复杂变换 = 简单操作的组合,每个操作原地完成"
  4. 如果面试官感兴趣,可以补充解法三展示更深入的思考
  5. 手动测试边界用例(1×1, 2×2),展示对解法的深入理解

🎤 面试现场

模拟面试中的完整对话流程,帮你练习"边想边说"。

面试官:请你解决一下这道题,原地旋转矩阵90度。

:(审题30秒)好的,这道题要求原地旋转一个 n×n 矩阵90度。让我先想一下... 我的第一个想法是创建一个新矩阵,按照坐标变换公式 (i,j) → (j, n-1-i) 复制,时间是 O(n²),但空间也是 O(n²)。 题目要求原地旋转,所以需要优化空间。我发现旋转90度可以拆分为两个原地操作:先转置(沿对角线翻转),再水平翻转(每行反转)。这样时间还是 O(n²),但空间降到 O(1)。

面试官:很好,请写一下代码。

:(边写边说)首先转置矩阵,遍历上三角部分交换 matrix[i][j] 和 matrix[j][i]。然后每行做水平翻转,交换左右对称的元素。两步都是原地操作,不需要额外空间。

面试官:测试一下?

:用示例 [[1,2,3],[4,5,6],[7,8,9]] 走一遍。转置后变成 [[1,4,7],[2,5,8],[3,6,9]],再每行反转得到 [[7,4,1],[8,5,2],[9,6,3]]。再测一个边界 [[1,2],[3,4]],转置成 [[1,3],[2,4]],反转成 [[3,1],[4,2]]。结果正确!

高频追问

追问应答策略
"还有更优解吗?""时间O(n²)已经是最优(每个元素至少访问一次),空间O(1)已满足原地要求。可以用四元素环状交换(解法三)减少常数操作,但时间空间复杂度相同。"
"如果旋转180度呢?""两次90度旋转,或者直接上下翻转+左右翻转。公式:(i,j)→(n-1-i, n-1-j),也是O(1)空间。"
"如果逆时针旋转呢?""逆时针90度 = 先转置 + 垂直翻转(上下反转)。或者顺时针旋转3次。"
"数据量非常大?""如果矩阵太大无法一次加载,可以分块旋转。如果是稀疏矩阵,可以只存非零元素并更新坐标。"

🎓 知识点总结

Python技巧卡片 🐍

# 技巧1:多变量同时交换 — Pythonic的元组解包
a, b = b, a  # 无需临时变量

# 技巧2:二维列表的zip转置 — 行列互换
matrix = [[1,2,3],[4,5,6]]
transposed = list(zip(*matrix))  # [(1,4), (2,5), (3,6)]

# 技巧3:切片反转 — 简洁的反转方式
row[:] = row[::-1]  # 原地反转
matrix[::-1]  # 返回逆序副本

# 技巧4:原地修改二维列表 — 必须用[:]赋值
matrix[:] = new_matrix  # ✅ 原地修改
matrix = new_matrix     # ❌ 只是改变引用

💡 底层原理(选读)

为什么转置+翻转等价于旋转?

从线性代数角度:

  • 转置:矩阵 A → A^T (关于主对角线对称)
  • 水平翻转:矩阵每行逆序 (关于竖直中轴对称)
  • 顺时针90度旋转矩阵 R = [0,-1; 1,0]

组合效果:先转置再水平翻转,坐标变换链: (i,j) --转置--> (j,i) --翻转--> (j, n-1-i) 恰好等于旋转公式!

为什么要原地修改? Python 中函数参数传递的是引用,直接修改 matrix 会影响调用者。 使用 matrix[:] = new_value 是原地修改切片,保持引用不变。

算法模式卡片 📐

  • 模式名称:矩阵变换组合
  • 适用条件:矩阵旋转/翻转问题,要求原地操作
  • 识别关键词:"矩阵旋转"、"原地"、"O(1)空间"
  • 核心思想:复杂变换 = 简单操作的组合
  • 模板代码:
def rotate_90_clockwise(matrix):
    n = len(matrix)
    # Step 1: 转置(沿主对角线)
    for i in range(n):
        for j in range(i + 1, n):
            matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]
    # Step 2: 水平翻转(每行反转)
    for i in range(n):
        matrix[i].reverse()

# 其他变换:
# - 逆时针90度: 转置 + 垂直翻转
# - 旋转180度: 水平翻转 + 垂直翻转
# - 水平翻转: matrix[i].reverse()
# - 垂直翻转: matrix.reverse()

易错点 ⚠️

  1. 转置时重复交换:

    • 错误:for i in range(n): for j in range(n): swap(i,j)
    • 原因:会把元素交换两次,回到原位
    • 正确:for i in range(n): for j in range(i+1, n): swap(i,j) (只遍历上三角)
  2. 混淆转置的对角线方向:

    • 主对角线(左上到右下):(i,j)↔(j,i)
    • 副对角线(右上到左下):(i,j)↔(n-1-j, n-1-i)
  3. 原地修改引用问题:

    • 错误:matrix = [[...]] (改变了局部变量引用)
    • 正确:matrix[:] = [[...]] (原地修改切片)

🏗️ 工程实战(选读)

这个算法思想在真实项目中的应用,让你知道"学了有什么用"。

  • 场景1:图像处理 — 旋转照片,裁剪视频帧

    • OpenCV、PIL等库都实现了矩阵旋转
    • 游戏开发中旋转贴图、精灵(Sprite)
  • 场景2:数据可视化 — 旋转热力图、矩阵图

    • Matplotlib、Seaborn绘图时调整坐标轴方向
  • 场景3:机器学习 — 数据增强(Data Augmentation)

    • 旋转训练图像增加样本多样性
    • 卷积神经网络(CNN)中旋转不变性

🏋️ 举一反三

完成本课后,试试这些同类题目来巩固知识:

题目难度相关知识点提示
LeetCode 54. 螺旋矩阵Medium矩阵遍历按顺时针方向逐层遍历
LeetCode 59. 螺旋矩阵 IIMedium矩阵构造逆向思维:逐层填充
LeetCode 73. 矩阵置零Medium原地修改用首行首列做标记
LeetCode 867. 转置矩阵Easy矩阵转置本题的子问题
LeetCode 566. 重塑矩阵Easy矩阵变换一维化再重组

📝 课后小测

试试这道变体题,不要看答案,自己先想5分钟!

题目:给定一个 m×n 的矩阵,如何将其逆时针旋转90度?

💡 提示(实在想不出来再点开)

逆时针90度 = 先转置 + 再垂直翻转(上下反转)

✅ 参考答案
def rotate_counterclockwise(matrix: List[List[int]]) -> None:
    """逆时针旋转90度"""
    n = len(matrix)

    # Step 1: 转置
    for i in range(n):
        for j in range(i + 1, n):
            matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]

    # Step 2: 垂直翻转(上下反转)
    matrix.reverse()  # 或者 matrix[:] = matrix[::-1]


# 测试
matrix = [[1,2,3],[4,5,6],[7,8,9]]
rotate_counterclockwise(matrix)
print(matrix)  # 输出:[[3,6,9],[2,5,8],[1,4,7]]

核心思路:

  • 顺时针:转置 + 水平翻转(每行反转)
  • 逆时针:转置 + 垂直翻转(上下反转)
  • 两者的区别仅在第二步翻转的方向!

如果这篇内容对你有帮助,推荐收藏 AI Compass:github.com/tingaicompa… 更多系统化题解、编程基础和 AI 学习资料都在这里,后续复习和拓展会更省时间。