738. 单调递增的数字(Monotone Increasing Digits)

频次 ★★★ · 难度 🟡 · 高频:字节

题目

给定一个非负整数 n,返回小于或等于 n 的最大数字,且该数字的各位数字单调递增(相邻位非递减,即高位 ≤ 低位)。

示例

输入: n = 10
输出: 9

输入: n = 1234
输出: 1234

输入: n = 332
输出: 299

思路

贪心(从右向左扫描):将数字转为字符数组,从右向左遍历。当发现 chars[i-1] > chars[i](即高位 > 低位,违反单调递增),执行:

  1. chars[i-1]--(高位减 1)
  2. 标记位置 i,之后从 i 到末尾全部变成 ‘9’

贪心原理:遇到第一个递减位置时,把高位减 1 后低位全变 9,得到的数字最大且满足单调递增。

代码

public int monotoneIncreasingDigits(int n) {
    char[] chars = String.valueOf(n).toCharArray();
    int flag = chars.length;               // 标记从哪开始全部变 9
    for (int i = chars.length - 1; i > 0; i--) {
        if (chars[i - 1] > chars[i]) {     // 高位 > 低位,违反单调递增
            chars[i - 1]--;                // 高位减 1
            flag = i;                      // 从 i 开始全部变 9
        }
    }
    for (int i = flag; i < chars.length; i++) {
        chars[i] = '9';
    }
    return Integer.parseInt(new String(chars));
}

复杂度

  • 时间:O(L) —— L 为数字位数,最多 10 位(int 范围)
  • 空间:O(L) —— 字符数组

边界条件

  • n = 0:返回 0
  • n 已是单调递增:返回 n 本身
  • n 全为相同数字(如 111):返回 n
  • 多段递减(如 332):flag 只会记录最后一次递减位置,因为高位减 1 后可能引发新的递减,需要继续向左检查

变式

易错点

  • flag 初始化为 chars.length:如果 n 本身就是单调递增,flag 不会更新,循环全变 9 不会执行,直接返回原值
  • 从右向左遍历:从左向右遍历无法正确处理”高位减 1 后前面可能继续递减”的情况。例如 332,从左向右只看到 3>3(不成立)、3>2(成立),把 3 减成 2 得 229,但 22 仍递减需要再处理。从右向左一遍就够
  • flag 记录的是”开始变 9 的位置”:不是记录递减位置,而是递减位置 + 1(即高位减 1 后,其后所有位变 9)

面试追问

  • 为什么从右向左遍历? 从右向左时,高位减 1 后可能引发新的递减关系,但 i 指针继续向左移动,下一轮循环会自然检查到。如果从左向右,需要回退重扫
  • 为什么遇到递减后低位全变 9? 贪心:在高位减 1 的前提下,低位全变 9 能得到最大的满足单调递增的数字

关联题