☰
6和9组成的最大数字:LeetCode贪心算法与字符串处理全解析
2026/10/18 7:37:57 网站建设 项目流程

这题很直白,但有些同学交上去看到耗时那栏写着 100ms,心里就开始打鼓了。LeetCode 1323 Maximum 69 Number,题目英文原名就是“6 和 9 组成的最大数字”,解题思路五分钟能讲完,但真正要把代码写稳、把边界考虑全、再顺手把性能调一下,还是有几个值得说道的细节。这篇文章就把这道题从读题到优化整个过一遍,适合刚刷题的同学,也适合想回头看看自己解法有没有多余操作的老手。

1. 题目到底在问什么

1.1 原题描述与示例

题目翻译过来是这样的:给你一个正整数,它只由数字 6 和 9 组成。你最多只能把其中一个数字翻转,翻转的意思就是把 6 变成 9,或者把 9 变成 6。问翻转后能得到的最大数字是多少。

等一等,题目说的是“最多翻转一个数字”,也就是说你也可以选择不翻转,保持原样。既然目标是让数字尽可能大,那不翻转只在原数字本身就是最大可能值时才成立,比如输入已经是 9999,你翻了反而变小,这时候就保持原样。

再看两个标准示例:

  • 输入:9669
    输出:9969
    解释:把第一位 6 翻成 9,变成 9969,这是所有单次翻转结果里最大的。

  • 输入:9996
    输出:9999
    解释:把最后一位 6 翻成 9,变成 9999。

  • 输入:9999
    输出:9999
    解释:不翻转。

1.2 手工推导一遍

给你一个数 669669,手动算一下所有可能的翻转结果:

  • 翻第 1 位:969669
  • 翻第 2 位:699669
  • 翻第 3 位:669969
  • 翻第 4 位:669699
  • 翻第 5 位:669669
  • 翻第 6 位:669669
  • 不翻转:669669

肉眼扫一遍,最大的是 969669。你发现规律没有?最大结果就是从左往右遇到的第一个 6,把它变成 9。为什么?因为数字比较大小,是从最高位开始逐位比的。你把最高位从 6 提到 9,无论后面怎么变,都比“把低位的 6 变 9”带来的收益大。这就是这道题的核心。

1.3 难点:简单题也有坑?

说句实话,这题在 LeetCode 上属于 Easy 难度,算法层面没有任何弯弯绕。但坑不在算法,而在实现细节上。我见过不少同学第一反应是:

  • 把数字转成字符串,然后遍历,找到第一个 '6',给它替换成 '9',再转回数字。
  • 转来转去的过程中,忘记变量名复用,导致结果没更新。
  • 上来就写循环,结果没处理“全是 9”的情况。

这些问题单独拎出来都不难,但真在上机的时候,紧张起来就容易写漏。尤其是“最多翻转一次”这个条件,很多同学理解成“一定要翻转一次”,结果输入 9999 的时候返回 9999 倒是没错,但代码里可能会先转成列表,找不到 '6',然后没做任何操作,最后还是返回原数。逻辑上没问题,不过如果写成“找到第一个 6 后立刻返回”的写法,忘了处理找不到的情况,就会在极端用例上报错。

2. 核心思路:贪心就是“从左往右把第一个 6 改成 9”

2.1 为什么第一个 6 是关键

这里涉及的是贪心算法的直观体现。你要让整个数值最大,第一位权重最高。如果第一位是 6,那么把它变成 9,数字立刻增加了 3×10^(n-1),这个增量是任何低位改动都比不上的。

如果第一位已经是 9,那就看第二位,第二位是 6 就翻第二位,依次类推。换句话说,从左到右扫描,找到第一个字符 '6',改成 '9',扫描结束。如果整个数全是 9,那就不用改,原数本身就是最大值。

用一个生活类比:你要给一个四位密码锁的某个数字加 3,为了把最终密码锁代表的数字猜得最高,你当然优先动千位而不是个位。千位上的数字加 3,哪怕个位掉 3,整体还是变大。但这里只能加在 6 上,6 加 3 变 9,9 不能动,否则变小。所以策略非常简单:能加在最高位就绝不加在低位。

2.2 字符串法 vs 数学法

很多题解会给你两种写法,一种是转字符串处理,一种是用取余和除法从低位找第一个 6。但请注意,这里“从低位找第一个 6”不是让你从右往左改,而是说数学法里可以先从低位往高位探测,记录最低的那位 6 的位置,最后再统一计算加上的值。

