☰
转置矩阵与其下标映射:从LeetCode 867题拆解二维数组基本功
2026/10/15 18:43:48 网站建设 项目流程

LeetCode 867 这道题,题面短到几乎没有存在感:给你一个二维矩阵,返回它的转置。按照网上常见的题解写法,核心代码只有三行,外层循环行,内层循环列,把matrix[i][j]放进res[j][i]就完事。但我在第一次刷这道题的时候,实际花掉的时间是 100 分钟。不是环境出了问题,也不是题目读不懂,而是围绕「转置到底改变了什么」这件事,我在脑子里反复推演了很久。真正卡住我的不是写循环,而是确认自己有没有把行列关系彻底想清楚。

这篇文章不是来教你背答案,而是想还原一下,一道简单题在资深思路里到底应该怎么拆解。适合刚开始刷题、对二维数组下标容易晕的人,也适合想从一道题里延伸出矩阵操作基本功的读者。

1. 为什么一道简单题会花掉 100 分钟:拆一下时间账

1.1 先看懂题目在问什么

第 867 题「Transpose Matrix」,中文一般叫「转置矩阵」。它做的事情很朴素:把原矩阵的行变成新矩阵的列,把原矩阵的列变成新矩阵的行。

比如一个 2 行 3 列的矩阵:

[[1, 2, 3], [4, 5, 6]]

转置以后得到 3 行 2 列:

[[1, 4], [2, 5], [3, 6]]

看起来很简单,对吧?转置的定义一句话就能说清楚,但你真去写代码,至少要处理两个问题:新矩阵的尺寸是多少?下标从旧数组到新数组怎么映射?这也是我 100 分钟里真正反复确认的核心。

1.2 我最初浪费时间的三个地方

第一个坑是把行数和列数写反。我当时下意识以为「转置就是行列数互换,所以两个维度调一下就行」,于是写下了类似new int[rows][cols]的初始化,结果一运行就数组越界。仔细一想才反应过来,原来的rows是 2,cols是 3,转置后应该是 3x2,新数组的初始化必须写成new int[cols][rows]。

第二个坑是纠结「要不要在原地交换元素」。因为我之前刚刷过一些原地操作数组的题,遇到「变换矩阵」就下意识想省空间。可这题要求的是返回一个新数组,不是修改原数组,你非要在原数组上折腾,反而把自己绕晕。后来我翻题目给出的函数签名确认了一下,返回类型是新的二维数组,原地操作在这个题里根本没有必要成为优先考虑的方向。

第三个坑是怀疑「转置是不是等于把二维数组 reverse 一下」。我盯着输出看了很久,总觉得[[1,4],[2,5],[3,6]]和原数组之间应该有某种对称关系,但一下子又说不上来这种对称到底怎么用下标表达。现在回头看,这就是典型的「生活直觉」和「下标语言」没有互相翻译过来。

1.3 这份时间账花得值不值

如果只看提交时间,100 分钟换一道简单题,效率确实不高。但从理解牢固程度来说,这份时间账很值。因为在那之后,我遇到任何矩阵下标变换的题,都习惯先在纸上画出新旧形状,再写循环,基本没再因为行列反了而翻车。所以我想说,简单题花时间不丢人,真正亏的是「AC 了但不知道自己在做什么」。很多人刷题喜欢追求最短通过时间,但第 867 题这种题目,恰恰是花时间建立下标直觉的好机会,过了这村就没这店。

2. 转置矩阵的本质:索引互换,不是把数组倒过来

2.1 一个座位表的类比

为了让自己彻底想明白,我当时把矩阵想象成一张班级座位表。原矩阵里,行是学生,列是科目,每个格子里是某个学生在某科的成绩。转置以后,行变成科目,列变成学生,格子里装的内容不变,变的只是这张表从哪里看、怎么排。

这个类比看起来很幼稚,但特别有用。它让我抓住一个关键:转置改变的不是元素值,而是元素的位置索引。matrix[i][j]这个元素,无论放在哪,它还是原来的值,只是它在新的坐标系里被重新安排了座位。理解这一点以后,写代码时就再也不会想着「把数组里的元素倒序」之类的事了。

2.2 核心公式:result[j][i] = matrix[i][j]

转置的数学表达非常简单:设原矩阵为 A,转置后的矩阵为 B,那么B[j][i] = A[i][j]。也就是说,原矩阵的行号变成新矩阵的列号,原矩阵的列号变成新矩阵的行号。

