被力扣今天的每日一题虐的体无完肤-440:字典序的第k小数字

151 阅读3分钟

题目要求与快捷入口:leetcode-cn.com/problems/k-…

给定整数 n 和 k,返回  [1, n] 中字典序第 k 小的数字。

示例 1:

输入: n = 13, k = 2
输出: 10
解释: 字典序的排列是 [1, 10, 11, 12, 13, 2, 3, 4, 5, 6, 7, 8, 9],
所以第二小的数字是 10。
示例 2:

输入: n = 1, k = 1
输出: 1

作为一个菜鸡,看到这个题的第一反应是实现一个字典序排序的函数,然后找出第k-1个数即可。后来发现数据size是10^9,我就放弃了。对这种大数据量的题目,我一般是傻傻分不清的,只知道这种数据量的题目,你用n^2的复杂度,你就输了。硬着头皮在深夜写个题解,让自己加深对题目的理解。。。

思路:将整个过程分为两步:

  • 使用函数count(prefix,n),函数的功能是,给定一个数字prefix,在【1,n】中,找到以prefix开头的数有多少个。
  • 得到count(prefix,n)之后,findKthNumber迭代的求出符合条件的字典序第K个数组。
  1. 从最终答案的前缀开始枚举,初始时,前缀为1,也是字典序最小的数字。
  2. 假设当前的前缀是prefix,且我们需要找这个前缀下的第K小的数字。
  3. 若k==1,那么当前的前缀就是答案,否则,计算这个前缀能组成数字的个数,记作cnt。
    1. 如果k>cnt,说明答案不是以当前前缀为开头的数字,那么就需要往前去判断,令k=k-cnt,且前缀prefix+=1(原来以123开头的前缀个数小于k所以需要找124开头的),重新寻找在新的前缀下的新的第k个数。
    2. 若k<cnt,说明答案是以这个前缀构成的,k--,因为这个前缀本身也占了一个位置,然后令prefix=prefix*10(判断是以123开头的,那么需要进一步确定是以1230,1231,...,1239开头中的一种),往后增加一位0。
  4. 如何求以某个前缀prefix构成的数字的个数:若prefix10的数字位数小于n的数字位数,则prefix_0,prefix_1,...,prefix_9都可以构成答案呢,紧接着若prefix100的位数小于n的数字位数,prefix_00,prefix_01,...prefix_99也都是答案。当最后与n的位数相同时,需要判断下prefix是否小于等于n的前缀,根据情况补全相同位数下能构成数字的个数。

时间复杂度 计算以某个前缀构成的数字个数的时候需要 O(logn) 的时间。 每一位最多枚举 10 个数字,故总时间复杂度为 O(logn^2)

/**
* 字典序的第K小数字
* @param n
* @param k
* @return
*/
public int findKthNumber(int n, int k) {
    // 给定一个数字prefix,在【1,n】中找到以prefix为开头的数字个数。
    int prefix = 1;
    while (k>1){
        int cnt = count(prefix,n);
        if (k>cnt){
            prefix ++;
            k-=cnt;
        }else{
            // 求的是以prefix为前缀的第k小的数,这里发生进位,
            // 所以要把prefix删除,因为prefix要变了。例如当前prefix=123,进位1230 之前要把prefix这个数删掉。
            prefix *=10;
            k--;
        }
    }
    return prefix;
}

/**
* 找到小于n,以prefix开头的数有多少个
* @param prefix
* @param n
* @return
*/
private int count(int prefix, int n) {
    long t = prefix;
    int tot = 0,k=1;
    while (t*10<=n){
        tot+=k;
        t*=10;
        k*=10;
    }
    // n-t+1 是从t到n的个数,如果大于k代表最高位是不同的。
    if (n-t+1>k){
        tot+=k;
    }else{
        tot+=n-t+1;
    }
    return tot;
    }

还有大佬的思路使用十叉树,我大呼牛逼,惹不起惹不起。做过lc386的都知道可以dfs来进行十叉树的前序遍历来获得正确顺序。但是这题如果用dfs的话会超时,所以需要巧妙地剪枝。 附大佬的题解与cpp代码 (dfs剪枝) 假设答案在以节点cur为根的子树上(cur从1出发), 那么答案只有三个可能的落脚点 (1)cur本身 (2)与cur同层的下一个节点cur + 1,在以cur + 1为根节点的子树上 (3)在以cur的某个子节点为根的子树上 e.g. cur * 10, cur * 10 + 1, cur * 10 + 2, ..., cur * 10 + 9。

当答案为(1)时k为0。我们可以验证答案是否落在(2)上,通过计算以cur为根的子树大小,以cur, cur * 10, curr * 100, ..., curr * 10^k为左边界的枝节(inclusive)和以curr + 1, (curr + 1)* 10,..., (curr + 1) * 10 ^k为右边界(exclusive)中间的所有点。其中左边界上的所有点一定合法(<=n)。当右边界的点合法时 i.e. (curr + 1) * 10^k <= n + 1时,curr * 10 ^k~ (curr + 1) * 10^k之间的10^k个节点必然存在,需要从k中减去。但如果(curr + 1) * 10 ^k不合法,那么curr * 10 ^k~ n + 1之间的点也是存在的,需要从k中减去。 image.png 当我们把两边界之间的所有点都计算完后,我们得到子树大小step,如果step <= k则说明答案肯定在(3)上,否则在(2)上

class Solution {
public:
    int findKthNumber(int n, int k) {
        // cur @ 当前节点, cur + 1 @ 同层下一个节点
        int cur = 1; --k;
        while(k > 0){
            // first @ 从cur往左下的左边界上的点, last @ 从cur + 1往左下的右边界上的点
            // step @ 计算以cur为根节点的子树大小
            long long first = cur, last = cur + 1, step = 0;
            // 计算子树大小
            while(first <= n){
                step += min((long long) n + 1, last) - first;
                first *= 10;
                last *= 10;
            }
            // case 1 @ 子树大小 <= k, 答案可能落在同层的下一个节点
            if(step <= k){
                k -= step;
                cur ++;
            }
            // case 2 @ 答案一定落在cur子树上,探索下一层
            else{
                k --;
                cur *= 10;
            }
        }
        return cur;
    }
};