ARTICLE DETAIL

资讯详情

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

旋转图像LeetCode 48:矩阵原地旋转的坐标公式与两种实现

旋转图像LeetCode 48:矩阵原地旋转的坐标公式与两种实现 力扣hot100第20题旋转图像也就是LeetCode 48题是刷题清单里绕不开的经典矩阵题。题目要求把一个n×n的二维矩阵原地顺时针旋转90度不能另开一个数组存结果。很多第一次刷这题的人都有同感看着简单真写起来却容易出错尤其是对“左上角的值被覆盖后去哪了”这种细节没有把握。我当初在力扣热题100里刷到这题时第一反应是“重新开个矩阵往里填不就行了吗”但题目明确要求原地修改这直接逼着你把旋转从“读写”问题变成“置换”问题。这篇文章把两种主流做法、坐标公式、常见坑都串一遍适合正在刷hot100、准备机试和面试的朋友。1. 读懂题意旋转90度到底做了什么1.1 题目要求与示例拆解题目输入是一个n×n的二维矩阵输出是顺时针旋转90度后的同一个矩阵。以三阶矩阵为例输入1 2 3 4 5 6 7 8 9输出7 4 1 8 5 2 9 6 3逐个数地看你会发现一个非常朴素的对应关系原来的第一行变成了新矩阵的最后一列并且顺序是从下往上读。即原矩阵的1跑到了新矩阵的右上角2跑到了右边中间3跑到了右下角。再往深一层想其实每个元素的位置变化都满足一个统一的坐标规则原矩阵中A[i][j]这个元素旋转后应该出现在新矩阵的B[j][n-1-i]位置。这个公式是整个题目的核心。不管用哪种解法本质上都是在实现这个坐标映射。理解了这个后面看转置加翻转、逐层交换才不会被各种下标绕晕。1.2 从坐标变换理解旋转的本质为什么“转置加水平翻转”能等价于顺时针旋转90度这是这道题最值得搞懂的一点。矩阵转置的意思是把A[i][j]和A[j][i]互换也就是沿主对角线镜像。转置后原位置(i,j)的元素会跑到T[j][i]。水平翻转的意思是把每一行左右颠倒元素(i,j)会跑到(i, n-1-j)。现在把两个操作合起来先转置再水平翻转。原位置(i,j)的元素转置后先到(j,i)再水平翻转就到(j, n-1-i)。你看这和开头说的旋转目标位置(j, n-1-i)完全一样。所以顺序是固定的先转置再每行左右翻转得到的就是顺时针旋转90度的结果。如果把顺序反过来先水平翻转再转置得到的位置是(j, i)水平翻转后变成(j, n-1-i)不计算一下先水平翻转原元素到(i, n-1-j)再转置到(n-1-j, i)。这个结果对应的是逆时针旋转90度。所以“先翻转还是先转置”是有讲究的面试里经常会拿这个点来追问。1.3 两种思路怎么选这道题的标准解法就两种一种是转置加水平翻转一种是逐层四元交换。两者时间复杂度都是O(n^2)空间复杂度都是O(1)没有谁优谁劣但使用场景有差异。转置加翻转的优点是逻辑简单、代码短、下标不容易越界面试时边说边写很顺畅。逐层四元交换的好处是更“纯原地”没有中间视角而且对缓存局部性更友好如果面试官追问细节能讲出这一层会加分。我的建议是第一次刷先把转置加翻转吃透确保五分钟内能无脑写出来等复习第二轮时再把逐层交换练熟作为进阶方案。2. 方案一转置加水平翻转2.1 操作步骤与原理说明这个方案就两步第一步得到转置矩阵第二步把每一行左右翻转。第一步要特别注意遍历范围。常规的转置操作很多人会写成双层循环从0到n-1全遍历结果发现矩阵根本没变化。原因很简单交换matrix[i][j]和matrix[j][i]时如果你遍历了全矩阵那么当外层i1、j0时你已经把刚才i0、j1时交换过的一对又换回去了。正确做法是只遍历上三角也就是j从i1开始这样每个非对角线元素只交换一次。第二步的水平翻转每行内部左右对称交换循环只需要走到n/2不能走到n否则同样会换两次。两步都注意到“只处理一半”代码就不会出问题。用生活化的类比来理解转置相当于把一块方板沿着左上到右下的对角线折了一下水平翻转相当于再把这块板左右对折两次折叠叠加起来正好是把整块板顺时针转了90度。2.2 代码实现与逐行解析C实现很简洁void rotate(vectorvectorint 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 (int i 0; i n; i) { for (int j 0; j n / 2; j) { swap(matrix[i][j], matrix[i][n - 1 - j]); } } }Python版本同样直接def rotate(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): matrix[i].reverse()Python的reverse()天然就是水平翻转所以第二步代码更短。但要注意如果你打算在面试里写Pythonreverse()这个细节要知道它做了什么别让面试官觉得你在背API。2.3 为什么这个方案不容易写错转置加翻转的方案容错率高因为每一步都是“基础操作”。第一转置的边界条件非常固定j i 1你不需要去考虑当前处理的是哪一层只需要判断是否在对角线以上。第二水平翻转的j n/2也固定不管n是奇数还是偶数整数除法都能正确处理中间的对称轴。相比之下逐层交换方案要求你同时跟踪四个坐标一个符号写错就全盘错调试起来要费更多时间。我刷hot100时身边不少朋友选择背逐层交换的代码结果一周后再刷又不会了。但转置加翻转因为步骤少、每一步都有明确的几何含义哪怕三个月后忘了只要记得“先对角折再左右折”就能很快重新推出来。这也是我把它作为默认解法的原因。3. 方案二逐层原地四元交换3.1 分层拆解从外圈到内圈第二种思路是直接模拟旋转过程。把矩阵想象成一圈一圈的洋葱结构最外层是一圈次外层是一圈直到中心。旋转矩阵时每一圈内部自己转圈与圈之间互不影响。以四阶矩阵为例最外圈包含四个角和四条边除了四个角外每条边中间还有两个元素。这一整圈本身有12个元素但旋转时它们不是“各自搬家”而是每条边对应位置的一组四个元素同时轮转。处理完这一圈再处理内部那一圈内部那一圈就是一个2×2的小方阵四个元素循环转一遍。流程可以写成两层循环外层循环i控制当前是第几圈从0到n/2-1内层循环j控制当前圈内每条边上的第几个位置从i到n-2-i。这里最难理解的就是内层循环上限为什么是n-2-i而不是n-1-i后面专门解释。3.2 四元交换的实现代码核心操作是四元素循环交换。以原矩阵位置(i, j)为例顺时针旋转90度之后这个位置的新值应该来自左下角也就是(n-1-j, i)。这个(n-1-j, i)位置的新值又来自右下角(n-1-i, n-1-j)右下角的新值来自右上角(j, n-1-i)右上角的新值再回到左上角(i, j)。四者形成一个闭环。C实现如下void rotate(vectorvectorint matrix) { int n matrix.size(); for (int i 0; i n / 2; i) { for (int j i; j n - 1 - i; j) { int temp matrix[i][j]; matrix[i][j] matrix[n - 1 - j][i]; matrix[n - 1 - j][i] matrix[n - 1 - i][n - 1 - j]; matrix[n - 1 - i][n - 1 - j] matrix[j][n - 1 - i]; matrix[j][n - 1 - i] temp; } } }记忆方式四句赋值是从“左下角”开始喂给“左上角”按逆时针方向搬运。或者干脆只记坐标变化链(i,j) ← (n-1-j,i) ← (n-1-i,n-1-j) ← (j,n-1-i) ← (i,j)然后按箭头方向把temp串起来。3.3 边界条件的推导内层循环到底应该有多少次是很多人卡住的点。看第i圈这一圈的边长是n - 2*i但每条边上需要和另外三条边对应位置交换的元素个数是边长-1也就是n - 2*i - 1个。这是因为每条边的最后一个位置已经由下一条边的起始位置承接了如果把它也算进去最后会多交换一次整个圈又回到原样。举个例子三阶矩阵最外圈边长为3每条边需要处理的中间元素数是2也就是j 0和j 1对应的循环条件是j n - 1 - i即j 2。四阶矩阵最外圈边长4内层循环处理j 0, 1, 2共3次第二圈是2×2方阵边长2处理1次即j 1。不管n是奇数还是偶数这套公式都成立。n为奇数时最中间的一个格子单独留在中心不需要交换。如果内层误写成j n - 1 - i也就是处理了n - 2*i次看起来代码好像也没越界但角落元素会被连续换两次最终恢复原状整个矩阵除了可能某些位置变化基本等于没转这是新手最常见的隐蔽错误。4. 复杂度、扩展变种与面试问答4.1 时间与空间复杂度对比先说结论两种方案都是O(n^2)时间和O(1)额外空间都是遍历矩阵中每个元素常数次。理论上任何方案都不可能低于O(n^2)时间因为n×n矩阵有n^2个元素每个元素至少得访问一次才知道它该去哪。空间上要求原地修改所以额外空间不能随n增长。转置加翻转虽然过程直观但它不是“一步到位”的旋转而是先做一次变换再做一次变换每次变换都访问全矩阵总访问次数是2×n^2常数系数是2。逐层四元交换每个元素也只访问常数次两者实际运行时间差不多。两者真正的区别在代码风格上。我测试过在n比较大的情况下逐层交换因为能更好地按层访问连续内存缓存命中率略好一点但差距在刷题层面完全可以忽略。选择哪个取决于你在面试中想展示哪一面想稳选转置想炫选逐层。4.2 逆时针旋转与180度旋转面试题很少直接考顺时针旋转更多是换一个角度问比如逆时针旋转90度或者连续旋转多次。逆时针旋转90度也有对称的解法先沿副对角线翻转再水平翻转效果等同于逆时针90度。更简单的方式是复用顺时针代码把原地顺时针旋转函数连续调用三次就是逆时针一次。这个技巧在比赛中很实用不用额外记新的下标公式。旋转180度更简单每行先水平翻转再整列上下翻转或者反过来结果都一样。本质上180度旋转就是把每个元素(i,j)搬到(n-1-i,n-1-j)这是关于矩阵中心点的中心对称变换。用代码表示就是上下翻转加左右翻转两个循环。// 上下翻转 for (int i 0; i n / 2; i) { for (int j 0; j n; j) { swap(matrix[i][j], matrix[n - 1 - i][j]); } } // 左右翻转 for (int i 0; i n; i) { for (int j 0; j n / 2; j) { swap(matrix[i][j], matrix[i][n - 1 - j]); } }建议把“旋转90度转置水平翻转”“旋转180度水平垂直翻转”这两组关系刻在脑子里面试时遇到矩阵变换题都可以套用。4.3 面试追问的应答思路面试官在看完这道题之后大概率会追几个问题。第一个常见追问是“如果允许额外开一个矩阵你会怎么写”这时候你要能迅速给出新矩阵版本核心代码就是开头那个坐标公式新矩阵ans[j][n-1-i] matrix[i][j]。能写出这版说明你确实理解坐标映射而不是只背了原地解的代码。第二个追问是“两种原地方案你更喜欢哪种为什么”这时候别只说“都行”。比较好的回答思路是转置加翻转更容易推导适合现场演示正确性逐层交换更接近旋转的本质而且天然适合进一步优化的场景。如果面试官做的是图形学方向还可以补一句矩阵变换可以拆成多个基本变换的复合旋转矩阵等于转置矩阵和翻转矩阵的乘积。第三个追问可能涉及泛化“如果矩阵不是方阵怎么旋转”这里要坦白非方阵的原地旋转通常不现实因为形状都变了一般需要新矩阵存储。LeetCode的题明确限定n×n所以这个追问更多是考察你是否意识到“方阵”这个前提的价值。5. 实战踩坑与刷题心得5.1 常见的三种写错方式刷这题最容易踩的坑我总结成三句话转置遍历了全部元素翻转遍历了整行所有位置逐层交换多算了一个位置。第一种错误写出来之后非常迷惑代码运行矩阵看起来完全没变。原因是转置时从j0遍历到jn每个非对角线元素都被交换了两次最终回到原位。解决方法是记住转置只处理上三角j从i1开始。第二种错误是水平翻转的循环写成j n同样导致换两次。虽然看起来是“翻转了”实际矩阵还是原样。第三种错误则相反矩阵变化了但结果不对比如左上角的值去了不该去的地方这是逐层方案中内层循环边界多算了一位。还有一种隐蔽问题C里matrix.size()返回的是size_t无符号类型如果直接用n-1再和负数比较就容易出问题。写循环时先把n转成int能省去很多不必要的麻烦。5.2 快速自测方法写完代码建议先用2×2、3×3、4×4三个用例自测。2×2能验证基础四角交换3×3能验证奇数阶的中心不动4×4能验证多层嵌套。更实用的验证方式是利用旋转的性质一个矩阵连续顺时针旋转四次一定会回到原矩阵。你可以先手写一个小的测试矩阵然后调用rotate四次断言结果等于原矩阵。这个方法比对照样例输出更快尤其适合提交前快速排查。另一个性质是旋转两次等于180度旋转也就是每个元素(i,j)变成(n-1-i,n-1-j)。如果旋转两次后逐项比对这个关系也能快速判断代码对不对。我在本地刷题时习惯写一个随机矩阵生成器随机生成5×5矩阵旋转一次之后再用上面的性质验证反复跑几十组能在几分钟内确认代码在所有边界条件下都稳定。5.3 我的刷题习惯与建议力扣hot100里的题目很多都是“高频面试原题”旋转图像就是典型的代表。我自己刷这道题的过程比较曲折第一遍用新矩阵版本写对了觉得题很简单结果三天后手写原地版卡了二十分钟没写出来原因就是没理解坐标映射光在背代码。后来把坐标公式写在纸上推导了一遍转置加翻转的等价性才算真正掌握。如果你也正在刷这道题我的建议很简单先确保能写出新矩阵版本然后原地化最后再理解逐层交换。别跳过推导步骤直接背代码面试时一旦被追问“为什么”背答案很容易露馅。矩阵旋转这类题一旦理解了坐标变换后面再遇到生命游戏、螺旋矩阵这些类似题目都会觉得轻松很多。最后分享一个我自己一直在用的小技巧把这道题的坐标公式写在代码注释里。下次回看代码不需要重新推导一眼就能看懂当时的设计思路。刷题不是为了刷数量一道经典题吃透比囫囵吞枣过十道更有价值。
返回列表