440. 字典序的第K小数字 详解 & leetcode中相关的 top-K 题目

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); //左
    }

}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

熠熠98

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值