【C/C++】400. 第 N 位数字

237 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第19天,点击查看活动详情


题目链接:400. 第 N 位数字

题目描述

给你一个整数 n ,请你在无限的整数序列 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, ...] 中找出并返回第 n 位上的数字。

提示:

  • 1n231 11 \leqslant n \leqslant 2^{31} - 1

示例 1:

输入:n = 3
输出:3

示例 2:

输入:n = 11
输出:0
解释:第 11 位数字在序列 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, ... 里是 0 ,它是 10 的一部分。

题意整理

将无限的整数序列 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, ...] 拼接成字符串 1 2 3 4 5 6 7 8 9 1 0 1 1... ,返回字符串第 n 位(如果下标从 0 开始,就是第 n - 1 位)数字。例如第 1 位为 111 位是 0

解题思路分析

习惯性动作:首先确定题目数据范围 1n231 11 \leqslant n \leqslant 2^{31} - 1 ,这里 n 的上界为 23112^{31} - 1 ,也就是 int 类型的最大值 2,147,483,647 ,涉及到 int 边界的时候需要注意溢出问题。很明显这个数据范围是不允许我们暴力解决的。

为了定位无限整数序列中的第 n 位数字的值,我们首先需要知道 n 位数字是在哪一个整数中 ,并且还需要知道 是所在整数的第几位 。如果能解决以上两个问题,我们就可以得到无限整数序列中的第 n 位数字的值。

问题一:如何确定第 n 位数字是在哪一个整数中

这里是这道题的核心难点,我们需要通过找规律的方式来确定:

  • 1 位数的范围是 19,共有 9 个数,所有 1 位整数的位数之和是 1×9=91 \times 9 = 9
  • 2 位数的取值范围是 1099,共有 90 个数,所有 2 位整数的位数之和是 2×90=1802 \times 90 = 180
  • 3 位数的取值范围是 100999,共有 900 个数,所有 3 位整数的位数之和是 3×900=27003 \times 900 = 2700
  • ......

通过以上规律我们可以推广到一般情形从而得出结论:

  • x 位数的范围是 10x110^{x - 1}10x110^x - 1
  • 共有 (10x1)10x1+1=9×10x1(10^x - 1) - 10^{x - 1} + 1 = 9 \times 10^{x - 1} 个数;
  • 所有 x 位数的位数之和是 x×9×10x1x \times 9 \times 10^{x - 1}

我们可以通过不断增加整数的位数,通过求和得到总的位数和,当加上当前 x 位整数的位数和大于 n 时,就可以 判断第 n 位数字所在的整数是多少位的整数 ,我们可以得到 x 位整数的第一个数为 10x110^{x-1},因为当前整数都为 x 位,所以可以直接利用除法得到第 n 位所在的具体数字是多少。

问题二:如何确定第 n 位数字是所在整数的第几位

当我们确定第 n 位数字是在哪一个整数中后,确定第 n 位数字是所在整数的第几位就很简单了,将剩余的位数用来确定第 n 位数字是在 x 位整数中的第几位即可。

解题的核心思想已经说完了,具体实现部分因人而异,不同的人有不同的实现方法,主要是要把解题的核心思想搞懂。