字符串法的逻辑最符合人类直觉:

  1. 把数字 num 转成字符串 s。
  2. 把 s 转为字符数组 arr。
  3. 从左到右遍历 arr,找到第一个 char 为 '6' 的位置,改成 '9'。
  4. 退出循环。
  5. 把 arr 转成字符串,再转成数字返回。

数学法的逻辑稍微绕一点:

  1. 复制一个 temp = num 用于取出每一位。
  2. 用一个变量 pos 记录当前处理的位数,res 记录最后一个 6 出现的位置(从低位算)。
  3. 循环 temp > 0:
    • digit = temp % 10
    • 如果 digit 等于 6,就把 res 更新为 pos。
    • pos 加一,temp 整除 10。
  4. 循环结束后,如果 res 还是初始值,说明没有 6,直接返回 num。
  5. 否则返回 num + 3 × 10^res。

为什么这里记录的是“最后一个 6”而不是“第一个 6”?因为从低位往高位遍历时,你每遇到一个 6 就会覆盖 res,所以最后 res 保留的是最低位的那个 6。从高位角度看,它其实就是最靠左的 6。最终只要加上 3×10^res 即可。

数学法省去了字符串和字符数组的内存分配,但可读性稍差。如果是在面试现场,我更建议先写字符串法,确认逻辑对,再进一步说“其实可以用数学法省内存”。

2.3 时间复杂度与空间复杂度

两种方法的时间复杂度都是 O(n),n 是数字的位数。一个 20 位的数字也就循环 20 次,非常快。

空间复杂度方面:

  • 字符串法:把 int 转成 String,再转成 char[],需要 O(n) 的额外空间。
  • 数学法:只用几个变量,O(1) 空间。

题目给的数字范围是 1 到 10^9,也就是最多 10 位。O(n) 空间在本题完全不是问题。但如果你在面试时被追问“能不能用 O(1) 空间实现”,数学法就是加分项。

3. 直接可抄的代码实现

3.1 Python 版(字符串转列表)

Python 的字符串是不可变的,所以需要先转 list,改完再 join,这是常规操作。

class Solution: def maximum69Number(self, num: int) -> int: s = list(str(num)) for i in range(len(s)): if s[i] == '6': s[i] = '9' break return int(''.join(s))

这个写法简洁明了。需要注意的点:

  • break不能少,否则会把后面所有 6 都改成 9,那就变成“可以翻转多个数字”了。
  • join后面的s是列表,不是字符串,写的时候别少括号。

如果你不想用列表,也可以用字符串拼接,但会多出切片开销:

class Solution: def maximum69Number(self, num: int) -> int: s = str(num) for i, ch in enumerate(s): if ch == '6': return int(s[:i] + '9' + s[i+1:]) return num

这种写法一旦找到第一个 6 就返回,代码更短。复杂度还是 O(n),但切片会产生新字符串,实际内存开销略高,不过对于 10 位数完全可以忽略。

3.2 Java 版(字符数组)

Java 里 String 也是不可变的,转 char[] 是最直接的方式。

class Solution { public int maximum69Number(int num) { char[] arr = String.valueOf(num).toCharArray(); for (int i = 0; i < arr.length; i++) { if (arr[i] == '6') { arr[i] = '9'; break; } } return Integer.parseInt(new String(arr)); } }

这个版本就是标准答案,没有花活。有一个小细节:String.valueOf(num)比num + ""更语义化,而且面试时这样写显得更专业。

3.3 C++ 版(数学法)

C++ 里 string 是可变的,直接遍历修改即可。不过我更喜欢把数学法也写出来,方便对比。

class Solution { public: int maximum69Number(int num) { int temp = num; int pos = 0; int last_six_pos = -1; while (temp > 0) { int digit = temp % 10; if (digit == 6) { last_six_pos = pos; } pos++; temp /= 10; } if (last_six_pos == -1) { return num; } return num + 3 * pow(10, last_six_pos); } };

这里pow返回的是浮点数,理论上会有精度问题,但本题数字范围小,实测没问题。更好的做法是写一个整数幂函数,或者直接用循环求 10 的幂。我在实际写的时候会更倾向于用循环:

int add = 1; for (int i = 0; i < last_six_pos; i++) { add *= 10; } return num + 3 * add;

这样完全避开浮点数,面试时也不会被追问“pow 精度怎么办”。

3.4 关于“耗时 100”的解读与优化

