题目
给定整数 n 和 k,找到 1 到 n 中字典序第 k 小的数字。
注意:1 ≤ k ≤ n ≤ 109。
示例输入:
n: 13 k: 2
输出:
10解释:
字典序的排列是 [1, 10, 11, 12, 13, 2, 3, 4, 5, 6, 7, 8, 9],所以第二小的数字是 10。
题解思路
1.什么是字典序?
字典序我们的常见的升降序不一样,它是一个树形结构,如下图所示,例如当输入n为110时,这个数组用字典序排序后前面数字为:1,10,100,101,102,103,104,105,106,107,108,109,11,110…所以,一个数乘十和一个数加一在字典序中,前者会排在更前,最终我们可以把它归纳成一棵十叉树,每个节点最多可以有十个子节点。
2.分解问题
在我们理解了什么是字典序之后,我们就可以把这道题的问题分解成下面几个问题:
- 计算前缀为1~9的节点下面各有多少个节点。
- 第k个数位于哪个前缀下。
- 找到排序为k的数。
(1)计算前缀为1~9的节点下面各有多少个节点。
这个问题比较简单,就是计算前缀为i+1节点的第一个元素和前缀为i节点的第一个元素的差值,然后遍历所有这个节点下的所有层级,就可以得到该前缀下所有的节点数。举个例子,例如n为20的数组对应字典序的十叉树如下:
它的层数是2层,所以我们需要计算两次,首先计算第一层,2减1为1,再计算第二层,20减10为10,然后累加得到前缀为1的所有节点个数为1+10=11个。我们把它用代码表示出来如下:
public long getPrefixNum(long prefix, long n) {
long curValue = prefix; // 前缀为prefix每一层元素的第一个数
long nextPrefix = prefix + 1; // 前缀为prefix+1的每一层的第一个数
long count = 0; // 计算前缀为prefix下节点总数的变量
while(curValue <= n) {
count += nextPrefix - curValue; // 计算每一层的第一个数的差值,并累加
curValue *= 10; // 切换到下一层
nextPrefix *= 10;
}
return count;
}
这里的代码看似没有问题,但是我们还把遗漏的case忽略掉了,假如当n大于100而小于200时,上面这段代码就会有问题。例如当n等于100的时候,实际上前缀为1的树里面已经达到了三层,而前缀为2的树还是只有两层,如果我们继续按照上面的代码,计算到第三次while循环的时候,nextPrefix就已经变成了300,显然300已经比n的值(299)大,所以我们不能用200-100来算,因为前缀1不是一个全十叉树,相反应该用n+1和nextPrefix的较小值来和100来相减,通过优化代码修改如下:
public long getPrefixNum(long prefix, long n) {
long curValue = prefix; // 前缀为prefix每一层元素的第一个数
long nextPrefix = prefix + 1; // 前缀为prefix+1的每一层的第一个数
long count = 0; // 计算前缀为prefix下节点总数的变量
while(curValue <= n) {
count += Math.min(n+1, nextPrefix) - curValue; // 计算每一层的第一个数的差值,并累加
curValue *= 10; // 切换到下一层
nextPrefix *= 10;
}
return count;
}
(2)第k个数位于哪个前缀下
分别遍历各个前缀的个数,然后比较k和该前缀以前的元素个数大小,如果k大的话,就将前缀加一,相反的话就落入该前缀里面。
(3)找到排序为k的数
确定了是哪个前缀之后,继续遍历层数,直到找到第k个数。
题解代码
public class Question440 {
public static void main(String[] args) {
System.out.println(findKthNumber(10, 100));
}
public static long getPrefixNum(long prefix, long n) {
long curValue = prefix; // 前缀为prefix每一层元素的第一个数
long nextPrefix = prefix + 1; // 前缀为prefix+1的每一层的第一个数
long count = 0; // 计算前缀为prefix下节点总数的变量
while(curValue <= n) {
count += Math.min(n+1, nextPrefix) - curValue; // 计算每一层的第一个数的差值,并累加
curValue *= 10; // 切换到下一层
nextPrefix *= 10;
}
return count;
}
public static int findKthNumber(int k, int n) {
int curPrefix = 1; //当前指向的数字
long curIndex = 1; //当前指向数字在字典序中的位置
while(curIndex < k) {
long prefixNum = getPrefixNum(curPrefix, n);
if (prefixNum + curIndex > k) {
curPrefix *= 10; //比如前缀1就变成10,10就变成了100,缩小区间
curIndex++; //当前数的位置就变成了原来的加1
} else {
curPrefix++; //比如前缀1变成了2,前缀10的话就变成了11,右移前缀
curIndex += prefixNum; //当前数的位置就变成了原来位置加上上一个前缀的子节点个数
}
}
return curPrefix;
}
}
复杂度
时间复杂度:O(n^2)
空间复杂度:O(1)