用代码语言翻译一下:

rows = len(matrix) cols = len(matrix[0]) res = [[0] * rows for _ in range(cols)] for i in range(rows): for j in range(cols): res[j][i] = matrix[i][j]

这里最关键的一点是:循环遍历的是原矩阵的每一个位置(i, j),然后把它的值放到新矩阵的(j, i)去。如果你用的是res[i][j] = matrix[j][i]这种写法,逻辑上也成立,但你需要保证i遍历的是新矩阵的行号范围,j遍历的是新矩阵的列号范围,也就是i要从 0 到cols-1,j要从 0 到rows-1。两种写法的本质一模一样,只是「谁当外层循环」的区别。

2.3 为什么直觉会反:下标语义要先定死

我后来发现自己浪费那么多时间,很大程度是因为在两种写法之间反复横跳。一会儿想着「从原矩阵找新位置」,一会儿又想着「从新矩阵找旧位置」,一下就把自己绕进去了。这里分享一个我的经验:一次性只选一种视角,别来回切换。

如果你选择「遍历原矩阵,把元素送到新矩阵」,那么所有循环变量都基于原矩阵的行列范围,赋值语句只有res[j][i] = matrix[i][j]这一种。如果你选择「遍历新矩阵,从原矩阵取元素」,那么循环变量基于新矩阵的形状,赋值语句只有res[i][j] = matrix[j][i]这一种。选定以后就不要中途换视角,这是避免低级错误最有效的方法。

3. 实现方案与复杂度:返回新矩阵是最稳妥的解法

3.1 三种常见语言的写法

Python 版本:

class Solution: def transpose(self, matrix: List[List[int]]) -> List[List[int]]: rows = len(matrix) cols = len(matrix[0]) res = [[0] * rows for _ in range(cols)] for i in range(rows): for j in range(cols): res[j][i] = matrix[i][j] return res

Java 版本:

class Solution { public int[][] transpose(int[][] matrix) { int rows = matrix.length; int cols = matrix[0].length; int[][] res = new int[cols][rows]; for (int i = 0; i < rows; i++) { for (int j = 0; j < cols; j++) { res[j][i] = matrix[i][j]; } } return res; } }

C++ 版本:

class Solution { public: vector<vector<int>> transpose(vector<vector<int>>& matrix) { int rows = matrix.size(); int cols = matrix.empty() ? 0 : matrix[0].size(); vector<vector<int>> res(cols, vector<int>(rows, 0)); for (int i = 0; i < rows; i++) { for (int j = 0; j < cols; j++) { res[j][i] = matrix[i][j]; } } return res; } };

三种写法结构一致,都是先取行列数,再初始化新矩阵,最后二重循环赋值。语言本身的差异不会影响核心逻辑,你能完整写出任何一种,说明思路已经通了。

3.2 初始化维度为什么是最容易出错的地方

我见过很多人第一次写这道题,包括我自己,最容易在res的初始化上报错。原因很简单:我们把「转置」理解成「形状互换」,于是顺手把rows和cols交换了一下,写成了new int[rows][cols]。

但如果你认真推一遍,会发现rows表示原矩阵的行数,cols表示原矩阵的列数,转置后的矩阵应该是「原列数行、原行数列」,也就是cols行、rows列。所以在 Java/C++/Python 里都要写成res[cols][rows]这种形式。这不是什么高深知识点,纯粹是「先把形状画出来」就能避免的坑。

提示:写这类题时,先别碰代码,在纸上写一行:原矩阵 2x3,转置矩阵 3x2。把目标形状确定下来,再去初始化数组,基本就不会犯这种低级错误。

3.3 方阵原地转置:能写,但不是首选

如果原矩阵是方阵,也就是rows == cols,那么转置前后形状不变,确实可以做原地转置。方法也很经典:沿主对角线交换元素。

for (int i = 0; i < rows; i++) { for (int j = i + 1; j < cols; j++) { swap(matrix[i][j], matrix[j][i]); } }

这里有一个非常重要的细节:内层循环从i + 1开始,不能从 0 开始。如果你从 0 开始,等于把矩阵里每一对(i, j)和(j, i)交换了两次,最后等于没换。这个错误在我最初踩坑的时候也遇到过,而且非常隐蔽,因为对于 2x2 的小矩阵,你可能恰好看不出来。

但第 867 题并不要求原地,而且输入输出形状在非方阵时根本不同,所以用新数组返回是最安全、最符合题意的做法。方阵原地转置可以作为知识扩展去练习,但不要把它当成这道题的标准答案。

