想系统提升编程能力、查看更完整的学习路线,欢迎访问 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度可以拆分为两个原地操作的组合:
- 转置:沿主对角线(左上到右下)翻转 → matrix[i][j] 与 matrix[j][i] 交换
- 水平翻转:每行左右反转 → 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)满足原地旋转的要求
- 代码简洁清晰,分为两个独立步骤,易于理解和调试
- 思路直观:旋转 = 转置 + 翻转,容易记忆和复现
面试建议:
- 先用30秒口述暴力法思路(辅助矩阵),展示理解坐标变换
- 立即优化到🏆最优解(转置+翻转),强调"拆分为简单操作组合"的优化思想
- 重点讲解最优解的核心思想:"复杂变换 = 简单操作的组合,每个操作原地完成"
- 如果面试官感兴趣,可以补充解法三展示更深入的思考
- 手动测试边界用例(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()
易错点 ⚠️
-
转置时重复交换:
- 错误:
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)(只遍历上三角)
- 错误:
-
混淆转置的对角线方向:
- 主对角线(左上到右下):(i,j)↔(j,i)
- 副对角线(右上到左下):(i,j)↔(n-1-j, n-1-i)
-
原地修改引用问题:
- 错误:
matrix = [[...]](改变了局部变量引用) - 正确:
matrix[:] = [[...]](原地修改切片)
- 错误:
🏗️ 工程实战(选读)
这个算法思想在真实项目中的应用,让你知道"学了有什么用"。
-
场景1:图像处理 — 旋转照片,裁剪视频帧
- OpenCV、PIL等库都实现了矩阵旋转
- 游戏开发中旋转贴图、精灵(Sprite)
-
场景2:数据可视化 — 旋转热力图、矩阵图
- Matplotlib、Seaborn绘图时调整坐标轴方向
-
场景3:机器学习 — 数据增强(Data Augmentation)
- 旋转训练图像增加样本多样性
- 卷积神经网络(CNN)中旋转不变性
🏋️ 举一反三
完成本课后,试试这些同类题目来巩固知识:
| 题目 | 难度 | 相关知识点 | 提示 |
|---|---|---|---|
| LeetCode 54. 螺旋矩阵 | Medium | 矩阵遍历 | 按顺时针方向逐层遍历 |
| LeetCode 59. 螺旋矩阵 II | Medium | 矩阵构造 | 逆向思维:逐层填充 |
| 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 学习资料都在这里,后续复习和拓展会更省时间。