资讯详情

C语言求5×5矩阵鞍点:定义、解法与常见错误全解析

📅 2026/10/10 8:19:00 | 华诺云谱 👁 阅读
C语言求5×5矩阵鞍点:定义、解法与常见错误全解析
1. 这一题到底在考什么5×5矩阵鞍点的准确定义前两天在技术群里看到有人发了一道题说是菜鸟教程C经典100例的练习52用C语言算一个5×5矩阵的鞍点。发代码的人自己调了半天输出要么是错的要么什么都不输出。我扫了一眼他的代码第一反应是——这题不是不会写是题目本身的理解出了问题。1.1 鞍点的数学定义与直观理解鞍点这个名词听起来有点唬人但定义其实很简单在一个二维矩阵里某个位置上的元素在它所在的那一行里是最大的同时在它所在的那一列里是最小的。把这个位置叫鞍点。举个例子下面这个5×5矩阵1 2 3 4 5 2 3 4 5 6 3 4 5 6 7 4 5 6 7 8 5 6 7 8 9右下角的9就是鞍点。它位于第5行在第5行里9是最大值它同时位于第5列在第5列里9也是最小值因为这一列是5、6、7、8、9递增的。这个位置同时满足“行内最大”和“列内最小”两个条件。为什么要叫“鞍”点你可以想象一个马鞍的曲面沿一个方向看它是最高点沿另一个方向看它又是最低点。矩阵里的鞍点也是这个意思沿着行方向看它是这行的“山顶”沿着列方向看它是这列的“谷底”。这题值得注意的地方有三个鞍点不一定是整个矩阵的最大值或最小值。它只是局部行和局部列上的极值。懂了这个你才不会在代码里写成“先找全局最大再判断”那是错的思路。矩阵不一定有鞍点。随机生成的5×5矩阵大概率一个鞍点都没有。所以程序必须处理“不存在鞍点”的情况输出相应提示而不是什么都不输出。鞍点可能有多个。比如一个5×5矩阵所有元素都是同一个数那每个位置既是行最大又是列最小一共25个鞍点。所以程序不应该找到一个就立刻结束而要把所有满足条件的位置全部输出。1.2 为什么这道练习离不开stdio.h和limits.h练习标题里特别强调了两个头文件stdio.h和limits.h。stdio.h大家都能理解scanf、printf都在里面输入输出靠它。limits.h的用途则容易被忽略但恰恰是这道题容易翻车的地方。我在群里看到的那份代码问题就出在这里。他写的是int max 0;然后用这个max去跟当前行的每个元素比较。表面上看没问题但如果这一行的所有数全是负数比如{-1, -2, -3, -4, -5}那这个max初始值0从头到尾都比任何一个元素大循环结束后max依然是0行最大值的候选人直接变成了“矩阵里不存在的0”后面的列验证全部白做。正确的做法是用limits.h里提供的INT_MIN来初始化行最大值int max INT_MIN;INT_MIN是int类型能表示的最小值在多数编译环境下是-2147483648。任何合法输入都比它大这样不管矩阵里是正数、负数还是0第一次比较就能正确地把第一个元素设为候选最大值。同理如果要求列最小值初始化就应该用INT_MAX。这个细节就是练习52的真正考点之一不是考你鞍点算法有多复杂而是考你有没有意识到“极值初始化不能拍脑袋写0”。1.3 最常见的错误理解把“行最大”当成唯一条件还有一个高频错误就是只算“每行的最大值”然后直接输出位置完全不管“该位置在列上是否最小”。比如上面那个递增矩阵如果只按行找最大值第1行最大是5第5列第2行最大是6第5列依此类推输出一串位置但其中真正满足列最小条件的只有右下角的9。用错误算法的输出结果有一部分是“看似合理实则错误”的。所以这一题的核心可以概括成一句话先找到每一行的最大值候选位置再去这些候选位置所在的列上做“最小值”校验。两步缺一不可。2. 按部就班的经典解法先找行内候选再做列验证理解定义之后最自然的写法就是两步走。这也是教科书风格的解法逻辑直观适合作为第一版实现。2.1 第一步逐行扫描找出最大值所在列用一个外层循环控制行号i内层循环扫第i行的所有列找出该行的最大值并记录它所在的列号col。这里有一个之前提到的关键点用INT_MIN初始化max而不是用0。2.2 第二步对候选列做最小值校验拿到候选位置[i][col]后不能直接输出。需要固定列号col把整个列从上到下扫一遍确认matrix[i][col]是不是这一列里的最小值。如果这一列里有任何一个元素比它小那它就不是鞍点。我见过很多初学者在这个环节犯的一个错误是找到行最大值后马上用“这个元素是否比左边右边的元素大”来验证。这是多余的因为你在第一步已经通过行扫描保证它是行内最大了。第二步需要做的只是“换一个方向再验证一次”。2.3 易错点同一行出现多个最大值时必须全部检验这里有一个细节大部分教材都不会专门讲但我建议你认真看。如果一行里有多个相同的最大值比如这一行是{3, 5, 1, 5, 2}那么第2列和第4列都是最大值候选人。这两个位置都要拿去列上验证。只验证其中一个可能刚好漏掉真正的鞍点。我第一次写这道题的时候就犯过这个错误。我的第一个版本用了一个整数col来记录最大值所在列如果遇到matrix[i][j] max就更新它等于max时不处理。结果矩阵正好出现一行两个相同最大值而真正的鞍点恰好在后一个位置程序没输出。表面上看代码“逻辑没错”实际上漏了分支。正确的处理方式是先把这一行里所有的最大值列号都记下来再逐个做列验证。下面是我整理的第一版完整可运行代码支持输出所有鞍点而不是找到一个就停#include stdio.h #include limits.h #define ROWS 5 #define COLS 5 int main() { int matrix[ROWS][COLS]; int i, j; printf(请输入%d行%d列的矩阵元素\n, ROWS, COLS); for (i 0; i ROWS; i) { for (j 0; j COLS; j) { scanf(%d, matrix[i][j]); } } int found 0; for (i 0; i ROWS; i) { int maxVal INT_MIN; int maxCols[COLS]; int maxCount 0; // 第一遍找出第i行的最大值 for (j 0; j COLS; j) { if (matrix[i][j] maxVal) { maxVal matrix[i][j]; } } // 第二遍记录所有等于最大值的列号 for (j 0; j COLS; j) { if (matrix[i][j] maxVal) { maxCols[maxCount] j; } } // 对每一个候选位置做列最小值校验 for (int k 0; k maxCount; k) { int col maxCols[k]; int isMinInCol 1; for (int r 0; r ROWS; r) { if (matrix[r][col] maxVal) { isMinInCol 0; break; } } if (isMinInCol) { printf(鞍点matrix[%d][%d] %d\n, i, col, maxVal); found 1; } } } if (!found) { printf(该矩阵不存在鞍点。\n); } return 0; }这段代码的运行逻辑可以拆成三层外层循环负责“按行找候选”中间层负责“记下所有候选列”内层负责“在列上做最终验证”。三个循环各司其职出错时也容易定位。这个版本的时间复杂度是 O(n × m × (n m))对每一行找最大值要扫m个元素n行就是n×m对每个候选列再验证m列上的n个元素最坏情况下每行m个候选所以整体复杂度在5×5这种固定小矩阵下完全够用。但如果以后要处理大矩阵这个复杂度就会显得笨重。3. 另一种更漂亮的实现预处理行列极值一遍遍历出结果教科书版本思路清晰但代码量稍大。其实这道题存在一个更简洁的做法先一次性把所有行的最大值、所有列的最小值算出来然后再遍历一遍矩阵凡是同时等于“本行最大值”和“本列最小值”的位置就是鞍点。3.1 用rowMax和colMin两个数组把复杂度降到O(n×m)思路是这样的开一个数组rowMax[i]存第i行的最大值开一个数组colMin[j]存第j列的最小值输入矩阵的同时顺手填充这两个数组最后做一次双层循环判断matrix[i][j] rowMax[i] matrix[i][j] colMin[j]是否成立。这个思路绕开了“先找候选再验证”的嵌套式结构把行和列的极值信息提前提取出来最终判断条件只是一个非常干净的表达式。判断条件的含义可以这样理解如果某个位置的值等于它那一行的最大值说明它是行内最大同时又等于它那一列的最小值说明它是列内最小。两个条件同时成立就是鞍点。这个版本天然支持多个最大值、多个最小值、多个鞍点的情况不需要额外记录“所有候选列”因为最终判断是针对每个位置独立进行的。3.2 完整代码对照#include stdio.h #include limits.h #define ROWS 5 #define COLS 5 int main() { int matrix[ROWS][COLS]; int rowMax[ROWS]; int colMin[COLS]; // 初始化极值数组 for (int i 0; i ROWS; i) { rowMax[i] INT_MIN; } for (int j 0; j COLS; j) { colMin[j] INT_MAX; } printf(请输入%d行%d列的矩阵元素\n, ROWS, COLS); for (int i 0; i ROWS; i) { for (int j 0; j COLS; j) { scanf(%d, matrix[i][j]); // 输入的同时更新行最大值 if (matrix[i][j] rowMax[i]) { rowMax[i] matrix[i][j]; } // 输入的同时更新列最小值 if (matrix[i][j] colMin[j]) { colMin[j] matrix[i][j]; } } } int found 0; for (int i 0; i ROWS; i) { for (int j 0; j COLS; j) { if (matrix[i][j] rowMax[i] matrix[i][j] colMin[j]) { printf(鞍点matrix[%d][%d] %d\n, i, j, matrix[i][j]); found 1; } } } if (!found) { printf(该矩阵不存在鞍点。\n); } return 0; }注意这里有一个很实用的小技巧行列极值的更新是在scanf读入数据的循环里顺便完成的不需要额外再写两个双重循环去扫描矩阵。输入结束后rowMax和colMin就已经是最终结果了。3.3 两种解法对比与选择建议两种解法跑同一个输入输出完全一致。区别主要在三个方面对比项解法一候选列验证解法二预处理极值代码直观性步骤清晰适合教学思路更抽象但代码更短时间复杂度O(n×m×(nm))O(n×m)多鞍点处理需要额外数组记录候选列天然支持适合场景初学者理解算法过程比赛或实际项目中推荐我个人建议如果你是在学习阶段优先把解法一写一遍。因为解法一强迫你思考“候选”和“验证”这两个阶段这对理解双层循环和二维数组的索引关系帮助很大。写通了解法一再改成解法二会非常快而且你能理解为什么预处理能省掉一层循环。如果你已经有了一定基础直接写解法二就可以。面试时被问到“如何找矩阵鞍点”解法二是更漂亮的回答。4. 我实际调试时踩过的三个坑代码写出来是一回事真正跑起来是另一回事。我在写这道题和帮别人改代码的过程中遇到最多的坑有三个全部是那种“编译能过、逻辑看着对、输出就是不对”的类型。4.1 坑一用0初始化最大值负数矩阵直接全废前面已经提到过这个坑但我想再展开说说它的具体表现。假设输入矩阵是-1 -2 -3 -4 -5 -2 -3 -4 -5 -6 -3 -4 -5 -6 -7 -4 -5 -6 -7 -8 -5 -6 -7 -8 -9用int max 0;去逐行找最大值每一行的max在扫描结束后依然还是0因为行内所有元素都比0小。于是程序认为每一行的最大值是0然后拿着这个“根本不在矩阵里的0”去列上验证结论当然是找不到鞍点输出“不存在鞍点”。如果矩阵里恰好有负数也有正数比如第1行是{-5, 3, -4, 2, 1}那最大值的候选能正确找出3问题暂时被掩盖。但一旦某行全是负数马上翻车。解决办法行最大值一律用INT_MIN初始化列最小值一律用INT_MAX初始化。这是使用limits.h的核心意义不是随便引入一个头文件充门面。4.2 坑二行和列的索引颠倒二维数组的索引顺序是matrix[行][列]这个大部分人不会搞错。但写出matrix[i][j]之后到列验证环节就乱了。我见过一份代码行验证里写的是matrix[i][j]列验证里一顺手也写成matrix[i][j]结果整个验证过程还是在同一行里打转完全没有去做“固定列、扫描行”的操作。调试的时候打印i和j才发现列验证循环里行的变化根本没体现出来。正确写法是候选位置是[i][col]列验证时要遍历行号所以应该用matrix[r][col]其中r从0到ROWS-1变化。固定的是列号col变化的是行号r。4.3 坑三判断条件里和混用导致逻辑偏差解法二的最终判断是if (matrix[i][j] rowMax[i] matrix[i][j] colMin[j])有次帮人改代码看到他写的是if (matrix[i][j] rowMax[i] colMin[j] rowMax[i])表面上看好像也合理如果当前位置等于行最大值同时列最小值也等于行最大值那三者相等当前位置自然也是列最小值。但如果这一列里有多个相同的最小值而当前行最大值恰好等于列最小值这个判断也能成立。数学上两式等价代码风格上却容易引人误解。真正容易出问题的是另一个变体。有人为了省事写成if (matrix[i][j] rowMax[i] colMin[j])在C语言里是左结合运算符这个表达式会先计算matrix[i][j] rowMax[i]得到一个0或1的整数结果再拿这个0或1去跟colMin[j]比较。假如colMin[j]正好等于1而matrix[i][j] rowMax[i]的结果刚好是1这个判断就可能离奇地成立假如colMin[j]是0那整个表达式几乎永远为假。这种写法属于典型的一行代码埋雷必须拆开写成连接的两个独立判断。5. 三组可以抄走的测试用例与验证方法代码写完不是终点测试才是真正让你有信心交付的过程。下面几组测试用例我都实际跑过可以直接复制。5.1 递增矩阵必定有鞍点输入1 2 3 4 5 2 3 4 5 6 3 4 5 6 7 4 5 6 7 8 5 6 7 8 9输出鞍点matrix[4][4] 9这个矩阵右下角一定是鞍点原因我在开头分析过每一行向右递增每一列向下递增右下角既是最后一行最大值也是最后一列最小值。用它做冒烟测试能快速确认程序主流程没跑偏。5.2 全等矩阵验证多鞍点输出输入7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7输出应该是25个鞍点每个位置都满足条件。如果你的程序只输出一个就停了说明“找到一个鞍点就return”的逻辑有问题。这组用例专门用来检验多鞍点处理。5.3 无鞍点矩阵怎么构造想让矩阵没有鞍点最简单的办法是让矩阵的某个值“行最大但列上不是最小”同时破坏所有候选位置。这里给一组我验证过的输入2 1 3 4 5 1 2 1 3 4 3 1 2 5 6 4 3 5 1 2 5 4 6 2 1跑一下你会看到输出“该矩阵不存在鞍点”。构造这种矩阵不需要什么精妙技巧我通常是先随便填一个5×5矩阵跑一遍程序如果有鞍点就微调那个位置的值破坏它的“列最小”属性多试几次就能得到无鞍点用例。5.4 手工模拟一次循环把程序的每一步走通上面这些用例跑通后我建议你挑其中一组拿纸笔手工模拟一遍程序的执行过程。比如用递增矩阵走到i0这一行时rowMax[0]是5候选列是4列验证时发现第5列是{5,6,7,8,9}5不是最小值所以第一行没有鞍点。继续走i1rowMax[1]是6候选列还是4列验证同样失败……直到i4rowMax[4]是9候选列是4第5列最小值是9条件成立输出鞍点。这个模拟过程只需要几十秒但对理解二维数组的索引关系帮助极大。6. 从5×5到任意N×M扩展玩法与思考题练习52只是菜鸟教程100例里的一小步但鞍点问题本身可以延伸出不少有意思的东西。如果5×5已经做熟了下面这些方向值得再花点时间研究。6.1 用宏定义摆脱固定尺寸解法二的代码里用了#define ROWS 5和#define COLS 5要改成任意N×M矩阵只需要改这两个宏或者把下面的逻辑提取成函数。void findSaddlePoints(int matrix[][COLS], int rows, int cols) { // 把 main 里的核心逻辑搬进来参数化行列数 }顺便说一句函数形参里写int matrix[][COLS]时COLS必须是编译期常量所以宏定义常量在这里是必要的。这也是为什么很多人一做大矩阵就用动态数组或者一维数组模拟二维本质上是想绕开这个限制。但作为练习题固定尺寸反而更容易专注在算法本身上。6.2 变种一反鞍点把题目条件反过来某位置是行最小、列最大叫反鞍点。实现思路完全对称只要把rowMax改成rowMin、colMin改成colMax初始化时分别用INT_MAX和INT_MIN判断条件改成if (matrix[i][j] rowMin[i] matrix[i][j] colMax[j])感兴趣的话可以试试把两个功能写进同一个程序里用一份输入同时输出鞍点和反鞍点训练自己对称思考的能力。6.3 变种二把“点”升级成“行”有些教材的扩展题会问是否存在某一行它的最大值恰好也是某一列的最小值并且这个位置不止一个这类题本质上就是“多鞍点问题”的变体解法二的预处理思路可以直接复用。更进一步的玩法是判断“鞍行”某一行里存在一个元素它同时是该行最大值和所在列最小值。此时鞍点可能不存在但鞍行可能有一整行满足条件。这种题的实现策略是把每行是否满足条件记录成一个标志数组本质上还是极值预处理那一套。6.4 延伸这和“找出每行最大值”系列题目的关联练习52做完之后你可以把同系列的题目串起来看找每行最大值、找每列最小值、求对角线和、矩阵转置……这些都是二维数组的基本功。鞍点题之所以被选进经典100例正是因为它把“行扫描”和“列扫描”两种遍历模式组合在了同一道题里还把极值初始化的坑埋了进去。把这些基本功吃透后面遇到图像处理、动态规划里的矩阵题目你会发现自己对二维数组的掌控力明显不一样。我个人在写完这题之后最大的体会是找鞍点的算法一点都不难难的是把“同时满足两个方向条件”这个逻辑表达清楚并且处理好边界情况。如果你能不看答案独立把解法二写出来并跑通三个测试用例那这一关就算真正过了。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