3.4 复杂度分析

时间复杂度上,二重循环遍历了原矩阵的全部元素,矩阵大小是rows * cols,所以时间是 O(nm)。空间复杂度上,新建了一个同样规模的矩阵,所以额外空间是 O(nm)。如果改成方阵原地转置,额外空间可以降到 O(1),但代价是只适用于方阵,并且修改了原矩阵。面试时如果被追问「能不能优化空间」,你可以直接给出这两种方案的对比,这比闷头写代码要加分。

3.5 转置不是旋转,别混淆

这道题很常见的一个混淆点是「转置」和「旋转 90 度」之间的关系。转置只做行列互换,不做行的倒序或列的倒序。而顺时针旋转 90 度需要两步:先把行变列,再把每一行反转。它们是两种不同的矩阵操作。如果你把第 867 题和另一道「旋转图像」的题放在一起做,很快就会发现两者的下标映射公式完全不同。提前区分清楚,省得后面刷题时串味。

4. 边界条件与测试用例:这几组用例不写,AC 了也心虚

4.1 空矩阵与空行

第 867 题的输入理论上是一个二维矩阵,但你不能假设它一定非空。如果matrix = [],此时matrix[0]根本不存在,直接取matrix[0].length会越界。C++ 里我写了matrix.empty()的判断,Python/Java 里也可以先判断matrix == null || matrix.length == 0,或者matrix == []。

还有一种边界是matrix = [[]],它表示一个没有元素的行。此时rows = 1,cols = 0,转置后的矩阵应该是 0 行 1 列,还是 1 行 0 列表?严格来说,转置结果应该是一个空矩阵。如果你不做特殊处理,初始化res的时候用[0] * rows可能得到[[0]],这就不对了。所以要么提前判空返回空数组,要么在初始化逻辑里处理cols == 0的情况。实际提交时不一定测到这种输入,但本地验证的时候一定要自己加上。

4.2 单行、单列、1x1

单行矩阵[[1, 2, 3]]是 1x3,转置后变成 3x1:

[[1], [2], [3]]

单列矩阵[[1], [2], [3]]是 3x1,转置后变成 1x3:

[[1, 2, 3]]

这两组用例特别适合检验你有没有把rows和cols搞反。很多人写普通 2x3 矩阵时能蒙对,一旦遇到 1x3 或 3x1,行列就乱了。1x1 矩阵[[1]]转置后还是[[1]],这是最简单的用例,可以用来验证代码没有额外报错。

4.3 一个本地验证转置是否正确的通用方法

转置有一个非常好的性质:对任意矩阵 A,转置两次会得到原来的矩阵transpose(transpose(A)) == A。这意味着你可以写一个小脚本,随机生成各种形状的矩阵,然后连续调用自己的transpose函数两次,最后比对结果。

import random def transpose(matrix): rows = len(matrix) cols = len(matrix[0]) if rows > 0 else 0 if rows == 0 or cols == 0: return [] res = [[0] * rows for _ in range(cols)] for i in range(rows): for j in range(cols): res[j][i] = matrix[i][j] return res for _ in range(1000): rows = random.randint(0, 5) cols = random.randint(0, 5) matrix = [[random.randint(-10, 10) for _ in range(cols)] for _ in range(rows)] if rows == 0 or cols == 0: continue assert transpose(transpose(matrix)) == matrix

这个测试能跑通,说明你的实现大概率是正确的。而且它比手动看输出要省心得多。我在刷题时会保留这种本地验证脚本,遇到矩阵类题目先跑一遍,再提交平台,能省掉很多试错时间。

5. 从第 867 题往前看:矩阵操作题的基本功

5.1 面试里可能的追问方向

第 867 题本身简单,但面试官如果想深挖,完全可以从这道题延伸出很多问题。第一个追问是「如果矩阵是方阵,能不能原地转置」,这个上面已经讲过,关键在于主对角线交换,并且内层循环从i + 1开始。

第二个追问是「如果矩阵特别大,内存放不下,该怎么转置」。这种场景在真实工程里很常见,比如一张超大位图、一个大规模稀疏矩阵。答案通常涉及分块处理:把大矩阵分成若干小块,每次只读两块进内存,交换对应位置的元素。这个方向已经超出简单算法题本身,但它确确实实是转置在生产环境中的一个真实应用场景。

