1. 先别急着写代码:把"对角线求和"这层纸捅破
很多刷题的朋友看到"简单"标签就直接开敲,结果样例一过就以为完事了。LeetCode 1572这道题,我见过太多人在n为奇数的时候翻车,所以决定把这道题完整地拆一遍,从题目解读到代码实现,再到面试变形,一次讲透。
题目原文很简单:给你一个n x n的方阵mat,请返回矩阵对角线上元素的和。注意,当矩阵维度n为奇数时,两条对角线会经过同一个中心元素,这个元素只能被计算一次。
先说一个容易被忽略的事实:这题虽然标"简单",但它同时考察了三样东西——数组索引基本功、对称性观察力、边界条件敏感度。如果你只是背下解法,那遇到变体题(比如"只求副对角线"或者"矩阵不是方阵")又会卡住。
1.1 题目到底要求什么
输入是一个二维整数数组,输出是一个整数。示例我就不重复贴了,但核心语义是:
- 主对角线:从左上到右下。
- 副对角线:从右上到左下。
- 两条线的交点(如果有)只计入一次。
很多第一次做这道题的朋友会写出双重循环,判断if (i == j || i + j == n - 1)然后累加。这个判断本身没错,但它不是最优解,也不是最不容易错的写法。真正值得思考的问题是:为什么副对角线满足i + j = n - 1,而不是别的式子?
这个式子本质上是在描述"对称位置"的关系。你如果拿一张方格纸画一个3 x 3矩阵,给每个格子标上行号和列号,副对角线上的点分别是(0,2)、(1,1)、(2,0),行加列都等于2,也就是n - 1。所以i + j == n - 1不是硬记的,是"右上角那一列的坐标特征"推出来的。
1.2 真正的考点在边界条件
边界条件是这道题的灵魂。当n是偶数,两条对角线永远不会相交;当n是奇数,中心元素(n/2, n/2)会在两条对角线上同时出现。如果你用双重循环加if判断,恰好会在中心元素处重复计算一次。
不要小看这个错误。我刷题群里有人用暴力法提交,样例全过,一提交就被判错,原因是n为奇数的隐藏用例没有覆盖到。所以不管用哪种解法,都要在代码里显式处理重叠元素,而不是碰运气。
从这道题出发,我更想聊的是如何把一道简单题的价值榨干——包括复杂度优化、不同语言的写法差异,以及如何从一道题引申出一类题的解法。下面一节我们先把索引规律讲透,这是所有解法的基础。
2. 索引规律推导与数学归纳:为什么副对角线是i + j = n - 1
这节内容看起来很基础,但我建议你别跳过。因为很多时候我们写代码"差一个条件",本质是对索引规律的理解不到位。
2.1 主对角线的直觉
主对角线上任意元素的行号等于列号,也就是i == j。这一点不用多说,但我想补充一个观察角度:主对角线描述的是"行列同步增长"的过程。从(0,0)出发,每走一步行号加1、列号也加1,最终抵达(n-1, n-1)。
如果你想把主对角线遍历一遍,不需要双重循环,单循环就够了:
for i in range(n): result += mat[i][i]这个写法的意思很直接:第i行取第i列,行和列一起走,一次遍历拿完主对角线所有元素。
2.2 副对角线的对称性推导
副对角线从右上(0, n-1)到左下(n-1, 0)。每走一步,行号加1、列号减1。你观察任意一个副对角线上的元素(i, j),会发现i增加的量恰好被j减少的量抵消,所以i + j恒等于n - 1。
换一个更直观的说法:副对角线上的元素,相对于矩阵的"水平中轴线"是对称的。把第j列改成"从右往左数"的第n - 1 - j列,那么副对角线就是在"行列编号从同侧对齐"的意义上的主对角线。
所以副对角线遍历可以写成:
for i in range(n): result += mat[i][n - 1 - i]注意这里列索引是n - 1 - i,不是n - i。初学者最容易在这边多写或者少写一个1。你代入i = 0验证:列号是n - 1,对应右上角,正确;代入i = n - 1:列号是0,对应左下角,也正确。
一个实用的自查方法:把n = 3代入公式,手动在纸上列出三条副对角线坐标,再对比代码里的索引表达式,确保首尾两个点都对得上,基本就不会错了。
2.3 合并公式与重叠判断
把两条对角线放在一起,扫描整个矩阵的话,条件就是:
i == j (主对角线) i + j == n - 1 (副对角线)两者取并集,并且当i == j && i + j == n - 1时,代表同一个元素被两条对角线同时包含。把两个等式联立:
i == j i + j == n - 1解得i = j = (n - 1) / 2。这个解存在的条件是(n - 1)必须能被2整除,也就是n为奇数。这就是"为什么只有奇数阶才需要去重"的数学根源,不是靠感觉,是靠联立方程推出来的。
所以:
n为偶数:两条对角线无交集,直接累加。n为奇数:中心元素出现两次,减掉一次即可。
很多题解里写if (n % 2 == 1) result -= mat[n/2][n/2],原理就在这里。
3. 三种写法的完整实现:从暴力到单循环再到一行流
掌握了索引规律,实现就只是时间问题。现在我给出三种可运行的写法,并逐个分析它们的适用场景和优缺点。这里我以Python为主,因为代码短、适合演示思路,但后面会补充Java和C++的关键差异。
3.1 暴力双循环(最容易想到,也最容易错)
思路:遍历每个格子,如果满足主对角线或副对角线条件就累加。
def diagonal_sum_brute(mat): n = len(mat) total = 0 for i in range(n): for j in range(n): if i == j or i + j == n - 1: total += mat[i][j] return total这段代码能过,但有两个问题。第一,它的时间复杂度是O(n^2),虽然n不大时无所谓,但作为算法题,这是"能跑但不够好"的典型;第二,如果不在最后单独扣掉中心元素,那奇数阶时中心元素会被加两次。你可能会问:if条件里两个等式都满足时不应该算一次吗?注意,or并行判断时,只要任一条件为真就进入累加,不会说"两个条件同时满足就只加一次",所以双重循环天然会把中心元素算两遍。
如果你坚持用暴力法,正确姿势是加一个去重判断:
def diagonal_sum_brute_fixed(mat): n = len(mat) total = 0 for i in range(n): for j in range(n): if i == j or i + j == n - 1: total += mat[i][j] # 中心元素被加了两次,扣掉一次 if n % 2 == 1: total -= mat[n // 2][n // 2] return total但说实话,都走到双重循环这一步了,不如直接改成单循环,代码量几乎一样,还更快。
3.2 单次遍历(推荐写法)
核心思路:一次循环遍历每一行,每行取两个元素,分别是主对角线和副对角线上的点。只有当n为奇数且这两个点是同一个位置时,才只加一次。
def diagonal_sum(mat): n = len(mat) total = 0 for i in range(n): total += mat[i][i] # 主对角线 total += mat[i][n - 1 - i] # 副对角线 if n % 2 == 1: total -= mat[n // 2][n // 2] # 重叠的中心元素减掉一次 return total这个写法有几个优点:
- 时间复杂度降到
O(n),空间复杂度O(1)。 - 主对角线和副对角线的累加逻辑清晰分离,代码一眼能读懂。
- 去重逻辑集中在最后一行,出错了也好排查。
我刷题这么多年,碰到"对角线"相关的问题,基本都是用这个思路先写一遍,再根据题目要求微调。
3.3 进阶写法:提前排除重叠(避免最后再减)
还有第二种单循环思路——在循环体内判断中心元素。好处是不用最后回头减一次,逻辑更内聚:
def diagonal_sum_adv(mat): n = len(mat) total = 0 for i in range(n): total += mat[i][i] if i != n - 1 - i: total += mat[i][n - 1 - i] return total这里用i != n - 1 - i来判断"当前行的副对角线元素是否和主对角线元素是同一个位置"。如果是同一个位置,就跳过副对角线累加。这个判断不需要额外考虑n的奇偶性,因为i != n - 1 - i自然在奇数阶的中心行不成立,偶数阶每一行都成立。
两种单循环写法都能AC,我个人更推荐3.2节那种"先全部加完再减"的版本,因为它把"是否重复"这个逻辑变成了一个显式的数学条件,读代码的人一眼就能看到奇偶性的处理。而i != n - 1 - i这种写法虽然更精简,但对初学者来说不够直观。
3.4 Java和C++的写法差异
Java版本大同小异:
class Solution { public int diagonalSum(int[][] mat) { int n = mat.length; int sum = 0; for (int i = 0; i < n; i++) { sum += mat[i][i]; sum += mat[i][n - 1 - i]; } if (n % 2 == 1) { sum -= mat[n / 2][n / 2]; } return sum; } }C++版本:
class Solution { public: int diagonalSum(vector<vector<int>>& mat) { int n = mat.size(); int sum = 0; for (int i = 0; i < n; i++) { sum += mat[i][i]; sum += mat[i][n - 1 - i]; } if (n % 2 == 1) { sum -= mat[n / 2][n / 2]; } return sum; } };语言差异不大,唯一的注意点是C++里vector<vector<int>>的size()返回的是size_t,如果直接和int混用,在某些编译器告警级别下会有符号比较警告。刷题平台一般不管,但如果你在公司笔试环境里跑,建议显式转成int n = mat.size();。
4. 这道"简单题"的坑:重叠元素的处理与失分点
4.1 典型错误汇总
我总结了几类在题解区和评论区高频出现的错误,每一种都反映了一个思维盲区:
| 错误类型 | 错误代码/行为 | 后果 |
|---|---|---|
| 忘记去重 | 两条对角线分别用两个循环累加,最后不扣中心元素 | 奇数阶时结果偏大 |
| 索引越界 | 副对角线写成mat[i][n - i] | 运行时报错或结果错误 |
| 条件判断顺序混乱 | if (i == j && i + j == n - 1)当作并集处理 | 漏掉只满足一条对角线的元素 |
| 想当然的对称性 | 认为副对角线是mat[i][n - i - 2] | 结果张冠李戴 |
第一个错误最隐蔽,因为n为偶数时结果正确,样例恰好给个偶数阶,直接误导你。第二个错误是"少减1"的经典问题,代入边界就能发现。第三个错误把or和and搞混,属于逻辑运算符基本功不扎实。第四个错误本质上是对坐标规律理解有偏差。
4.2 怎么在面试中表现这道题
这道题如果出现在面试里,面试官大概率不是为了考你算法,而是在看你的思考过程和代码习惯。我的建议是:
- 先说出暴力方案,并主动分析复杂度是
O(n^2)。 - 紧接着提出优化:因为主对角线和副对角线每行都恰好有一个元素,所以可以单循环
O(n)解决。 - 走到去重环节时,主动提一句"当
n为奇数时,中心元素(n/2, n/2)会被加两次,需要扣掉"。
你如果能主动说出这三点,在面试官眼里这道题你就过关了。最怕的是闷头写代码,写完只丢一句"通过了",没有任何分析过程。
4.3 一个小技巧:用极端用例自测
无论用什么解法,提交前养成自测的习惯。对于矩阵类题目,至少测三组输入:
n = 1,此时主对角线和副对角线完全重叠,答案就是mat[0][0]。n = 2,两条对角线不重叠,答案是两个对角线元素之和。n = 3,验证中心元素是否被正确减去一次。
我自己的习惯是,即使平台已经给了示例,也会额外构造一个n = 1的用例跑一遍。因为n = 1是边界中的边界,最容易暴露索引计算和去重逻辑的问题。
5. 复杂度分析与同类题延伸:把一道简单题的价值榨干
5.1 复杂度对比
| 方案 | 时间复杂度 | 空间复杂度 | 代码量 |
|---|---|---|---|
| 暴力双循环 + if判断 | O(n^2) | O(1) | 短,但边缘易错 |
| 双循环 + 去重 | O(n^2) | O(1) | 中等 |
| 单循环 + 去重 | O(n) | O(1) | 短,推荐 |
| 单循环 + 提前排除 | O(n) | O(1) | 短,逻辑内聚 |
空间复杂度都是O(1),因为只额外用了一个整数变量。区别在时间上:暴力法每个格子都要判断一次,单循环每行只处理两个元素。当n = 1000时,暴力法要判断100万个格子,单循环只需处理2000个元素,差距是三个数量级。这就是为什么即使是"简单题",也值得用更好的算法——因为数据规模一旦上来,暴力解法会立刻拖垮性能。
5.2 变体一:只求副对角线的和
这是最直接的变体。去掉主对角线的累加即可:
def anti_diagonal_sum(mat): n = len(mat) total = 0 for i in range(n): total += mat[i][n - 1 - i] return total这时不需要考虑去重,因为没有两条线交集的问题了。但要注意,如果你的循环里同时还保留了主对角线累加,就又会踩重复的坑。
5.3 变体二:非方阵矩阵怎么办
题目明确说了n x n,但如果面试官追问"如果矩阵是m x n怎么办",你可以这样分析:主对角线依然满足i == j,但副对角线需要重新定义。通常"副对角线"在非方阵中没有唯一标准定义,所以一般不会这样考。不过可以引申出一个思路:先明确对角线的定义,再根据定义写出坐标关系式,最后实现。这个思路适用于所有矩阵索引类的题目。
5.4 同类题延伸
碰上矩阵对角线或者边界遍历的问题,下面几道LeetCode题值得一起刷:
- 766. 托普利茨矩阵:判断每条对角线上的元素是否都相同。核心也是对角线的索引规律。
- 498. 对角线遍历:按对角线方向交替遍历矩阵,难度中等,是对角线规律的综合应用。
- 54. 螺旋矩阵:按螺旋顺序遍历矩阵,虽然不完全是"对角线"问题,但同样考察了方向控制和边界切换。
- 1572本身:作为入门题,用来建立"观察坐标规律"的直觉刚刚好。
刷这几道题时你会发现一个共同点:先把坐标规律写在纸上,再动手写代码。矩阵类题目最怕一上来就空想索引关系,画个3阶或4阶矩阵,把坐标都标出来,规律一目了然。
6. 写在最后的刷题心得
这道题我至少给初学者讲过五遍,每次都会强调一件事:简单题不等于可以随便写。你可以在10分钟内AC,但如果没有理解副对角线i + j = n - 1的推导过程,没有分析过重叠元素的条件,那这道题的价值就只值"一道AC记录",而不是"一次思维训练"。
我见过太多人刷了几百道题,遇到新题还是无从下手,原因就是刷题时只重数量不重方法。像1572这种简单题,恰恰是建立"坐标规律思维"的最佳练习素材。它不涉及复杂的算法,纯粹考察你能不能把一个矩阵的几何特征转化成简洁的索引公式。
我个人推荐的做法是:把这道题放在"数组/矩阵索引规律"这个分类下,和766、498放一起刷。刷完之后自己尝试改几个变体——比如要求对角线乘积、要求返回两条对角线的最大值、要求对两条对角线分别求和再比较大小。每改一个变体,你对索引规律的理解就更深一层。
最后分享一个小技巧:写矩阵下标的时候,遇到减法索引,比如n - 1 - i,在注释里写清楚"副对角线列号",这样即使过几个月回看代码,也能一眼读懂当初的意图。好的代码不只是机器能跑,更是半年后的自己能看懂。