前两天帮一个学弟改代码,题目就是“输入一个 n×n 矩阵,将其按副对角线交换后输出”。他说自己写了大半天,跑样例倒是能过,一提交就 WA,完全不知道错在哪。我打开他代码一看,好家伙,把副对角线当成了主对角线,全程在做转置。这个题乍看就是入门级矩阵操作,实际上藏着两个最容易掉进去的坑:一是副对角线的坐标映射到底怎么写,二是原地交换的时候遍历范围控制不好。这篇就把这个题完整拆一遍,从数学映射到代码实现再到边界情况,一次性讲清楚。
说白了,“按副对角线交换”本质是在做一种矩阵变换。你不需要背什么高深理论,只要搞清楚坐标映射关系,不管输入行列是几、题目换成主对角线还是旋转90度,都能直接套。我下面会给出 Python、C++ 的完整实现,也会把原地交换和新建矩阵两种方案的取舍讲明白。适合正在刷题、准备机试或考研复试、或者在工作中突然被矩阵操作卡住的人。
1. 副对角线交换到底在做什么
1.1 先分清主对角线和副对角线
主对角线大家都很熟,从左上角到右下角,元素满足行号等于列号,也就是 i == j。副对角线(也叫反对角线、次对角线)是从右上角到左下角这条线,元素满足行号加列号等于 n-1,也就是 i + j == n - 1。注意这是从 0 开始计数的结果,如果从 1 开始数,就是 i + j == n + 1。
很多刚入门的人栽就栽在这里,看到“对角线交换”就条件反射写成转置。转置是沿主对角线翻转,跟副对角线翻转差很远的。拿一个 3×3 矩阵举例:
code复制1 2 3
4 5 6
7 8 9
主对角线翻转(转置)的结果是:
code复制1 4 7
2 5 8
3 6 9
副对角线翻转的结果是:
code复制9 6 3
8 5 2
7 4 1
这两个结果完全不一样,如果题目写的是副对角线,你非要转置,那肯定 WA。所以动手写代码前,先在纸上把这个例子过一遍,非常有必要。
1.2 坐标映射公式是怎么推出来的
我习惯把这个过程理解成“把一张写满数字的方格纸,沿右上到左下的对角线对折”。对折之后,原来在 (i, j) 位置的格子,会跑到对面镜像的位置。问题是,这个镜像位置的下标怎么表达?
先说结论:如果矩阵是 n×n,元素 A[i][j] 沿副对角线翻转后的目标是:
code复制B[i][j] = A[n - 1 - j][n - 1 - i]
也就是说,新矩阵的第 i 行第 j 列,取的是原矩阵第 n-1-j 行、第 n-1-i 列的元素。
这个公式怎么来的?可以用 3×3 的例子验证。索引范围是 0、1、2,所以 n-1 就是 2。
A[0][0]在右上到左下的对角线上看,它应该跑到B[2][2],也就是二维表的右下角。代入公式:B[0][0] = A[2][2],对。A[0][1]应该跑到B[1][2](右上那块的镜像位置)。代入公式:B[0][1] = A[1][2],对。- 副对角线上的元素
A[0][2]、A[1][1]、A[2][0],按公式算出来还是它们自己,因为B[0][2] = A[0][2],B[1][1] = A[1][1],B[2][0] = A[2][0]。
再换个 4×4 的例子快速验证一个点。假设 n=4,原矩阵 A[1][2] 应该映射到哪里?代入公式:B[1][2] = A[4-1-2][4-1-1] = A[1][2]。等等,这里算出来还是它自己,因为点 (1,2) 正好落在 4×4 矩阵的副对角线上(1+2=3=n-1)。换个不在对角线上的:A[1][0],代入公式 B[1][0] = A[4-1-0][4-1-1] = A[3][2]。手动在原矩阵里画一下,(1,0) 的镜像确实在 (3,2),正确。
看到这里你应该能感觉到,这个公式并不需要死记。你只需要记住副对角线上的点满足 i + j = n - 1,然后想象一下整行整列反向映射,就能推出来。核心就是两个逆序:行坐标变成列坐标的逆序,列坐标变成行坐标的逆序。
1.3 副对角线翻转和转置、旋转是什么关系
这块是我觉得最值得讲透的地方,因为理解了等价关系,很多写法就会豁然开朗。
副对角线翻转可以拆成三步:先上下翻转,再左右翻转,最后转置。用 3×3 例子走一遍:
原矩阵:
code复制1 2 3
4 5 6
7 8 9
上下翻转(行逆序):
code复制7 8 9
4 5 6
1 2 3
再左右翻转(每行逆序):
code复制9 8 7
6 5 4
3 2 1
最后转置:
code复制9 6 3
8 5 2
7 4 1
正好就是副对角线翻转的结果。
换句话说,“先上下翻转再左右翻转”这一步,数学上就是矩阵绕中心旋转 180 度。所以副对角线翻转也等于“先旋转 180 度再转置”。这个视角很有用:Python 里如果用 NumPy,可以直接写 np.flip(A).T;如果你正在用某个工具库,组合现有的翻转、转置函数就能完成任务,不用自己造轮子。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 三种主流实现方案,按场景选
2.1 直接映射法:最稳,适合一切场景
直接映射法就是新开一个同样大小的矩阵,然后按 B[i][j] = A[n-1-j][n-1-i] 填数。
python复制n = int(input())
a = [list(map(int, input().split())) for _ in range(n)]
b = [[0] * n for _ in range(n)]
for i in range(n):
for j in range(n):
b[i][j] = a[n - 1 - j][n - 1 - i]
for row in b:
print(' '.join(map(str, row)))
这段代码最不容易出错,尤其适合新手跑通流程。新建矩阵的好处是根本不用操心“会不会把还没用的数据提前覆盖掉”,因为读的都是原矩阵,写的是新矩阵,两个空间是隔离的。
需要注意的是,Python 里创建二维列表千万别写成 [[0] * n] * n。这个写法会创建 n 个指向同一行对象的引用,你改 b[0][0],整列全部跟着变,这是个经典坑。正确写法是列表推导式 [[0] * n for _ in range(n)],每一行都是独立的新列表。
2.2 原地交换法:省内存,但要小心遍历范围
如果矩阵很大,或者题目明确不允许额外开一个同样大小的矩阵,那就用原地交换。思路很直接:沿副对角线对称的两个元素需要互换位置,比如 A[i][j] 和 A[n-1-j][n-1-i] 是一对,交换一次即可。
关键问题是遍历范围。很多人第一次写会直接:
python复制for i in range(n):
for j in range(n):
a[i][j], a[n-1-j][n-1-i] = a[n-1-j][n-1-i], a[i][j]
跑完发现矩阵根本没变。原因很简单:你遍历了全部 n×n 个位置,每一对元素都被交换了两次。第一次把 A 换到 B,第二次又从 B 换回 A,最后等于什么都没干。
正确做法是只遍历副对角线一侧的元素。对第 i 行来说,只需要遍历 j 从 0 到 n-2-i 即可。
python复制n = int(input())
a = [list(map(int, input().split())) for _ in range(n)]
for i in range(n):
for j in range(n - 1 - i):
a[i][j], a[n - 1 - j][n - 1 - i] = a[n - 1 - j][n - 1 - i], a[i][j]
for row in a:
print(' '.join(map(str, row)))
为什么 j 的上限是 n - 1 - i?因为副对角线上的点满足 j = n - 1 - i。这些点在原地交换中是不需要动的,所以严格小于这个值就行。再算一下交换次数:总共有 n² 个元素,副对角线上有 n 个元素不动,剩下 n² - n 个元素两两配对,所以实际交换次数是 n×(n-1)÷2。这个数字也可以验证你的循环范围对不对。
2.3 Python 进阶写法:列表推导、zip 与 NumPy
如果是在数据分析或日常脚本里遇到这个需求,不用非得写双重循环。Python 的组合写法可以很优雅地表达“先 180° 旋转再转置”:
python复制def flip_anti_diagonal(mat):
return [list(row) for row in zip(*[row[::-1] for row in mat[::-1]])]
拆解一下:mat[::-1] 是把整个矩阵上下翻转;再对每个元素做 row[::-1],就是左右翻转,合起来等于旋转 180 度;zip(*...) 是做转置。所以核心思路和 1.3 节完全一致。
如果你在用 NumPy,那就更简单了:
python复制import numpy as np
a = np.array([list(map(int, input().split())) for _ in range(n)])
b = np.flip(a).T
np.flip(a) 默认翻转所有轴,也就是 180 度旋转,.T 是转置。两条语句搞定。注意 np.flip 和 np.rot90 的区别在实际使用中很常见,np.rot90(a, 2) 也是 180 度旋转,再 .T 同样能达到效果。我试过在矩阵规模比较大的时候,NumPy 方案明显比纯 Python 双重循环快得多,所以工程里我还是推荐 NumPy。
2.4 三种方案对比与选型建议
| 方案 | 代码复杂度 | 空间占用 | 适用场景 |
|---|---|---|---|
| 直接映射法 | 低,逻辑清晰 | O(n²) 新矩阵 | 新手、非方阵、快速实现 |
| 原地交换法 | 中,需控制遍历范围 | O(1) 额外空间 | 大矩阵、内存受限场景 |
| zip/NumPy 写法 | 低,但需理解组合逻辑 | 视实现方式而定 | 日常脚本、数据分析、追求简洁 |
我的建议是:考试或面试时优先写直接映射法,因为不容易错,思路一眼能看懂。如果面试官追问能不能省空间,再手写原地交换。实际工程项目里,如果数据量不大,直接用 NumPy 最省心。
3. 输入输出处理与完整代码
3.1 刷题平台常见的矩阵读入方式
这个题的输入格式一般是:第一行给一个整数 n,接下来 n 行每行 n 个整数。对应 Python 读法:
python复制n = int(input())
a = [list(map(int, input().split())) for _ in range(n)]
这里要注意,input().split() 默认按任意空白字符切分,所以一行里多个空格或者行首行尾有空格都没问题。
但有些平台不给 n,而是连续给出一堆数,最后 EOF 结束。这种就需要逐行读,或者干脆一次性读完:
python复制import sys
data = sys.stdin.read().split()
sys.stdin.buffer.read().split() 返回的是字节串,速度更快,适合数据量大的情况。处理完后可以自己拼回矩阵。如果只是刷题,数据量通常不大,用 sys.stdin.read() 就够。
还有一种是多组测试数据,每组前面给一个 n。这种一般用循环包一层:
python复制import sys
def solve(n, lines):
...
data = sys.stdin.read().strip().split()
idx = 0
while idx < len(data):
n = int(data[idx])
idx += 1
a = []
for _ in range(n):
a.append(list(map(int, data[idx:idx + n])))
idx += n
# 处理当前矩阵
这个模板我用了很多次,核心就是维护一个下标指针,再配合列表切片取每一行,比每次调用 input() 更稳,不会因为空行、末尾换行符出问题。
3.2 输出格式别在这里翻车
输出阶段最常见的两个坑:行尾多空格、缺换行。很多 OJ 对行尾空格容忍,但有些严格比对字符串就必挂。我之前见过一个学弟用循环拼字符串,最后没处理空格,全 WA。
推荐做法有两种。Python 里最简单的是:
python复制for row in b:
print(' '.join(map(str, row)))
join 只在元素之间加空格,行尾没有多余空格,而且 print 自带换行。不推荐用 for x in row: print(x, end=' '),虽然也能跑,但行尾空格很难看,还得额外补换行。
C++ 里我习惯这样控制:
cpp复制for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
if (j) cout << ' ';
cout << b[i][j];
}
cout << '\n';
}
用 if (j) 判断是否是行内第一个元素,第一个不打印空格,后面的都前置一个空格。这样行尾永远没有多余空格。
3.3 可直接跑的完整代码
Python 版本(直接映射法,最通用):
python复制n = int(input())
a = [list(map(int, input().split())) for _ in range(n)]
b = [[0] * n for _ in range(n)]
for i in range(n):
for j in range(n):
b[i][j] = a[n - 1 - j][n - 1 - i]
for row in b:
print(' '.join(map(str, row)))
C++ 版本(原地交换法):
cpp复制#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<vector<int>> a(n, vector<int>(n));
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
cin >> a[i][j];
}
}
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n - 1 - i; ++j) {
swap(a[i][j], a[n - 1 - j][n - 1 - i]);
}
}
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
if (j) cout << ' ';
cout << a[i][j];
}
cout << '\n';
}
return 0;
}
注意 C++ 里我加了 ios::sync_with_stdio(false) 和 cin.tie(nullptr),这两行是为了加快 cin、cout 的速度。刷题时如果不加,数据量大一点可能被 IO 卡超时。输出用 '\n' 而不是 endl,也是避免每次输出都强制刷新缓冲区。
4. 边界情况、非方阵和多组数据
4.1 奇偶尺寸下的副对角线元素
当 n 是奇数时,比如 3×3 或 5×5,副对角线的正中间会有一个元素,它恰好是关于副对角线对称的“不动点”。以 3×3 为例,就是 A[1][1],5×5 就是 A[2][2]。原地交换时它和自身交换,没任何影响,所以不需要特殊处理,只要循环范围正确,它自然不会被波及。
当 n 是偶数时,副对角线上没有中心元素,所有元素都成对交换。比如 4×4 矩阵,副对角线上的 4 个元素各自不动,其余元素两两配对。无论是直接映射还是原地交换,行为都和奇数情况一样,代码不需要分开处理。
1×1 矩阵是极端情况,原矩阵就一个数,翻转后还是它。原地交换的循环条件 for j in range(n - 1 - i) 在 n=1 时是 range(0),直接不执行,输出原样,天然正确。
4.2 非方阵到底能不能按副对角线翻转
这个问题一定要看题目怎么定义。严格从数学角度讲,副对角线是从矩阵右上角到左下角的斜线,当一个矩阵不是方阵时,这条线画出来是偏的。如果按照几何镜像来理解,一个 m 行 n 列的矩阵沿副对角线方向翻转后,结果是 n 行 m 列,类似转置会交换行列数。
举个例子,2 行 3 列的矩阵:
code复制1 2 3
4 5 6
按几何镜像沿副对角线翻转,结果是 3 行 2 列:
code复制6 3
5 2
4 1
可以看到维度反转了。这种情况下绝对不能原地交换,必须新建矩阵,而且循环维度要反过来。
但很多习题里说“按副对角线交换
