题目

给定整数 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

提示:

1 <= k <= n <= 10^9

思路

很好的题目,一开始就想到了这其实是一个多叉树的遍历问题,构造出来是一个字典树,如下图所示(画的有点丑…),图中的440. 字典序的第K小数字 - 图1440. 字典序的第K小数字 - 图2440. 字典序的第K小数字 - 图3440. 字典序的第K小数字 - 图4。前序遍历第440. 字典序的第K小数字 - 图5个节点440. 字典序的第K小数字 - 图6就是答案。

image.png

因为440. 字典序的第K小数字 - 图8的数据范围,所以肯定不能直接挨个节点遍历。

我们可以发现,对于第440. 字典序的第K小数字 - 图9小的节点440. 字典序的第K小数字 - 图10,往后再数440. 字典序的第K小数字 - 图11个就是答案。记以节点440. 字典序的第K小数字 - 图12为根节点的子树一共有440. 字典序的第K小数字 - 图13个节点。

如果440. 字典序的第K小数字 - 图14,那么,第440. 字典序的第K小数字 - 图15小的数肯定不在这个子树中,直接跳到440. 字典序的第K小数字 - 图16相邻的节点440. 字典序的第K小数字 - 图17,反之,在该子树中,440. 字典序的第K小数字 - 图18更新为左边第一个孩子,即440. 字典序的第K小数字 - 图19

通过这个思路,我们可加快寻找第440. 字典序的第K小数字 - 图20小的数,具体实现见代码。

代码

  1. class Solution {
  2. public int findKthNumber(int n, int k) {
  3. // 当前数字,可以看做是一个前序遍历的指针
  4. int p = 1;
  5. // 当前数字是第i小的
  6. int i = 1;
  7. // 要找第k小的
  8. while (i < k) {
  9. int cnt = getChildNum(p, n);
  10. // 对应思路中的分析
  11. // 如果cnt <= k - i 第k个肯定不在当前子树中 去p右边相邻的兄弟节点寻找 所以p++
  12. if (cnt <= k - i) {
  13. p++;
  14. i += cnt;
  15. } else {
  16. // cnt > k - i 第k个在当前子树中 在当前子树中寻找 先看左边第一个孩子节点 所以p*=10
  17. p *= 10;
  18. i++;
  19. }
  20. }
  21. return p;
  22. }
  23. // 计算以p为根节点的子树的节点个数(包括p节点)
  24. private int getChildNum(long p, int n) {
  25. // 一层一层累加计数的思路计算节点个数
  26. // 第一层1个 第二层10个 第三层100个
  27. // cnt为累加的节点个数 最后的返回值
  28. int cnt = 0;
  29. // p指向当前层第一个节点 last指向最后一个
  30. long last = p;
  31. while (p <= n) {
  32. // 将当前层个数累加
  33. cnt += Math.min(last, n) - p + 1;
  34. // 更新p和last
  35. p *= 10;
  36. last = 10 * last + 9;
  37. }
  38. return cnt;
  39. }
  40. }