求两个整数最大值:入门题里的边界、抽象与工程扩展 求两个整数中的最大值这道题基本是每个学编程的人都会碰到的入门题。我在给新人做 Code Review 时也经常看到它的各种变体说实话能把这道简单题写利索的人真的不多。它到底能有多简单写过一两周代码的人闭着眼都能写比较两个数把大的那个返回。但正是这种题目最容易暴露一个人的基本功——有没有分支思维会不会做函数抽象懂不懂边界情况甚至有没有认真想过整数这个词意味着什么。这篇文章就把这道题里里外外拆一遍从各语言写法、边界场景、工程扩展讲到调试技巧适合所有想把这题吃透的初学者也适合陪跑新人的老手用来当复盘素材。1. 先别急着写代码这道题的考点根本不是比大小1.1 为什么所有教材都爱拿它当入门题几乎每一本程序设计教材讲到分支结构那一章都会安排一道求两个整数最大值的练习。原因很简单它是最小的、能完整展示决策的代码单元。你不需要理解数组、指针、类这些复杂概念只需要明白一件事——程序里有一种机制可以根据条件的真假走不同的路。这个机制是所有逻辑的起点而两数取大恰好是描述它最直观的例子。更进一步说这个题练的不只是 if。一个完整的最小程序还需要输入、计算、输出三个环节。从键盘读入两个整数经过一个选择结构把较大者打印出来。这一套流程几乎涵盖了所有命令式程序的基本骨架。所以它不是比大小这么简单它是你第一次接触到输入-处理-输出完整数据流的训练场。1.2 同一个题目三种段位的写法我在 Code Review 里见过同一个题目的三种写法。第一种是刚学三天的萌新写出来的代码是这样的#include stdio.h int main() { int a, b; scanf(%d %d, a, b); if (a b) printf(%d\n, a); else printf(%d\n, b); return 0; }这种写法本身没错但它把求最大值的逻辑和输出耦合在一起了。如果明天需求变成先求最大值再做别的操作这段代码就没法复用。第二种是老手写的会用函数抽象int max_int(int a, int b) { return a b ? a : b; }把比较逻辑单独拎出来输入输出交给调用方这才算把这道题做对了。第三种是考虑过边界和习惯的写法int max_int(int a, int b) { return a b ? a : b; }有人会问 还是 有区别吗真较起真来a 和 b 相等时返回谁都一样但用 在语义上更严谨——当 a 不小于 b 时取 a覆盖了所有情况读代码的人不用去推理 else 分支代表什么。这三种写法的差距恰好对应了一句话入门的人写功能进阶的人写抽象资深的人写语义。后面要讲的所有内容都是在语义这两个字上做文章。2. 五种主流语言的惯用写法与各自的性格2.1 C语言三目运算和宏一个甜一个苦C 语言里最标准的答案是三目运算符return a b ? a : b;。三目运算本质上是一个表达式可以嵌在赋值、返回值里比 if-else 更紧凑。很多讲 C 的书会告诉你能不用三目就不用三目因为可读性差但取两个数最大值这个场景恰恰是三目最合适的地方——条件、真值、假值都短一眼看得完。有些不那么严谨的教材会建议用宏#define MAX(a, b) ((a) (b) ? (a) : (b))这里有个非常经典的坑。宏是预处理阶段的文本替换它不检查类型也不保存中间值。如果你写MAX(x, y)预处理器会把它展开成((x) (y) ? (x) : (y))如果x确实大于yx 会被自增两次。更隐蔽的是如果比较的两个表达式带副作用你根本没法预测执行了几次。我的建议是宏版本只适合在竞赛题里快速套用生产代码老老实实写函数或者用inline、泛型宏。这个话题后面专门展开这里先记住一个结论——宏给你便利也给你定时炸弹。2.2 Python、Java、Go 和 JavaScript 的标准答案Python 的风格和 C 完全不同。它鼓励你直接用现成的东西所以最地道的写法其实是def max_of_two(a, b): return max(a, b)内置的max()函数支持任意多个参数也支持传一个可迭代对象进去。有人觉得这样算作弊但 Python 的哲学就是内置优先。如果一定要练逻辑也可以用条件表达式return a if a b else b。不过在生产代码里我建议直接用内置max因为标准库的实现考虑了泛型、性能和各种边界比自己手写稳得多。Java 里同样有Math.max(a, b)它针对int、long、float、double各有一组重载JIT 在热点路径上还会做针对性优化。Go 语言直到 1.21 才提供内置的min/max此前标准做法是func maxOfTwo(a, b int) int { if a b { return a } return b }Go 的哲学是少魔法多显式所以哪怕一个几行的函数也要求你老老实实写。JavaScript 那边则简单粗暴Math.max(a, b)但要注意它会把参数隐式转成数字如果你不小心传了字符串10它也能给你 10。各语言风格对比如下语言推荐写法风格特征注意事项C三目运算符或函数显式、贴近底层别用裸宏处理带副作用的参数Python内置 max()内置优先、简洁参数名别取 max会遮蔽内建函数JavaMath.max(a, b)标准库完善注意包装类型的空值Goif 实现显式优先泛型版本 1.21 之后可用JavaScriptMath.max(a, b)隐式转换传数组要用展开运算符注意超大数组栈溢出2.3 为什么我更推荐你手写一遍看到这你可能会问既然 Java、Python 都有现成函数那我为什么要手写因为现成函数帮不了你的学习过程。手写一遍会让你理解比较的本质理解分支的执行路径也才能在将来遇到自定义对象、浮点精度、未定义比较规则时知道该怎么做。另一个原因是很多场景不给你用现成函数的机会。比如你在写嵌入式固件编译器不支持完整标准库比如你在面试中被要求在纸上写个不含任何标准库调用的版本再比如你要在自己定义的类上取最大值就得自己实现比较逻辑。我的建议是先用内置函数解决问题再至少手写一遍纯逻辑版本保证两件事你都能做。这样既高效又不至于被工具架空。3. 最容易翻车的边界场景从32位整数溢出谈起3.1 整数不是无限大的32位尤其如此热搜词里有个32位有符号整数这让我想起一个很常见的误解初学者总觉得int就是数学里的整数可以随便存。实际上 C、Java 等语言里的 32 位有符号int取值范围是 -2147483648 到 2147483647。一旦运算结果越过这个边界就会发生溢出行为可能让你崩溃。好消息是return a b ? a : b;这种写法只做比较和原值返回本身不会让数值越界——a 和 b 原本就合法那么返回值也一定是二者之一不会凭空产生新数。坏消息是如果你的求最大值前面还有别的计算例如先求abs(a)再比较那就要小心了abs(-2147483648)在 32 位 int 里根本存不下这已经算未定义行为。还有人在算(a b) / 2这样巧妙的中间值时溢出这些都是典型的边界坑。如果你处理的是超出 64 位范围的超大整数比如大整数加法、大整数乘除法的中间结果普通的int、long早就装不下了得动用任意精度库或专门的高精度数据结构。这时候比大小的逻辑还是一样但数据类型换成了自定义结构比较函数得自己写复杂度也要从千万位级别的数字里去比较——这也是热词里大整数加法并行这类话题的起点。3.2 无符号、浮点数与 NaN比大小也存在语言歧义C 语言里有个经典面试题if (-1 1U)结果是什么按数学直觉应该是假但正确答案是真。原因在于1U是无符号整数C 的类型转换规则会把-1转成无符号数变成一个巨大的正数于是比较结果被翻转了。这就是整数二字在不同上下文里的不同含义。所以 C 语言里做比较前最好显式确认两个操作数的符号性一致。浮点数更是重灾区。0.1 0.2 0.3这种问题大家听得多但在取最大值时也不算完——如果你要在一组数据里取最大值而数据里出现了NaN在很多语言里任何和NaN的比较都会返回假导致最大值怎么比较都不更新。处理办法是先过滤掉NaN或者在比较时写死碰到NaN就跳过。JavaScript 里Math.max遇到NaN会直接返回NaN这也是一个容易踩的坑。3.3 宏定义的二次求值问题值得单开一节前面提过的 C 语言宏#define MAX(a, b)展开后会出现二次求值的问题。看这个例子int x 1; int y 2; int m MAX(x, y);宏展开后变成((x) (y) ? (x) : (y))如果x先把 x 变成 2而 2 2 不成立那么结果取 yx 只自增了一次看起来还好。但把 y 改成 3那么x y为真时 x 会变成 2随后三目运算符的真分支又执行一次xx 变成 3。你在调用处只写了一句话x 却变了两回这就是典型的非预期副作用。避免方法有几种一是严格给宏参数加括号但只能解决运算符优先级解决不了副作用二是改用inline函数编译器在优化时同样能内联展开但没有宏的文本替换问题三是利用编译器扩展或泛型宏实现类型安全版本。我的日常建议很朴素函数优先宏最后才考虑。4. 从两个数到一堆数工程里更常遇到的最大值问题4.1 数组遍历与分治把这道题推广成 n 个数实际开发中你很少只面对两个整数。更多情况是给你一个数组让你求最大值。实现方式无非两种循环遍历和分治。循环遍历是 O(n)空间 O(1)代码闭着眼都能写def max_of_list(nums): ans float(-inf) for n in nums: if n ans: ans n return ans分治则把数组切成两半分别求最大值再合并虽然复杂度也是 O(n)纯粹是为了练习递归思维。这里我要提一句热词里的数组求区间最大值的算法题它和求整个数组最大值不一样。区间最大值通常指任意给定[l, r]区间内的最大值如果用遍历每次都是 O(n)面对大量查询就扛不住了。这时候就需要线段树、ST 表这类数据结构预处理后单次查询能做到 O(log n) 甚至 O(1)。这类问题的起点仍然是比较两个数取大这个原子操作——只是把它塞进了树形结构里。4.2 数据库里的 MAX你查到的真的是你想要的吗仁者见仁数据库层面的最大值问题同样常见。MySQL 的整数类型有 TINYINT、SMALLINT、MEDIUMINT、INT、BIGINT占用字节分别是 1、2、3、4、8对应取值范围差异极大。如果你把需要存到十亿级的数据塞进 INT写行时就可能直接报字段溢出。这和代码里int溢出一个道理只是换成数据库来提醒你。取某列最大值SQL 写起来很简单SELECT MAX(score) FROM students;但这里面有个容易被忽略的小坑MAX 默认只比较列的值不比较别的字段。如果你想查出分数最高的那个学生的完整记录直接WHERE score MAX(score)是写不出来的需要用子查询或排序SELECT * FROM students ORDER BY score DESC LIMIT 1;这类写法在高频面试里经常出现。另外像 Stata 这类统计软件也提供max、min命令本质上是把维护最大值的逻辑封装成了一个命令让你不用自己写循环。原理相通只是应用场景不同。4.3 滑动窗口最大值与 Top-K换了马甲还认识吗热词里的滑动窗口最大值是 LeetCode 第 239 题给你一个数组和一个窗口大小 k窗口每次向右移动一格要你输出窗口内所有元素的最大值。暴力做法是每格遍历 k 个元素复杂度 O(nk)数据一大就超时。经典解法是单调队列维护一个从队头到队尾逐渐减小的队列窗口滑动时把新元素从队尾塞进去同时丢掉队尾那些比它小的元素因为它们永远不可能再作为最大值出场。这样每个元素最多进队出队一次整体复杂度降到 O(n)。你会发现这道题的每一个步骤还是在做两个数比大小只是通过队列这个容器把比较结果缓存下来了。同样的思维还能推到n 个数中确定第 k 个最大值——也就是 Top-K 问题用堆实现时堆内部的每次调整也是依靠比较。所有这些问题共享同一个底层逻辑比较是原子的数据结构决定效率而求两个整数的最大值就是那个原子。5. 写错这道题的人比你想的多一份避坑清单5.1 典型错误与真实案例我见过不少新人在这道题上犯一些低级错误归纳起来其实就几类。第一类是 C 语言的分号问题。if (a b);后面多个分号if 就直接结束了后面的花括号变成了普通代码块输出结果永远是整段代码执行完的最终值。这类错误很难一眼看出来因为编译器不报错。第二类是宏参数没加括号。#define MAX(a, b) a b ? a : b遇到MAX(a 1, b)就出问题因为表达式会变成a 1 b ? a 1 : b看起来好像也对但如果你再参与别的运算优先级立刻乱套。第三类是命名冲突。Python 里有人把函数参数叫max结果在函数内部调用max(a, b)时解释器把max当作本地变量直接报错或拿到一个奇怪的类型。第四类是用浮点数比较大小却不考虑精度误差。如果你在处理金额、坐标这类数据两个相等的浮点数在二进制表示里可能不相等导致取最大值时选了看起来一样但实际略微不同的那个。5.2 一份可以直接抄的测试用例很多人写完题目就直接交差也不测边界。这是我从做这道题开始就养成的习惯无论题目多简单至少准备一组覆盖边界的测试数据。如果是求两个整数最大值我建议至少覆盖两数都是正数且左边较大、右边较大各一组两数都是负数验证比较方向没有写反一正一负两数相等其中一个是最小值-2147483648另一个是最大值2147483647如果是浮点数版本补一个NaN用例明确期望行为。我甚至会把测试写成一个简单的断言脚本每次改动都跑一遍。比如def test_max_int(): assert max_of_two(1, 2) 2 assert max_of_two(2, 1) 2 assert max_of_two(-1, -2) -1 assert max_of_two(-1, 1) 1 assert max_of_two(5, 5) 5 assert max_of_two(-2147483648, 2147483647) 2147483647这些用例的价值不只是防止写错更是让你在重构代码时有个安全网。哪怕后面把循环改成 reduce、把函数改成 lambda只要测试不通过就得停下来想清楚。5.3 从 Code Review 视角看这道题最后聊聊 Code Review。我自己评审代码时看到一个两数取最大值的实现会依次问三个问题第一逻辑是不是抽成了独立函数还是裸写在调用方里第二边界情况有没有考虑尤其是类型范围和空值第三命名和注释是否准确——函数叫max_of_two还是getBiggerValue都行但千万别叫handleData这种毫无信息的名字。前两个问题偏技术第三个问题偏工程素养。可以说一道两行代码的基础题足以看出一个人有没有养成函数不是写给自己看的是写给半年后的自己和其他协作者看的这个意识。这也是我为什么愿意花一整篇来聊这个初级题目的原因。最后说点个人的体会。这几年我带过的实习生、新同事加在一起也有几十个了我特别爱在第一次聊技术时让他们写这道题。不是为了考倒谁而是因为从这道题能迅速判断出几个关键习惯会不会写测试、知不知道边界、有没有函数抽象意识。很多时候代码的差距不在天赋就在这些最基础的环节上——你把求两个整数的最大值写好了后面写复杂逻辑时至少不会在最底层踩坑。建议你也拿这道题当个自测工具用不同语言各写一遍再补上边界测试收获会比预期大得多。