440. 字典序的第K小数字 困难
题目描述:给定整数 n 和 k,返回 [1, n] 中字典序第 k 小的数字。
思路分析
什么是字典序?简而言之,就是根据数字的前缀进行排序。
比如 10 < 9,因为 10 的前缀是 1,比 9 小。
再比如 112 < 12,因为 112 的前缀 11 小于 12。
这样排序下来,会跟平常的升序排序会有非常大的不同。比如,一个数乘 10,或者加 1,哪个大?可能你会吃惊,后者会更大。
我们先将字典序抽象为字典树,如下图:
每一个节点都拥有 10 个孩子节点,因为作为一个前缀 ,它后面可以接 0~9 这十个数字。而且你可以非常容易地发现,整个字典序排列也就是对十叉树进行先序遍历(1, 10, 100, 101, … 11, 110 …)。
我们需要找到排在第k位的数。找到他的排位,需要搞清楚三件事情:
- 怎么确定一个前缀下所有子节点的个数?
- 如果第 k 个数在当前的前缀下,怎么继续往下面的子节点找?
- 如果第 k 个数不在当前的前缀,即当前的前缀比较小,如何扩大前缀,增大寻找的范围?
思路拆解分析
第一步:确定指定前缀下所有子节点数。
现在我们的目标是给定一个前缀,然后返回下面子节点总数。把数字全部转成字符串并排序,显然不现实。
字序列的字典序有一个特点:以数字 i
开头的所有数字串 按字典序一定 排在以数字 i+1
开头的所有数字串的前面。
对每个数字 i,唯一能做的就是确定以数字 i 开头的字符串的个数。但是有一个限制是,以 i 开头的数字不能超过最大数字 n。
- 比如 i = 1,n = 12时,以 1 开头的有 1,10,11,12,13,14,15,16,17,18,19。但是 n = 12,则数字只能是 1,10,11,12。以 1 开头的数字串共有 3 个。
所以,我们的思路就是用下一个前缀的起点减去当前前缀的起点,那么就是当前前缀下的所有子节点数总和了。
// prefix是前缀,n是上界
public long getCount(int prefix,int n) {
// 当前的前缀
long cur = prefix;
// 下一个前缀
long next = prefix + 1;
long count = 0;
while (cur <= n) {
count += next - cur;
cur *= 10;
next *= 10;
}
return count;
}
对上面代码进行分析:
- 如果说刚刚prefix是1,next是2,那么现在分别变成10和20,1为前缀的子节点增加10个,十叉树增加一层, 变成了两层。
- 如果说现在prefix是10,next是20,那么现在分别变成100和200,1为前缀的子节点增加100个,十叉树又增加了一层,变成了三层
当然,不知道大家发现一个问题没有,当 next 的值大于上界的时候
,那以这个前缀为根节点的十叉树就不是满十叉树了,应该到上界那里
,后面都不再有子节点。因此,count += next - cur;
还是有些问题的,做一下修改:
count += Math.min(n + 1,next) - cur;
那么为什么是 n+1 ,而不是 n 呢?不是说了 n 是上界吗?
假若现在上界 n为 13,算出以 1 为前缀的子节点数,首先 1 本身是一个节点,接下来要算下面 10,11,12,13一共有 5 个子节点。那么如果用 count += Math.min(n + 1,next) - cur;
会怎么样?
这时候算出来会少一个,13 - 10 加上根节点,最后只有 4 个。因此我们必须要写 n+1。
// prefix是前缀,n是上界
public long getCount(int prefix,int n) {
// 当前的前缀
long cur = prefix;
// 下一个前缀
long next = prefix + 1;
long count = 0;
while (cur <= n) {
count += Math.min(n + 1,next) - cur;
cur *= 10;
next *= 10;
}
return count;
}
第二步:如果第 k 个数在当前的前缀下,继续往下面的子节点找。
继续往下面的子节点找,prefix这样处理就可以了。
prefix *= 10;
第三步:第 k 个数不在当前的前缀,即当前的前缀比较小,继续扩大前缀寻找。
第k个数不在当前前缀下,说明当前的前缀小了,我们扩大前缀。
prefix ++;
最终代码整合
我们对上面的思路分析进行代码整合:
class Solution {
public int findKthNumber(int n, int k) {
// 作为一个指针,指向当前所在位置,当p==k时,也就是到了排位第k的数
int p = 1;
int prefix = 1;
while (p < k) {
// 获得当前前缀下有多少个子节点
long count = getCount(prefix, n);
if (p + count > k) { // 第k个数在当前前缀下
prefix *= 10;
p++;
} else if (p + count <= k) { // 第k个数不在当前前缀下
prefix++;
p += count; // 注意这里的操作,把指针指向了下一前缀的起点
}
}
return prefix;
}
// prefix是前缀,n是上界
public long getCount(int prefix,int n) {
// 当前的前缀
long cur = prefix;
// 下一个前缀
long next = prefix + 1;
long count = 0;
while (cur <= n) {
count += Math.min(n + 1,next) - cur;
cur *= 10;
next *= 10;
}
return count;
}
}
leetcode相关 top-K 题目
剑指 Offer II 076. 数组中的第 k 大的数字
- 建立一个优先级队列(小顶堆),里面只维护 k 个元素;
- 首先将数组中前k个元素放入堆中,堆顶是最小的元素;
- 后面从第 k+1 个数开始遍历数组,比较堆顶元素与数组元素中的值,当堆顶元素小于数组中的元素时,将堆顶元素弹出,新元素入堆,这样最终堆顶元素即为第k大。
public int findKthLargest(int[] nums, int k) {
// 构造小顶堆
PriorityQueue<Integer> queue = new PriorityQueue<>();
// 将前k个数放入优先级队列,此时堆顶就是最大的数
for (int i = 0; i < k; i++) {
queue.offer(nums[i]);
}
// 堆顶维护的就是第 k 大的值
for (int i = k; i < nums.length; i++) {
if (nums[i] > queue.peek()) {
queue.poll();
queue.offer(nums[i]);
}
}
return queue.peek();
}
剑指 Offer 54. 二叉搜索树的第k大节点
思路:二叉搜索树中序遍历的倒序是一个单调递减的序列,所以我们按照右中左的方式遍历即可。
class Solution {
int index = 0;
int result = 0;
public int kthLargest(TreeNode root, int k) {
dfs(root,k);
return result;
}
public void dfs(TreeNode root,int k) {
if (root == null) {
return;
}
dfs(root.right,k);
index++;
if(k == index) result = root.val; //根
dfs(root.left,k); //左
}
}