题目来源
题目描述
题目解析
思路一
class Solution {
/*
* 计算[1,n]内以cur为根(开头)的节点个数
*/
long getNodes(long prefix, long n){ //prefix是前缀,n是上限
long curr = prefix;
long next = prefix + 1; //下一个前缀
long totalNodes = 0; // 统计合格的节点个数
while (curr <= n){ //当前的前缀当然不能大于上限
//totalNodes += (std::min(next, n + 1) - curr); //下一个前缀的起点减去当前前缀的起点
totalNodes += std::min(next - curr, n + 1 - curr);
curr *= 10; //往下层走
next *= 10; //往下层走
}
return totalNodes; //把当前前缀下的子节点和返回去。
}
public:
int findKthNumber(int n, int k) {
long cur = 1; // 首先遍历以1开头大的数字
--k; // 因为1遍历了,因此k--
while (k > 0){
int nodes = getNodes(cur, n); // 获取以cur开头的子节点合格(<=n)数目nodes
if(nodes <= k){ // 若nodes<=k的话说明把这nodes个节点分完都还没到k
cur++; // cur向右走
k -= nodes; // 抵消掉nodes个节点
} else{ // 若nodes>k的话说明把nodes个节点够分
cur *= 10; // cur往下走
--k; // 将cur计算进k
}
}
return (int)cur; // 最后cur会停留在第k小的数上
}
};
思路二
这道题是leetcode:386. 字典序排数的延伸,之前让按字典顺序打印数组,而这道题让我们快速定位某一个位置,那么我们就不能像之前那道题一样,一个一个的遍历,这样无法通过OJ,这也是这道题被定为Hard的原因。那么我们得找出能够快速定位的方法。
仔细观察字典顺序的数组,我们可以发现,其实这是个10叉树,就是每个节点的子节点可以有是个,比如数字1的子节点就是10到19,数字10的子节点可以是100到109,但是由于n大小的限制,构成的并不是一个满十叉树。我们分析题目中给的例子可以知道,数字1的子节点有4个(10,11,12,13),而后面的数字2到9都没有子节点,那么这道题实际上就变成了一个先序遍历十叉树的问题,那么难点就变成了如何计算出每个节点的子节点的个数,比如数字1和数字2,我们要求按照字典遍历顺序从1到2要经过多少数字,实现把1本身这个数字加到step中。然后我们把范围扩大十倍,范围变成10到20之间,但是由于我们要考虑n的大小,首先n为13,所以只有4个子节点,这样我们就知道从数字1遍历到数字2要经过5个数字,然后我们看step是否小于等于k,如果是,我们cur自增1,k减去step;如果不是,说明要求的数字在子节点中,我们此时cur乘以10,k自减1,以此类推,直到k为0推出循环,此时cur即为所求:
class Solution {
public:
int findKthNumber(int n, int k) {
int cur = 1;
--k;
while (k > 0) {
long long step = 0, first = cur, last = cur + 1;
while (first <= n) {
step += min((long long)n + 1, last) - first;
first *= 10;
last *= 10;
}
if (step <= k) {
++cur;
k -= step;
} else {
cur *= 10;
--k;
}
}
return cur;
}
};
这是一颗字典树
每一个节点都拥有10个子节点,并且每个子节点又会有10个子节点……。可以发现,整个字典序排列也就是对十叉树进行前序遍历:1, 10, 100, 101, … 11, 110 …
根据题目的意思,我们需要找到排在第k位的数,找到这个排位,需要知道三件事情:
- 怎么确定一个前缀下所有子节点的个数
- 如果第k个数在当前的前缀下,怎么继续往下面的子节点找?
- 如果第k个数不在当前的前缀,即当前的前缀比较小,如何扩大前缀,增大寻找的范围。
确定指定前缀下所有子节点数
现在我们要做的是,给定一个前缀,返回下面的子节点的总数。
思路是:下一个前缀的起点减去当前前缀的起点,那么就是需要的当前前缀下的所有子节点数总和啦。
long getCount(long prefix, long n){ //prefix是前缀,n是上限
long curr = prefix;
long next = prefix + 1; //下一个前缀
long count = 0;
while (curr <= n){ //当前的前缀当然不能大于上限
count += (next - curr); //下一个前缀的起点减去当前前缀的起点
curr *= 10;
next *= 10;
// 如果说刚刚prefix是1,next是2,那么现在分别变成10和20
// 1为前缀的子节点增加10个,十叉树增加一层, 变成了两层
// 如果说现在prefix是10,next是20,那么现在分别变成100和200,
// 1为前缀的子节点增加100个,十叉树又增加了一层,变成了三层
}
return count; //把当前前缀下的子节点和返回去。
}
问题是,当next的值大于上限的时候,那么以这个前缀为根节点的十叉树就不是满十叉树了,应该到上限那里,后面不再有子节点,所以count+=next−cur
还是有些问题的。我们来修正这个问题:
count += (std::min(next, n + 1) - curr);
你可能会问:咦?怎么是 n+1 ,而不是 n 呢?不是说好了 n 是上限吗?
举个例子,假若现在上界 n为 12,算出以 1 为前缀的子节点数,首先 1 本身是一个节点,接下来要算下面 10,11,12,一共有 4 个子节点。
如果用std::min(next, n + 1) - curr
,这时候算出来会少一个,12 - 10 加上根节点,最后只有 3 个。因此我们务必要写 n+1。即:
long getCount(long prefix, long n){ //prefix是前缀,n是上限
long curr = prefix;
long next = prefix + 1; //下一个前缀
long count = 0;
while (curr <= n){ //当前的前缀当然不能大于上限
count += (std::min(next, n + 1) - curr); //下一个前缀的起点减去当前前缀的起点
curr *= 10;
next *= 10;
// 如果说刚刚prefix是1,next是2,那么现在分别变成10和20
// 1为前缀的子节点增加10个,十叉树增加一层, 变成了两层
// 如果说现在prefix是10,next是20,那么现在分别变成100和200,
// 1为前缀的子节点增加100个,十叉树又增加了一层,变成了三层
}
return count; //把当前前缀下的子节点和返回去。
}
第k个数在当前前缀下
需要往子树里面去看,也就是加一层:
prefix *= 10
第k个数不在当前前缀下
说白了,当前的前缀小了嘛,我们扩大前缀。
prefix ++;
总结
class Solution {
/*
* 计算[1,n]内以cur为根(开头)的节点个数
*/
long getNodes(long prefix, long n){ //prefix是前缀,n是上限
long curr = prefix;
long next = prefix + 1; //下一个前缀
long totalNodes = 0; // 统计合格的节点个数
while (curr <= n){ //当前的前缀当然不能大于上限
//totalNodes += (std::min(next, n + 1) - curr); //下一个前缀的起点减去当前前缀的起点
totalNodes += std::min(next - curr, n + 1 - curr);
curr *= 10; //往下层走
next *= 10; //往下层走
// 如果说刚刚prefix是1,next是2,那么现在分别变成10和20
// 1为前缀的子节点增加10个,十叉树增加一层, 变成了两层
// 如果说现在prefix是10,next是20,那么现在分别变成100和200,
// 1为前缀的子节点增加100个,十叉树又增加了一层,变成了三层
}
return totalNodes; //把当前前缀下的子节点和返回去。
}
public:
int findKthNumber(int n, int k) {
long curr = 1; //第一字典序小的(就是1)
long prefix = 1;
while (curr < k){ // 前缀从1开始
long nodes = getNodes(prefix, n); // 以 prefix 为根, 以 n 为最大值的十叉树的元素总个数
if(curr + nodes > k) { //第k个数在当前前缀下
prefix *= 10; //往下层遍历
curr++; //把指针指向了第一个子节点的位置,比如11乘10后变成110,指针从11指向了110
}else { // 以 prefix 为根的十叉树内没有第 k 个元素
prefix++; //扩大区间
curr += nodes ;
}
}
return prefix;
}
};