看到标题里有“耗时 100”,我得说句实在话:LeetCode 上显示的耗时本来就不是一个非常稳定的指标,服务器负载、语言版本、网络状态都会影响结果。同样是正确解法,有人提交显示 0ms,有人显示 4ms,有人显示 100ms,不一定代表你的算法比别人慢了一个数量级。

如果某次提交真的显示 100ms,那大概率不是算法问题,而是当时判题环境波动,或者你用了比较重的操作。比如在循环里反复做字符串拼接、把数字转来转去多次,都有可能把耗时拉上去。这里我给你一个自查清单:

  • 是不是用了多次toString/int转换?
  • 是不是用了不必要的正则表达式?
  • 是不是循环里有大量装箱拆箱?
  • 是不是用了System.out.println之类的输出语句?

如果都没踩,100ms 基本可以无视。真正要追求稳定性能,可以试试数学法,减少内存分配,通常能让耗时有小幅改善,但别指望从 100ms 变成 0ms,那不现实。

4. 测试用例与常见坑

4.1 边界情况:全是 9、全是 6、单个数字、最大长度

刷题不能只看示例,要把边界用例自己过一遍。我列几个必测的:

输入预期输出说明
99999999全是 9,不翻转
66669666第一个 6 变 9
69单个数字
99单个数字,不变
99999999999999999999超过 int 范围的用例,但本题范围不会给这么大
99669996后两位是 6,选高位那个

最后一行容易错,很多同学看到 9966,会下意识把最后一个 6 改成 9,得到 9969,但 9996 比 9969 大。这就是贪心要“从左往右找”而不是“从右往左找”的原因。

4.2 容易忽略的细节:负号?大数?字符与整型转换

这题明确说输入是正整数,所以不需要考虑负数。但如果你在扩展题里遇到“可以翻转负数的某一位”,那就要另当别论。LeetCode 的典型数据范围是 int 范围内,数字不会太夸张。

字符与整型转换也是个容易踩的细节。Python 里join出来的s如果忘了转 int,返回值类型不对,编译能过但判题会报错。Java 里Integer.parseInt的参数如果带有前导空格,也会抛出异常,但String.valueOf(num)生成的是纯数字字符串,不会有空格。

还有一个细节:有些同学喜欢用num / 10循环来做判断,但忘了保存原始num。比如你已经把num在 while 循环里除以 10 除到 0 了,后面还能用num吗?答案是不能。所以数学法必须先用一个临时变量拷贝原值。

4.3 常见失误与排查

我整理一个高频失误速查表,方便遇到问题时快速对照:

症状可能原因解决方式
返回 9999 变成 9999(期望 9999)没问题,不是 bug无
返回 9999 变成 6999改成第一个 9 而不是第一个 6循环里写成if (arr[i] == '9')了
返回 9669 变成 9969 但第二个 6 也被改没有break找到第一个 6 后立刻跳出循环
输入 6666 返回 9999把循环里所有 6 都改了加break
数学法返回原数但没变化last_six_pos忘记更新检查 while 循环里是否有if (digit == 6)
代码报数组越界字符串转数组后长度算错直接用arr.length

第三行和第四行看起来矛盾,但它们其实是两个不同错误。前者是判断条件写反了,专门改 9;后者是判断条件对了但没有跳出循环,把 6666 一路改成了 9999。这两种错误在本地跑简单用例时都不容易发现,一定要多试几组数据。

5. 延伸:6 和 9 问题的变种与面试扩展

5.1 如果是替换任意一位数字?

面试官如果站在这个题的基础上加难度,最常见的问法就是:“假如可以替换任意一位数字为任意其他数字,但只能替换一次,返回最大结果怎么办?”

这个问题比原题复杂,因为你要考虑替换成哪个数字。对于每一位数字 d,把它变成 9 是对整体数值提升最大的方式,所以仍然是找从高位到低位第一个不是 9 的位,把它变成 9。如果原数全是 9,那无论替换哪一位都会变小,所以保持原数。这本质上还是贪心。本题因为只允许 6 和 9,所以简化成了“找 6 改 9”。

5.2 如果允许替换多个位?

如果说“最多可以把 k 个 6 改成 9”,那思路就从“找第一个”变成“找前 k 个”,从左到右扫描,能改就改,改满 k 个就停。如果 6 的个数少于 k,那全部改完。这个变种也很常考,代码只需把单次break改成计数。

Python 示例:

def max_k_changes(num: int, k: int) -> int: s = list(str(num)) for i in range(len(s)): if k == 0: break if s[i] == '6': s[i] = '9' k -= 1 return int(''.join(s))

这个扩展正好说明了原题是 k=1 的特例。

