题目要求与快捷入口: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,也是字典序最小的数字。
- 假设当前的前缀是prefix,且我们需要找这个前缀下的第K小的数字。
- 若k==1,那么当前的前缀就是答案,否则,计算这个前缀能组成数字的个数,记作cnt。
- 如果k>cnt,说明答案不是以当前前缀为开头的数字,那么就需要往前去判断,令k=k-cnt,且前缀prefix+=1(原来以123开头的前缀个数小于k所以需要找124开头的),重新寻找在新的前缀下的新的第k个数。
- 若k<cnt,说明答案是以这个前缀构成的,k--,因为这个前缀本身也占了一个位置,然后令prefix=prefix*10(判断是以123开头的,那么需要进一步确定是以1230,1231,...,1239开头中的一种),往后增加一位0。
- 如何求以某个前缀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中减去。
当我们把两边界之间的所有点都计算完后,我们得到子树大小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;
}
};