具体实现

  1. 确定第 n 位数字所在整数是几位数:

    • 方法一:二分查找 ,使用二分的前提条件是需要单调性,因为所有不超过 x 位整数的位数之和是关于 x 单调递增,因此是可以用二分查找的方法确定这个位数的值。对于不超过 x 位整数的所有位数之和小于 n,则可以判断第 n 位所在整数的位数一定大于 x 位。否则第 n 位所在整数的位数一定小于等于 x 位。

      二分查找复杂度分析

      • 时间复杂度:O(log10n×loglog10n)O(\log_{10} n \times \log \log_{10} n) 。用 DD 表示位数的上限,则有 D=O(log10n)D = O(\log_{10} n)。二分查找的执行次数是 O(logD)O(\log D),每次执行的时间复杂度是 O(D)O(D),因此总时间复杂度是 O(DlogD)=O(log10n×loglog10n)O(D \log D) = O(\log_{10} n \times \log \log_{10} n)
      • 空间复杂度:O(1)O(1),仅需使用到常数空间。
    • 方法二:直接计算 ,因为数据最大值的位数最多 9 位,所以也可以不使用二分查找,而是根据规律直接计算。已知 x 位数共有 9×10x19 \times 10^{x - 1} 个,所有 x 位数的位数之和是 x×9×10x1x \times 9 \times 10^{x - 1} 。使用 dcount 分别表示当前遍历到的位数和当前位数下的所有整数的位数之和,初始化 d = 1count = 9。不断将 n 减去当前整数位的位数之和 d * count ,然后将 d1,将 count 乘以 10,直到 n <= d * count,此时的 d 是目标数字所在整数的位数,n 是所有 d 位数中从第一位到目标数字的位数。

      直接计算复杂度分析

      • 时间复杂度:时间复杂度:O(log10n)O(\log_{10} n)。用 d 表示第 n 位数字所在整数的位数,循环需要遍历 d 次,由于 d=O(log10n)d = O(\log_{10} n) ,因此时间复杂度是 O(log10n)O(\log_{10} n)
      • 空间复杂度:O(1)O(1),仅需使用到常数空间。
  2. 确定第 n 位数字是在哪一个整数中的第几位

    在得到第 n 位数字所在整数是几位数之后,这里用 d 表示。将不超过 d - 1 的整数的位数之和减去得到第 n 位数在所有 d 位数的序列中的下标,为了方便计算,将下标转换成从 0 开始记数。因为当前整数都为 x 位,所以可以直接利用除法得到第 n 位所在的具体数字是多少,再根据剩余的位数进行判断具体数值即可。

这里需要注意细节的处理,明确下标的指代。

代码实现

方法一:二分

class Solution {
public:
    int findNthDigit(int n) {
        //确定位数上下界
        int low = 1, high = 9;
        //二分确定位数
        while (low < high) {
            int mid = (high - low) / 2 + low;
            if (totalDigits(mid) < n) {
                low = mid + 1;
            } else {
                high = mid;
            }
        }
        //得到位数d
        int d = low;
        //减去所有不超过d - 1位整数的位数和
        int prevDigits = totalDigits(d - 1);
        //注意这里下标指代
        int index = n - prevDigits - 1;
        //d位开始的数字
        int start = (int) pow(10, d - 1);
        //得到具体所在的整数
        int num = start + index / d;
        //明确第n位具体的值
        int digitIndex = index % d;
        int digit = (num / (int) (pow(10, d - digitIndex - 1))) % 10;
        return digit;
    }
    //计算不超过length位的整数位数和
    int totalDigits(int length) {
        int digits = 0;
        int curLength = 1, curCount = 9;
        while (curLength <= length) {
            digits += curLength * curCount;
            curLength++;
            curCount *= 10;
        }
        return digits;
    }
};

方法二:直接计算

class Solution {
public:
    int findNthDigit(int n) {
        //1位的数字共9个 [1, 9]
        //d表示当前遍历到的位数;count表示当前位数下的所有整数的位数之和
        int d = 1, count = 9;
        //找到n所在数字的位数
        while(n > (long)d * count){
            n -= d * count;
            d++;
            count *= 10;
        }
        //start为d位开始的数字
        int start = pow(10, d - 1);
        //index为目标数字在所有 d 位数中的下标,为了方便计算,将下标转换成从 0 开始记数。
        int index = n - 1;
        //num为n所在的数字
        int num = start + (index / d);
        //digitIndex为在数字num中的下标
        int digitIndex = index % d;
        //最后答案就是取出num中第digitIndex位数字
        int ans = (num / (int) pow(10, d - digitIndex - 1)) % 10;
        return ans;
    }
};

总结

该题的核心难点在于 如何确定第 n 位数字是在哪一个整数中 ,解决这个难点问题的核心思想 是先确定第 n 位数字所在整数是几位数 ,这里是需要通过找规律的方式来确定。不用纠结于具体的实现方法,而是要把解题的核心思路搞清楚,无论是二分查找还是直接计算,其核心思想都离不开寻找第 n 位数字所在整数是几位数。


结束语

你是否经常感慨,好像别人的成功都来得很容易。事实上,别人受过的苦累我们并不知晓。所有光鲜的背后都意味着深深的自律。雕塑自己的过程必定伴随着疼痛与辛苦,可那一锤一凿的自我敲打,会让我们收获更好的自己。