ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

旋转图像原地算法:从坐标映射到三种解法,掌握LeetCode 48

旋转图像原地算法:从坐标映射到三种解法,掌握LeetCode 48 Hot100 刷到第 16 题正好是 48. 旋转图像。这道题的题干短得离谱给你一个 n x n 的二维矩阵要求原地顺时针旋转 90 度。我第一次刷到它的时候觉得不过如此不就是行列互换再加个倒序吗结果真动手写原地版本的时候边界条件硬是让我反工了两次。网上题解很多但大多只丢一段 Python 代码最多补两句注释这篇想把从坐标映射到三种解法的推导过程完整写一遍顺便把我在本地调试时踩过的坑记下来。不管你是正在按 Hot100 题单刷题的人还是准备面试想突击矩阵类题或者纯粹对图像旋转背后的代码实现感兴趣这篇都能给你一套可以直接照着写的东西。1. 这题到底在考什么先看清楚问题本身1.1 题目要求与输入输出题目本身完全是“电梯面试”风格输入一个n x n的二维矩阵n范围为 1 到 20要求把矩阵顺时针旋转 90 度必须在原矩阵上直接修改不能额外开一个矩阵来接收结果标准示例是输入matrix [[1,2,3],[4,5,6],[7,8,9]] 输出[[7,4,1],[8,5,2],[9,6,3]]很多人第一眼看到这个输出会总结出一个“感觉”旋转后的第一行就是原矩阵第一列从下往上读。确实从元素位置看旋转后的第j行是从原矩阵第j列自底向上取数据填出来的。但如果你只是记住这个规律真正手写循环时很容易栽在坐标变换上。这道题真正考的不是“你会不会旋转”而是你对二维数组下标变换的敏感度以及在 O(1) 额外空间的限制下怎么通过交换元素完成任务。用一句话概括考点你能不能把一个坐标映射关系转化成一段没有边界 bug 的原地交换代码。1.2 为什么 Hot100 一定要收它Hot100 不是简单按难度排的题单它收的题都是面试里反复出现的“母题”。旋转图像就是典型代表因为它小巧但信息密度很高它考二维数组遍历但又不止于遍历它考原地操作但比反转链表那种明显交换更隐蔽它考数学映射但你不需要推出很复杂的公式只需要算清楚一个格子的来龙去脉面试官很喜欢在这道题后面接 follow-up改成逆时针呢旋转两次呢给你一个m x n的矩形而不是正方形呢这些追问本质上都是在同一个坐标映射上做文章。另外在实际工程里二维矩阵旋转也不是什么冷门操作。图像处理里最常见的像素旋转就是把图片当作 RGB 通道矩阵来做游戏里的地图旋转、棋盘状态轮换底层也是同样逻辑。所以这道题刷明白后面的螺旋矩阵、矩阵置零、转置矩阵甚至简单图像数据增强的代码都会顺手很多。1.3 坐标映射旋转背后的那一眼公式所有解法都围绕一个核心问题旧位置的元素旋转后去了哪个新位置或者反过来新位置的元素是从旧位置的哪里搬来的以顺时针旋转 90 度为例假设旧矩阵中某个位置是(i, j)i是行j是列那么它旋转后会落在新矩阵的(j, n - 1 - i)位置。拆开看其实不玄原本在第i行旋转后行坐标变成了列坐标j原本在第j列旋转后列坐标变成了n - 1 - i也就是从右边数过去的第i列拿 3 阶矩阵的四个角验证一下非常直观原位置新位置(0,0) 左上角(0,2) 右上角(0,2) 右上角(2,2) 右下角(2,2) 右下角(2,0) 左下角(2,0) 左下角(0,0) 左上角四个角正好沿着顺时针方向转了一圈。这也是“辅助矩阵解法”和“原地四元素轮换解法”共同的依据。如果你更喜欢“新位置来自哪里”的写法也可以记成new[i][j] old[n - 1 - j][i]这两个公式本质是一回事只是遍历基准不同。我个人习惯用“旧位置去往新位置(j, n-1-i)”作为推导起点因为后面写四元素轮换时顺着这个方向不容易绕晕。2. 先用辅助矩阵把问题“算对”2.1 辅助矩阵解法到底怎么写最优解要求原地但第一次接触这道题时我建议先老老实实写一个辅助矩阵版本把坐标关系验证一遍。这不仅帮助理解题意也能在面试时先给面试官展示“我意识到了坐标映射然后再优化成原地”。辅助矩阵的思路非常简单新建一个同样大小的矩阵遍历原矩阵的每个位置(i, j)把元素放到新矩阵的(j, n - 1 - i)位置最后再把新矩阵内容拷贝回原矩阵。def rotate_with_extra_space(matrix): n len(matrix) tmp [[0] * n for _ in range(n)] for i in range(n): for j in range(n): tmp[j][n - 1 - i] matrix[i][j] matrix[:] tmp注意最后一行我写的是matrix[:] tmp不是matrix tmp。这是一个非常经典的 Python 坑matrix tmp只是把局部变量重新绑定到新列表上调用方手里的原矩阵并不会改变而matrix[:] tmp是把tmp的元素原地写入matrix指向的列表函数外才能看到变化。如果你把题目改成“返回一个新矩阵”那直接return tmp就行但 LeetCode 这道题明确要求原地修改所以必须写回。2.2 为什么面试官不让你用辅助矩阵辅助矩阵的代码这么直白为什么不能直接交因为原题限制了 O(1) 额外空间而且在实际场景中这个限制非常合理。想象一下真实图像处理一张 4000 x 4000 的图片像素矩阵用辅助矩阵意味着你要新开一个容量为 1600 万个元素的内存块。假如每个像素是 3 通道的 RGB 值内存直接翻倍甚至更多。在这种场景下能够原地旋转不只是“算法技巧”而是实打实的资源节省。另外面试官看这道题通常也不是真的在意你多开了一个n x n的二维数组毕竟n 20时那点空间微不足道。他在意的是你有没有原地操作的意识以及能不能用交换、反转等操作完成同样的效果。辅助矩阵版本的最大价值是“验算”当你写原地版本没把握时可以先跑一遍辅助版本把结果当作 ground truth 来对照。3. 原地解法一先转置再水平翻转3.1 为什么转置之后水平翻转刚好是顺时针 90 度这是我最推荐记住的解法因为逻辑链条非常顺写起来也不容易错。先回顾转置操作矩阵转置就是把A[i][j]和A[j][i]交换相当于沿主对角线左上到右下翻折。做完转置后再对每一行做水平翻转也就是沿垂直中线镜像。这两步叠加的效果恰好就是顺时针旋转 90 度。用公式验证一遍转置后B[i][j] A[j][i]水平翻转后C[i][j] B[i][n - 1 - j]代入得C[i][j] A[n - 1 - j][i]而上一节说过顺时针旋转 90 度的位置映射正是new[i][j] old[n - 1 - j][i]。公式完全吻合。我举个例子3 阶矩阵1 2 3 1 4 7 7 4 1 4 5 6 - 2 5 8 - 8 5 2 7 8 9 3 6 9 9 6 3 原矩阵 转置后 水平翻转后第二步水平翻转后第一行从1 4 7变成7 4 1正好是旋转结果的第二行没问题——最终得到的就是顺时针旋转 90 度后的矩阵。3.2 代码与实现细节def rotate(matrix): n len(matrix) # 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] # 2. 水平翻转对每一行做反转 for i in range(n): matrix[i].reverse()这里有两个细节值得单独说。第一转置的双层循环中内层j一定要从i 1开始。因为j从 0 开始的话(i, j)和(j, i)会被交换两次等于交换了又换回来矩阵完全没变化。主对角线上的元素(i, i)本来就不需要交换。第二水平翻转用matrix[i].reverse()是最省事的写法。如果没有reverse可以自己写双指针for i in range(n): left, right 0, n - 1 while left right: matrix[i][left], matrix[i][right] matrix[i][right], matrix[i][left] left 1 right - 1这两种写法等价但注意不要写matrix[i] matrix[i][::-1]后忘掉写回因为matrix[i][::-1]会生成一个新列表直接赋值给局部行引用同样不会修改原矩阵。时间复杂度是 O(n^2)因为要遍历矩阵一半以上的元素额外空间是 O(1)只用了几个临时变量。3.3 为什么要强调“顺序”转置加水平翻转的顺序不能反。如果你先水平翻转再转置得到的是逆时针旋转 90 度的结果而不是顺时针。验证一下先水平翻转再转置公式变成C[i][j] A[j][n - 1 - i]。上一节我们推导过逆时针旋转 90 度的映射就是new[i][j] old[j][n - 1 - i]。所以这道题如果面试官追加一句“那逆时针呢”你可以直接说“把这两步顺序反过来”也可以说“先转置再垂直翻转”后面扩展部分我会给具体代码。我自己刷题时对这种方法有一种“肌肉记忆”看到旋转 90 度先想转置再想镜像永远不要凭感觉乱猜方向。4. 原地解法二逐层四元素轮换4.1 为什么要学第二种解法转置加水平翻转已经足够应付面试那还有必要学逐层轮换吗有。原因有两个第一这方法完全按“旧位置去往新位置”的映射来交换元素不需要背“转置 镜像”的组合。对一部分人来说这种方法的每一步都更直接容易当场推出来。第二有些面试官会要求“一次遍历完成”或者不让你用reverse又或者追问“在不使用额外 API 的情况下怎么做”。逐层四元素轮换就是纯下标交换一行reverse都不用是最纯粹的原地图谱变换。4.2 从外层到内层每一层做轮换图像旋转是自外向内逐层进行的。最外一圈的元素转完之后往内缩一圈继续旋转直到最中心。对于每一层我们定义四个边界top ibottom n - 1 - ileft iright n - 1 - i在当前层内遍历offset从 0 到right - left - 1也就是除开“最后一个元素”的所有位置。每次处理四个位置左上(top, left offset)右上(top offset, right)右下(bottom, right - offset)左下(bottom - offset, left)顺时针轮换的逻辑是左下搬到左上右下搬到左下右上搬到右下左上搬到右上。为什么是左下搬到左上回到坐标映射旋转后(top, left offset)位置的新值应该来自原矩阵的(bottom - offset, left)也就是左下角网上数offset的位置。以此类推。代码长这样def rotate(matrix): n len(matrix) top, left 0, 0 bottom, right n - 1, n - 1 while top bottom: for offset in range(right - left): # 保存左上角 tmp matrix[top][left offset] # 左下 - 左上 matrix[top][left offset] matrix[bottom - offset][left] # 右下 - 左下 matrix[bottom - offset][left] matrix[bottom][right - offset] # 右上 - 右下 matrix[bottom][right - offset] matrix[top offset][right] # 左上 - 右上 matrix[top offset][right] tmp top 1 left 1 bottom - 1 right - 1我用while top bottom来控制层数而不是for i in range(n // 2)本质上等价。n4时有两层n3时只有一层最中心的元素n为奇数时那个单独格子不需要移动。内层循环写range(right - left)而不是range(n - 1 - i)是因为right - left表示的正是当前层的边长减一。比如最外层right - left n - 1要处理从 0 到n - 2共n - 1个 offset。最后一个元素对角位置的格子不能处理因为处理到 offset n-1 时四个位置会重叠回同一个元素等于白转还可能出错。4.3 两种原地解法的对比对比维度转置 水平翻转逐层四元素轮换代码长度很短两个循环略长需要维护四边界出错点转置内层j起点、是否写回offset 范围、四条赋值顺序可扩展性容易改逆时针换顺序或换翻转方向直接控制方向适合当数学推导面试友好度高容易解释高但需要现场画图额外空间O(1)O(1)时间复杂度O(n^2)O(n^2)我个人建议两种都写一遍。转置法更适合“快速 AC”逐层四元素轮换更适合你真正理解坐标映射。如果你时间紧优先吃透转置法如果还有余力四元素轮换能帮你在将来处理其他矩阵变换题目时少走弯路。5. 现场调试边界条件到底怎么写5.1 我在本地踩过的坑刷题最怕的不是不会做而是会做但栽在一些莫名其妙的边界条件上。旋转图像这道题我总结过几个高频坑基本覆盖了 90% 的翻车现场。第一个坑是转置循环的边界。把内层j写成range(n)之后矩阵交换了两次最后输出原样。这种 bug 隐蔽得很因为矩阵“看起来没错”一旦用非对称数据测试立刻露馅。第二个坑是水平翻转用list(reversed(matrix[i]))但忘写回。reversed()返回迭代器直接matrix[i] reversed(matrix[i])会把矩阵行变成一个无法访问下标的 reverse 迭代器程序当场报错。matrix[i][::-1]同理新建列表后必须重新赋值回去而且要确保这个赋值真的执行了。第三个坑是四元素轮换的 offset 范围。我一开始写for offset in range(n - 1 - i)结果当层数缩进的时候i和外层边界对不上最内层多转了一次。后来改成直接以right - left作为当前层长度逻辑才稳定下来。第四个坑是 Python 的函数参数问题。很多人会把辅助矩阵解法写成matrix tmp然后发现 LeetCode 输出还是原矩阵然后开始怀疑人生。记住我在辅助矩阵那一节强调的原地修改列表用matrix[:] tmp不是重新绑定名字。第五个坑是把 n 1 的情况忘掉。n 1时矩阵只有一个元素任何旋转都不影响它。转置法没问题四元素轮换的while top bottom循环直接不进入也没问题。但如果你在四元素轮换里写while top bottomoffset范围是 0代码虽然不会报错但多一层没意义的逻辑没有必要。5.2 一套可以直接照抄的最小测试集写算法题的调试建议是不要只测试题目给的示例要自己构造 1 阶、2 阶、3 阶、4 阶矩阵特别关注非对称数字。这样坐标有没有搞错一眼就能看出来。下面是我常用的测试用例n输入期望输出验证重点1[[5]][[5]]单元素边界2[[1,2],[3,4]][[3,1],[4,2]]最小非平凡矩阵3[[1,2,3],[4,5,6],[7,8,9]][[7,4,1],[8,5,2],[9,6,3]]奇数阶中心点4[[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]][[13,9,5,1],[14,10,6,2],[15,11,7,3],[16,12,8,4]]多层边界我自己调试时会在本地写一个print_matrix函数每转完一层就打印一次。别嫌土矩阵题最怕的就是“脑子觉得对实际全错”打印出来看角点位置比冥想高效得多。如果不想手动看输出可以写一段断言自动化验证def test_rotate(): cases [ ([[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]]), ] for inp, expected in cases: rotate(inp) assert inp expected, fexpected {expected}, got {inp} print(all passed)这段代码会直接告诉你哪一步写歪了反复擦掉重画的时间都能省下来。5.3 原地性怎么自查面试时如果被追问“你怎么证明你是原地操作”最简单的回答是新解法里除了交换用的临时变量没有新建长度和输入规模相关的数组额外空间复杂度 O(1)。在 Python 里你可以用id(matrix)来观察原地旋转前后matrix指向的列表对象地址应该不变。如果你写出matrix new_matrix地址会变说明没有原地。配合这个观察写完之后跑一次心里会更有底。6. 改变方向与周边扩展6.1 逆时针 90 度怎么写面试里最常见的 follow-up 就是逆时针。学会顺时针之后逆时针不用背代码直接用同一个坐标映射推。逆时针旋转 90 度的旧位置映射是new[i][j] old[j][n - 1 - i]。对应到操作组合可以先转置再垂直翻转上下翻转也就是沿水平中线对折。def rotate_counterclockwise(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 i in range(n // 2): matrix[i], matrix[n - 1 - i] matrix[n - 1 - i], matrix[i]验证一下 3 阶矩阵先转置得到[[1,4,7],[2,5,8],[3,6,9]]再上下翻转得到[[3,6,9],[2,5,8],[1,4,7]]正是逆时针旋转 90 度的结果。如果不想记“先转置再垂直翻转”也可以记“先水平翻转再转置”两者结果一样。我推荐记前者因为和顺时针版本“转置 垂直/水平翻转”是一套对称记忆。6.2 旋转任意次数呢如果题目改成“将矩阵旋转 k 次”直接用循环调旋转函数就行但要先做k % 4因为旋转 4 次回到原状。旋转次数多的话不取模会做大量无意义操作。def rotate_k(matrix, k): k % 4 for _ in range(k): rotate(matrix) # 顺时针旋转 90 度如果k 2也可以手动做两次转置翻转或者直接先上下翻转再左右翻转效果等价于旋转 180 度。展开写是# 旋转 180 度 matrix.reverse() # 上下翻转 for row in matrix: row.reverse() # 左右翻转这种扩展题真正训练的是“旋转次数取模”的思维以及知道旋转 180 度可以拆成两个方向的镜像组合。6.3 矩阵旋转在刷题里的亲戚做完这一题可以顺手把这几道题拉出来一起刷它们共享同一套“坐标映射 边界控制”的底层能力螺旋矩阵同样是二维矩阵的边界模拟只是变成了蛇形读取螺旋矩阵 II反过来按螺旋顺序往矩阵里填数转置矩阵旋转 90 度的基础操作先搞清楚转置再搞旋转会顺很多矩阵置零看起来和旋转无关但处理行、列坐标映射时的细心程度完全一样我在刷完旋转图像后的第二天做了螺旋矩阵明显感觉边界条件敏感度上去了。这就是 Hot100 题单有意思的地方前 20 题做完后面的题很多都能套上相似的脑回路。6.4 刷题节奏上的小建议如果你现在也卡在 Hot100 前几十题我的体会是矩阵类题目不要硬背代码先画 3 阶或 4 阶的示意图手动标出几个关键位置的坐标变化再落到代码。旋转图像就是最好的“入门仪式”它逼你把“直觉能看懂”变成“公式能写对”。而且这种矩阵模拟能力在周赛里也很吃香。我印象里 LeetCode 周赛 430 前后就有一轮矩阵相关的题目思路和这道旋转图像非常接近都是把位置变换先写成坐标公式再去实现。先把 48 题吃透后面遇到任何“对矩阵做某种变换”的题你至少有了一个稳定的推导模板先定义旧位置到新位置的映射再决定要不要原地最后小心处理边界。我个人实际操作中的一个小习惯是每次写完旋转类题目都会自己追加一问——“如果是逆时针呢”“如果只旋转最外圈呢”“如果 n 是奇数呢”。多问自己几个变体之后再回到原题会发现原来熟记的那些代码已经没那么重要了真正留在脑子里的是一条清晰的坐标映射链。这道题刷完你对二维数组的掌控力基本就上一个台阶了。
返回列表