
这是一个很多C语言初学者都绕不开的问题也是我当年在校招笔试里真实碰到过的题目。题面很简单——输入一个3x3矩阵M输出它的转置矩阵MT但有个硬性要求必须在同一个数组上完成不允许再开辟新的数组。很多人听到“转置”第一反应就是新建一个MT[3][3]然后两层循环MT[j][i] M[i][j]这思路没错但恰恰违背了题目的考察意图。这道题真正想考你的是对数组下标关系的理解以及**“原地操作”的边界意识**。先说结论3x3矩阵原地转置完全可行核心就一句话——沿主对角线把对称位置上的元素两两交换。因为矩阵转置的本质就是a[i][j]变成a[j][i]放到方阵里看就是绕着主对角线左上到右下这条线“翻”一下。对角线上的元素a[0][0]、a[1][1]、a[2][2]转置前后位置不变剩下的元素都是成对出现的a[0][1]和a[1][0]互换a[0][2]和a[2][0]互换a[1][2]和a[2][1]互换。既然是两两互换用一个临时变量temp就够了完全不需要第二个数组。这篇文章我会从思路拆解、代码实现、边界细节到常见踩坑把这个“简单题”彻底讲透。不管你是刚学数组的萌新还是准备机试、笔试的考研党或校招党这篇文章都能让你彻底搞懂原地转置顺便把二维数组传参、动态分配这些衍生知识点也一并覆盖到。1. 问题拆解转置的数学本质和程序化思路1.1 先想清楚转置到底在做什么数学上矩阵转置就是把m行n列的矩阵M变成n行m列的M^T新矩阵的第i行第j列元素等于原矩阵第j行第i列元素。用C语言的数组表示就是MT[i][j] M[j][i]。关键点来了当且仅当矩阵是方阵行数等于列数时转置前后矩阵形状不变。3x3是方阵所以转置后还是3x3这就在物理上具备了“原地完成”的前提。如果你拿一个3x2的矩阵去转置得到的是2x3的矩阵元素个数虽然一样都是6个但内存布局变了——行数和列数都改了数组本身的形状也得改这在C语言的静态二维数组里是不可能原地做到的必须另开空间。所以这道题单独限定“3x3”表面上是简化实际上已经把“能不能原地”的数学条件给足你了。换句话说凡是考原地转置的题目几乎都是方阵这点先记牢。1.2 为什么不能直接写M[i][j] M[j][i]这是我最常看到的新手错误而且报错不大、逻辑极其隐蔽。有人会写出这样的代码for (i 0; i 3; i) for (j 0; j 3; j) M[i][j] M[j][i];表面上看这完全照着MT[i][j]M[j][i]的公式写但它是一个覆盖操作不是交换操作。假设M[0][1]原来是5M[1][0]原来是8。执行M[0][1] M[1][0]之后M[0][1]变成8但原来的5已经丢了。等到后面执行M[1][0] M[0][1]时取到的M[0][1]已经不是原来的5而是刚被覆盖的8。结果M[0][1]和M[1][0]全都变成8了数据直接损坏。这就像两个人要交换座位你不能让A直接坐到B的位置上完事——那B就没位置了。必须有一个“临时空位”把人暂时挪一下才能完成互换。在C语言里这个“临时空位”就是一个temp变量。所以矩阵原地转置的基本操作是temp M[i][j]; M[i][j] M[j][i]; M[j][i] temp;有个生活化的类比转置矩阵就像把一副扑克牌沿对角线“翻面”每一对对称位置的牌要互相换位置。你不能只把一张牌移到另一张的位置就结束得先把一张拿起来放到temp腾出空位再把对称的那张放过来最后把拿起来的放过去。这个“腾挪”的过程就是temp变量的价值。1.3 两层循环的遍历范围决定了你要交换谁确定了“两两交换”这个基本动作之后下一个问题就是两层循环到底要遍历哪些(i, j)组合直觉做法是两层都从0到2但这样会有个大问题每个元素对会被交换两次。(i0, j1)时交换M[0][1]和M[1][0]等到(i1, j0)时又把M[1][0]和M[0][1]交换回来。转了个寂寞矩阵回到原样。所以必须规定只遍历主对角线上方的元素也就是j i的位置每个元素对只处理一次。具体来说3x3矩阵里需要交换的三对就是M[0][1]和M[1][0]M[0][2]和M[2][0]M[1][2]和M[2][1]对角线上的M[0][0]、M[1][1]、M[2][2]自转不用动。所以内层循环的起点必须是j i 1而不是j 0。这一个1就是整道题最容易考、也最容易错的细节。2. 核心实现完整代码与逐行分析2.1 可以直接抄走的完整代码下面这段代码我实测过输入输出都验证无误可以直接拿去用#include stdio.h int main() { int M[3][3]; int i, j, temp; printf(请输入3x3矩阵的9个元素\n); for (i 0; i 3; i) { for (j 0; j 3; j) { scanf(%d, M[i][j]); } } // 原地转置只处理主对角线上方的元素两两交换 for (i 0; i 3; i) { for (j i 1; j 3; j) { temp M[i][j]; M[i][j] M[j][i]; M[j][i] temp; } } printf(转置后的矩阵MT为\n); for (i 0; i 3; i) { for (j 0; j 3; j) { printf(%d , M[i][j]); } printf(\n); } return 0; }运行时输入示例9 8 7 6 5 4 3 2 1输出结果9 6 3 8 5 2 7 4 1拿第一行验证一下原矩阵第一行是9 8 7转置后它们分别跑到了第一列的位置。M[0][1]原来是8和M[1][0]原为6交换后M[0][1]变成6M[1][0]变成8输出里第一行第二列确实是6第二行第一列确实是8。逻辑完全正确。2.2 循环边界为什么要这么写一步一步演示执行过程很多人能背下j i 1这个写法但不知道为什么。我们把三次外层循环逐一拆开看i0时j从1到2依次交换(0,1)-(1,0)、(0,2)-(2,0)这两对。此时第一行和第一列的元素全部归位。i1时j从2到2只交换(1,2)-(2,1)这一对。第二行和第二列剩余元素归位。i2时内层循环j从3开始但j 3不成立循环体一次都不执行。因为(2,2)是主对角线上的元素不需要交换。注意一个细节当i1时j没有从0开始所以不会再去交换M[1][0]和M[0][1]。这正是避免“二次交换”的关键。你可以试着把代码改成j 0跑一遍输入上面的数据最终输出会发现矩阵变回了原样——不是报错而是逻辑错了这种错误最难排查。所以整个转置过程一共只发生3次交换每次交换涉及3个赋值语句总共9次赋值。这就解开了为什么要求“不新开数组”也能实现——因为你只是把元素在数组内部“搬家”并没有新增存储空间除了一个temp这属于常数级额外空间在算法分析里不算“新数组”。2.3 输入输出的细节空格、换行、scanf的返回值实操中还有几个输入输出层面的细节值得注意第一scanf按空白字符分割输入。空格、Tab、换行对scanf(%d)来说都是分隔符。也就是说用户输入下面任何一种格式效果完全一样9 8 7 6 5 4 3 2 1或者分三行输入9 8 7 6 5 4 3 2 1甚至一次性输入9 8 7 6 5 4 3 2 1scanf都能正确读到9个值。这倒是个方便之处但我建议在题目没有明确格式要求时输出每行三个数、行末换行更符合矩阵的阅读习惯。你可以在printf(%d , M[i][j])后面加一个判断当j 2时输出换行效果一样。第二scanf的返回值值得检查。严谨一点可以这样写if (scanf(%d, M[i][j]) ! 1) { printf(输入格式错误请重新输入整数。\n); return 1; }scanf返回的是成功读取的变量个数。如果用户手滑输入了字母x读取失败程序会陷入死循环因为scanf无法消费掉那个非法字符会一直卡在那里。在笔试或机试环境中虽然不检查一般也能过测试用例但养成良好的防御习惯总没坏处。第三数组下标从0开始这点一定要在脑子里反复强化。M[3][3]的合法下标是0~2不存在M[3][0]、M[0][3]这样的位置。我第一次写转置时就犯过j 2和j 3混用的错虽然结果碰巧一样但理解上容易留下隐患。统一用 3代码更安全、更不易越界。3. 从3x3到通用n阶方阵的原地转置与动态矩阵3.1 一行代码改造成任意n阶方阵3x3写死之后很多人会问如果题目变成n阶方阵或者用户输入一个n然后跟着n*n个元素怎么办其实不必慌核心逻辑一点不变只需要把常量3换成变量n再把数组定义改一下。如果是n较小且已知上限的静态写法可以这样#include stdio.h #define MAX_N 100 int main() { int n; int M[MAX_N][MAX_N]; int i, j, temp; printf(请输入方阵阶数n不超过%d, MAX_N); scanf(%d, n); printf(请输入%d个元素\n, n * n); for (i 0; i n; i) for (j 0; j n; j) scanf(%d, M[i][j]); for (i 0; i n; i) for (j i 1; j n; j) { temp M[i][j]; M[i][j] M[j][i]; M[j][i] temp; } printf(转置结果为\n); for (i 0; i n; i) { for (j 0; j n; j) printf(%d , M[i][j]); printf(\n); } return 0; }看到没有真正的“转置三行”代码一行都不用改只是把3换成了n。这也再次说明你只要抓住“只处理上三角”这个本质不管矩阵多大逻辑都是同一套。3.2 动态分配二维数组的两种常见写法如果n可能很大比如上千阶静态二维数组有栈溢出风险就得用动态分配。这里有个C语言新手绕不开的坑用malloc分配二维数组并不像int M[n][n]那么直观。写法一指针数组每一行单独分配int **M (int **)malloc(n * sizeof(int *)); for (i 0; i n; i) { M[i] (int *)malloc(n * sizeof(int)); }这种写法的好处是M[i][j]的下标访问方式不变代码改造成本几乎为零。缺点是每一行的内存不一定连续而且释放时要先释放每行再释放行指针数组顺序不能反。写法二一次性分配连续内存块int *M (int *)malloc(n * n * sizeof(int)); // 访问 M[i][j] 时用 M[i * n j]这种写法内存完全连续局部性好、cache命中率高对大规模矩阵运算性能更友好。但在转置交换时要把M[i][j]写成M[i * n j]M[j][i]写成M[j * n i]代码可读性会差一些。对于3x3这种小矩阵无所谓但如果题目后面还跟着矩阵乘法的进阶要求连续分配往往更占优。注意动态分配用完后一定要free否则内存泄漏。释放顺序是先释放每一行的内存指针数组模式下最后释放M本身。写成for (i 0; i n; i) free(M[i]); free(M);顺序非常有讲究颠倒会崩溃。3.3 非方阵为什么不能原地转置一句话讲明白前面提了一句这里细说。假设有一个2x3矩阵1 2 3 4 5 6它在内存里按行连续存放1 2 3 4 5 6。转置后应该是2x3还是3x2是3x21 4 2 5 3 6元素个数还是6个但第1个元素原1还在第一位原第2个元素原2要挪到第4位原第3个元素原3挪到第5位……整个内存顺序发生了重排而且形状变了静态数组的“形状”在编译期就定死了你没法原地改变M[2][3]的维度。所以非方阵转置必须另开newM[3][2]来承接。这道题考3x3方阵本质就是把这个不可能变成可能——你不需要改变数组形状只需要在内部重组顺序而重组顺序恰好可以靠对称交换完成。3.4 进阶用函数封装转置时二维数组怎么传参很多教材讲到这里就停了但笔试里经常把原地转置封装成一个函数来考。这时候会冒出一个经典问题二维数组作为函数参数时第二维的大小必须显式声明。void transpose(int M[][3], int n) { int i, j, temp; for (i 0; i n; i) for (j i 1; j n; j) { temp M[i][j]; M[i][j] M[j][i]; M[j][i] temp; } }上面这个写法合法且常见。int M[][3]中的第一维[]可以省略但第二维[3]不能省。原因是C语言数组在内存中按行连续存储编译器必须知道“每行有几个元素”才能计算M[i][j]的真实地址。计算方式是base (i * 每行元素数 j) * 元素大小。如果你不告诉它每行有几个元素它连地址都算不出来。如果你想写得更通用可以用指针数组形式对应3.2节的动态分配写法一void transpose(int **M, int n) { // 逻辑同上只是M[i][j]的访问方式改为通过行指针间接寻址 }但要注意int M[3][3]这种静态二维数组不能直接当作int **传参类型并不兼容编译器会报warning甚至error。这是C语言里非常经典的“数组与指针关系”考点也正好呼应了搜索热词里的“指针数组”“数组指针”。如果你在面试中能把这个问题讲清楚面试官会高看你一眼。4. 常见错误与排查技巧这些坑我基本都踩过4.1 双重交换矩阵“莫名其妙”回到原样现象代码跑起来不报错输入输出格式都对但转置前后矩阵一模一样。原因内层循环从j 0开始导致(i,j)和(j,i)被加工了两遍。第一遍交换成功第二遍又换回来等于白干。排查方法在交换语句前加一行调试输出printf(swap (%d,%d) - (%d,%d)\n, i, j, j, i);观察是否有重复的下标对出现。比如(0,1)和(1,0)都出现在输出里那肯定是遍历范围错了。修复把内层循环改成for (j i 1; ...)。这一个字符之差就是整道题的分水岭。4.2 覆盖而非交换矩阵某些元素全变成同一个数现象转置后矩阵里大量出现重复数字比如对称位置两个数都变成了原来M[0][1]的值。原因直接把M[i][j] M[j][i]当成交换用没有temp也没有三步赋值的交换逻辑。前面“换座位”的类比已经解释过这属于对“数组赋值会覆盖原值”理解不够。排查方法只输入一个简单矩阵比如1 2 3 4 5 6 7 8 9转置后如果输出变成1 4 7 2 5 6 3 6 9这就是典型的未使用temp导致的覆盖后遗症因为M[1][2]和M[2][1]都变成了6。正确的输出应该是1 4 7 2 5 8 3 6 9拿这个用例一测问题立刻暴露。4.3 数组越界j 3写成j 3现象程序运行到一半崩溃或者矩阵里出现一个巨大的垃圾整数甚至scanf完之后程序行为诡异。原因数组M[3][3]合法下标是0到2访问M[0][3]实际上越界到了下一行的M[1][0]位置。C语言不检查数组越界它只是把一个“别的内存位置”的值读出来或写进去行为未定义。很多编译器和运行环境不报错但这纯粹是运气换一个系统可能直接段错误。排查方法这个问题极强的隐蔽性在于它可能“偶尔正常”。建议在所有涉及数组下标的循环条件处统一写成 3而不是 2保持风格一致。或者用gcc -fsanitizeaddress编译越界会立刻被检测出来——这是我调试C程序很喜欢用的工具你们也值得学会。4.4scanf读取失败造成的死循环现象明明输入的是9个正常的整数程序却像卡住了一样甚至疯狂输出提示信息。原因如果用户输入了非数字字符scanf返回0没有变量被赋值但那个非法字符还在缓冲区里没被消费。如果后面的循环继续scanf同一个字符又是0又没消费……于是无限循环。排查方法严格检查scanf的返回值发现异常就return或清空输入缓冲区。清空方式可以用while (getchar() ! \n);把残留字符全部读取掉。修复参考if (scanf(%d, M[i][j]) ! 1) { printf(输入无效程序退出。\n); return 1; }这种防御性编程在平时写作业时可能显得多余但在机试现场、在别人给你乱输数据测试时能帮你保住大量得分点。4.5 调试技巧把矩阵“打印出来看”永远是最快的矩阵题最有效的调试方式就是打印矩阵。你不需要用多高级的调试器一个printMatrix函数走天下。每完成一个阶段就打印一次当前状态看哪里和预期不符基本一眼定位。void printMatrix(int M[3][3]) { for (int i 0; i 3; i) { for (int j 0; j 3; j) printf(%d , M[i][j]); printf(\n); } }我调试这类代码的固定流程是输入一个已知答案的矩阵 → 转置前打印一次 → 转置后打印一次 → 和手算结果对照。如果手算结果是1 4 7 / 2 5 8 / 3 6 9输出的却是1 2 3 / 4 5 6 / 7 8 9那就是双重交换如果输出是1 4 7 / 2 5 6 / 3 6 9那就是覆盖。问题性质立刻分得一清二楚。5. 从矩阵转置延伸出去数组和指针的几个关联考点5.1 为什么“不新开数组”是一种资源意识这道题的“不新开数组”约束放在真实工程里其实是空间复杂度的考量。一个3x3矩阵多开一个同样大小的数组确实没多少内存9 * 4 36字节但如果你面对的是10000x10000的矩阵多开一个数组就是多占400MB内存这可能直接压垮嵌入式设备、缓存系统和大量并发场景下的服务端。原地操作O(1)额外空间的算法在图像处理、矩阵运算库、嵌入式线性代数库里都是非常有价值的优化方向。所以每次做题如果题目给了“原地操作”的限制先别急着吐槽“这有什么意义”尝试去理解出题人想让你建立的空间意识。等你在实际项目里被内存卡过几次脖子你就会感谢当年那个要求原地转置的题目。5.2 指针数组、数组指针和int **的三角关系这个知识点和矩阵题高度相关搜索热词里也有“指针数组存放字符串”“指针数组”等。简单理一下数组指针int (*p)[3]p是一个指针指向“含有3个int的一维数组”。把它指向M即M[3][3]后p[i][j]和M[i][j]等价。指针数组int *p[3]p是一个数组里面有3个int *元素。在3.2节的动态分配写法一中int **M配合每行malloc本质上就是一个“指针数组的动态版”。int **指向指针的指针通常配合malloc批量生产“二维数组”但它的内存布局和真正的int [3][3]不同。分清这三者你就能理解为什么void f(int **p)不能直接接收int M[3][3]——因为M在表达式上下文会退化成指向第一行的指针类型是“指向包含3个int数组的指针”即int (*)[3]和int **不是一回事。这块内容配合矩阵题一起学效率极高。5.3 如果要做得更“锦上添花”可以试试这三个小改造用while循环代替双重for把i和j做成两个循环变量在j n时i、j i 1。这能帮你加深对循环控制的理解但可读性会差一些不推荐在笔试时炫技。单层循环遍历上三角元素把(i, j)对映射成一维坐标for (int k 1; k n * (n - 1) / 2; k)再反解出(i, j)。这种映射方法在“将二维转一维”的算法题里很常用但反解过程容易出错复杂度提升不少。用位运算交换元素仅限整数且不怕溢出/异或特性a ^ b; b ^ a; a ^ b;不需要temp变量。但你要知道这种写法在矩阵转置里看起来“更清爽”实际上很难读而且对浮点数不适用笔试不强求。我个人建议老老实实写temp三步交换把精力放在循环边界和传参上性价比最高。等你对所有基础写法烂熟于心了再去玩这些“技巧”也不迟。最后分享一点我自己的体会这类题乍看简单但它把**数学定义转置、内存模型二维数组的连续存储、循环设计只处理上三角、数据搬移交换而非覆盖**全部串在了一起。能把这三五行核心代码讲清楚的人C语言基础绝对不会差。如果你在练习时遇到“明明照着答案写却还是不对”的情况别急着怀疑代码——先在纸上把3x3矩阵画出来手写一遍交换过程再对照代码一步步推基本都能找到问题。这个“先画图再写码”的习惯是我学C语言以来收获最大的一个技巧建议你们也试一试。