第三个追问可能是「转置在图像处理里有什么用」。灰度图像在程序里就是一个二维数组,转置之后相当于把图像沿着主对角线镜像了一下,再加上行反转或列反转,就能实现各种旋转效果。如果你能把第 867 题和图像处理联系起来,面试官对你的印象会比只会背题解的人好很多。

5.2 一张索引映射表

我在刷完这道题后做了一张小表格,把常见的矩阵变换全部列在一起,方便记忆和理解。这张表对我的帮助非常大,每次遇到矩阵操作题,我都会先在脑子里过一遍这张表。

变换类型原矩阵 (i, j) 映射到新矩阵的位置
转置(j, i)
顺时针旋转 90 度(j, rows-1-i)
逆时针旋转 90 度(cols-1-j, i)
水平翻转(i, cols-1-j)
垂直翻转(rows-1-i, j)

这张表的价值在于,它把所有常见矩阵变换统一成了「下标映射」这一个视角。第 867 题只是其中最简单的一种映射。如果你能自己推导出这张表里的每一个公式,那么矩阵相关的题对你来说就不再是「背模板」,而是「查字典」。

5.3 一道题延伸出的一类题

刷完转置矩阵后,你会遇到很多姿势相似但要求不同的矩阵题。比如旋转图像,核心是四次旋转交换;螺旋矩阵,核心是控制边界收缩;对角线遍历,核心是行下标加列下标的奇偶判断;矩阵置零,核心是标记空间如何压缩。这些题的共同点都是「二维数组下标变换」,差别在变换规则和边界条件。

所以我的建议是:不要只满足于第 867 题通过。你可以试着在本地把这张表里的每一种变换都写一遍,哪怕不提交到平台,也能显著提升下标的直觉。这种练习比盲目刷三五十道简单题更有效。

6. 我的实操建议:这样刷简单题不亏

6.1 把 AC 当作开始而不是终点

第 867 题 AC 之后,我做的第一件事不是赶着刷下一题,而是把代码本地运行了一遍,把输入输出都打印出来看。第一次打印 2x3 矩阵的转置结果时,我盯着那个 3x2 的矩阵看了很久,终于体会到「行列互换」不是一句背出来的定义,而是肉眼可见的形状变化。然后我又写了一个「遍历新矩阵、从旧矩阵取值」的版本,和标准版对比,发现两个版本的循环方向互为镜像。这种自己动手验证的过程,比看十遍题解都有效。

6.2 用「转置两次回到原点」验证直觉

前面提到过,转置两次会回到原矩阵。这个性质我每次写完转置代码都会拿来当自检工具。它最大的好处是不依赖参考答案。你不知道自己的实现对不对的时候,就随机生成几个矩阵,转置两次,再和原数组比较。如果一致,基本可以确定核心逻辑没问题;如果不一致,说明下标映射或者初始化维度出了问题。这个方法简单粗暴,但非常好使。

6.3 别被耗时记录绑架

回到标题里的「耗时 100」。刷题平台上的耗时记录很容易让人焦虑,尤其是看到别人几秒钟就完成了同一道题。但我的真实感受是,第 867 题花掉的那 100 分钟,远比我后来花 5 分钟刷过的那些所谓中等题有价值。因为简单题一旦想通,你会获得一个非常牢固的基础直觉;而中等题如果只是对着题解滑过去,三天后就忘了。耗时记录只是你理解过程的痕迹,不是效率低的判决书。如果你现在也卡在某道简单题上两小时,别慌,这很正常。

6.4 最后分享一个刷题时的小习惯

我在刷矩阵类题目时,有个习惯:先在纸上或者注释里写清楚输入和输出的形状。比如写一行2x3 -> 3x2,再写一行映射公式res[j][i] = matrix[i][j],然后才开始写代码。这个习惯看起来浪费时间,实际上能帮你把思路固定在正确的坐标系里,避免数组越界,也避免行列搞反。第 867 题之后,我把这个习惯沿用到了所有二维数组题上,至今仍觉得非常值。

第 867 题「Transpose Matrix」是一道很典型的入门级二维数组操作题,它不会让你感到高深,但它考验的是你是否真的理解了下标映射这件事。100 分钟换来的不是一段 AC 代码,而是一种一看矩阵就条件反射般在脑中画出形状变化的直觉。如果你现在也卡在自己的「耗时 100」上,不妨把节奏放慢一点,先别急着看题解,试着把那个为什么彻底想明白。后面再遇到类似题目时,你会感谢今天这个肯花时间的自己。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询