题目描述

✅ 400. 第 N 位数字

image-20260928201407994

题意分析

把正整数 1、2、3、… 的十进制表示依次拼接,得到连续的数字字符序列,返回其中从 1 开始计数的第 n 位数字。答案是 0 到 9 中的一个数,不是第 n 个整数,也不是包含目标位的整个整数。

n 最大可到 32 位有符号整数上限,逐个生成并拼接到目标位置会产生不必要的大量时间和空间开销。可以利用相同位数的整数具有相同长度,成组跳过前面的数字。

解法:按位数分组定位

核心思路

[!blue]

把正整数按十进制位数分组。当前组的每个整数有 digit 位,第一个整数为 start = 10^(digit - 1),共有 count = 9 * start 个整数,因此整组贡献 digit * count 个字符。

用 remain 表示跳过前面完整分组后,目标在剩余序列中从一开始的排名。若 remain > digit * count,目标不在本组,减去整组字符数,再进入下一位数组。若小于或等于,目标就在当前组;等于时恰好是本组最后一位,不能再跳过。

定位到分组后,将一基排名转为零基偏移 remain - 1。除以 digit 的商表示完整跨过了多少个本组整数,所以包含目标位的整数为 num = start + (remain - 1) / digit;余数 index = (remain - 1) % digit 表示目标在这个整数中从左数的零基位置。

目标位右边还有 digit - index - 1 位。把 num 连续除以十这么多次,目标位就移到了个位,再取 num % 10 即可。这个过程不必把整数转换成字符串。

即使输入 n 能放入 32 位整数,完整分组的字符总数也可能超过这个范围。代码从一开始使用 Java 的 long、Go 的 int64 保存分组变量,让乘法在宽整数类型中完成。

解题步骤

  1. 初始化 digit = 1、start = 1、count = 9、remain = n。
  2. 当 remain > digit * count 时,减去当前组字符数,将 digit 加一,并让 start、count 各乘十。
  3. 计算 num = start + (remain - 1) / digit 和 index = (remain - 1) % digit。
  4. 将 num 除以十共 digit - index - 1 次,去掉目标位右侧的数字。
  5. 返回 num % 10。

代码实现

class Solution {
    public int findNthDigit(int n) {
        long digit = 1;
        long start = 1;
        long count = 9;
        long remain = n;

        while (remain > digit * count) {
            remain -= digit * count;
            digit++;
            start *= 10;
            count *= 10;
        }

        // 剩余位置是一基,减一后再按位数定位具体整数。
        long num = start + (remain - 1) / digit;
        long index = (remain - 1) % digit;

        // 去掉目标位右侧的数字,再取个位即可得到目标。
        for (long remove = digit - index - 1; remove > 0; remove--) {
            num /= 10;
        }

        return (int) (num % 10);
    }
}
func findNthDigit(n int) int {
    digit := int64(1)
    start := int64(1)
    count := int64(9)
    remain := int64(n)

    for remain > digit*count {
        remain -= digit * count
        digit++
        start *= 10
        count *= 10
    }

    // 剩余位置是一基,减一后再按位数定位具体整数。
    num := start + (remain-1)/digit
    index := (remain - 1) % digit
    // 去掉目标位右侧的数字,再取个位即可得到目标。
    for remove := digit - index - 1; remove > 0; remove-- {
        num /= 10
    }
    return int(num % 10)
}

复杂度分析

  • 时间复杂度:$O(\log n)$。位数分组按十倍规模增长,跳组次数与目标整数的十进制位数同阶,最后去掉低位也只需相同量级的操作。
  • 空间复杂度:$O(1)$,只维护若干宽整数变量,不构造拼接序列或辅助字符串。

关键点总结

[!green]

  • count 是整数个数,digit * count 才是分组贡献的字符数量,跳过时不能混淆单位。
  • 先把剩余排名减一,商负责定位整数,余数负责定位整数内部的字符。
  • 余数从左端计数,而取模只能读取个位,需要先去掉目标位右侧的低位。
  • 分组乘法本身必须在宽整数类型中执行,不能溢出后再转换结果。

易错点总结

[!yellow]

  • 跳组条件写成 >=,会把恰好落在本组最后一位的目标错误移到下一组,应使用严格大于。
  • 忘记将 remain 减一就求商和余数,会在每个整数的末尾产生错位。
  • 把拼接序列当作从整数零开始,会多算一位;本题从正整数一开始。
  • 定位整数后直接返回 num % 10,只能得到该整数的最后一位,必须先移除目标右侧的数字。
  • 使用 32 位变量保存完整分组的字符总数,可能在比较目标是否越过本组之前就发生溢出。

相似题目

题目 难度 关联与区别
233. 数字 1 的个数 困难 同样按十进制位数分块统计,本题跳过整段数字占据的位数,原题累计某个数码的出现次数。
440. 字典序的第K小数字 困难 同样跳过大块候选而不逐个生成,原题按字典序前缀分块,本题按普通数值顺序的位数分块。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/92368500
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!