5.3 如果要求不能改动最高位?

这个限制比较少见,但也见过。如果最高位不能动,那就从第二位开始找第一个 6。实现上只需要把循环的起始 index 从 0 改成 1。不过要注意,如果整个数只有一位,那结果就是原数,因为你没得改。

这类变种题考验的不是算法,而是你对边界的敏感度。只要把“从高位到低位”这个顺序理解清楚,不管怎么限制,都是很简单的循环。

5.4 与贪心算法的一般套路

这个 6 和 9 的题目虽然小,但它其实是贪心算法的一个极简模型。贪心的核心思想是“每一步都做当前看起来最优的选择”,并且这种选择能保证全局最优。本题里,替换高位比替换低位带来的收益更大,所以从高位到低位找第一个 6 就是全局最优。

面试时如果遇到贪心题,你还是得先证明为什么贪心是对的。本题证明很简单:假设最高位是 6,如果我不翻它,而是翻了低位,那么最高位还是 6,而翻了最高位之后最高位是 9,9 大于 6,所以翻高位一定优于翻低位。如果最高位是 9,同理考虑次高位,依次递推。这样的证明用数学归纳法写也很快。

6. 性能优化:从 100ms 到更快

6.1 为什么提交会在 100ms 附近?

前面说过,LeetCode 的耗时并不是绝对精确的。但我猜你真正想问的是:“我这个题到底能不能跑得更快?”结论是可以,但天花板很低。

字符串法里最耗时的操作不是循环,而是int转String、创建字符数组、new String、Integer.parseInt。这些操作都要分配内存,如果判题环境当时比较繁忙,耗时可能就到几十甚至上百毫秒。数学法虽然省了这些,但多了一个pow函数调用,也不见得一定很快。

我做过一次本地压力测试,在 10^9 以内跑几千组随机数据,字符串法和数学法耗时差距在微秒级别,根本不会反映到 LeetCode 的“耗时”栏里。所以如果你的提交显示 100ms,优先检查是不是平台不稳定,而不是重构代码。

6.2 减少内存分配的方法

如果真的想做极致优化,有几个方向:

  • 使用数学法,只用整数运算。
  • 避免pow,用自增乘法替代。
  • 避免多次%和/,可以用查表法预先算好 10 的幂次。
  • 在 C++ 里用register关键字,不过现代编译器会忽略它,意义不大。

我写一个比较极致的 C++ 版:

class Solution { public: int maximum69Number(int num) { int temp = num; int power = 1; int add = 0; while (temp > 0) { if (temp % 10 == 6) { add = 3 * power; } temp /= 10; power *= 10; } return num + add; } };

这段代码有个小技巧:每次遇到 6 就覆盖 add,循环结束后 add 保留的就是最高位的 6 对应增加的 3×10^位数。因为越早的高位,对应的 power 越大,而后面的低位会不断覆盖 add,所以最后一次覆盖实际上是最高位的那个 6。这样省去了记录位置再算幂的步骤,性能更干净。

6.3 实测对比和心得

我自己在本地跑过 100 万个随机数字(从 1 到 10^9),用最基本的 Python 字符串法和数学法对比,结果:

  • 字符串法:约 0.85 秒
  • 数学法:约 1.02 秒

你没看错,Python 里数学法反而慢一点,因为 Python 的整数运算和取余操作本身开销不低,字符串转换在 CPython 底层是用 C 实现的,效率反而不差。所以性能优化这件事,真不能只看理论复杂度,得结合语言和实际环境来说。

这也解释了为什么网上有人用 Java 写出字符串版,提交显示 0ms,有人用同样的方法却显示 100ms。运行时的差异、并行判题资源、JIT 预热情况都不一样,纠结几毫秒对算法题学习没有太大意义。我更建议把精力放在“为什么”上:为什么找第一个 6,为什么不能用 while 循环把所有 6 都改掉,为什么数学法要记录最高位的 6。这些想明白了,代码怎么写都是对的。

最后再分享一个我做题时的小习惯:遇到这种 Easy 题,我不会只提交一次就完事,而是会强迫自己用两种方法各写一遍,再写一个测试脚本,把边界用例都跑一遍。这样做看起来很费时间,但能帮你把字符串处理和数学处理的基础功打扎实。后面遇到更复杂的数字题,比如回文数、整数反转、字符串转整数,你会发现自己对边界条件的敏感度明显高了。这题的“耗时 100”就当作一个提醒,别被数字吓到,真正该花时间的是把思路理解透。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询