leetcode:440. 字典序的第K小数字

题目来源

题目描述

在这里插入图片描述

题目解析

思路一

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;
    }

};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
给定一个整数数组 nums 和一个目标值 target,要求在数组中找出两个数的和等于目标值,并返回这两个数的索引。 思路1:暴力法 最简单的思路是使用两层循环遍历数组的所有组合,判断两个数的和是否等于目标值。如果等于目标值,则返回这两个数的索引。 此方法的时间复杂度为O(n^2),空间复杂度为O(1)。 思路2:哈希表 为了优化时间复杂度,可以使用哈希表来存储数组中的元素和对应的索引。遍历数组,对于每个元素nums[i],我们可以通过计算target - nums[i]的值,查找哈希表中是否存在这个差值。 如果存在,则说明找到了两个数的和等于目标值,返回它们的索引。如果不存在,将当前元素nums[i]和它的索引存入哈希表中。 此方法的时间复杂度为O(n),空间复杂度为O(n)。 思路3:双指针 如果数组已经排序,可以使用双指针的方法来求解。假设数组从小到大排序,定义左指针left指向数组的第一个元素,右指针right指向数组的最后一个元素。 如果当前两个指针指向的数的和等于目标值,则返回它们的索引。如果和小于目标值,则将左指针右移一位,使得和增大;如果和大于目标值,则将右指针左移一位,使得和减小。 继续移动指针,直到找到两个数的和等于目标值或者左指针超过了右指针。 此方法的时间复杂度为O(nlogn),空间复杂度为O(1)。 以上三种方法都可以解决问题,选择合适的方法取决于具体的应用场景和要求。如果数组规模较小并且不需要考虑额外的空间使用,则暴力法是最简单的方法。如果数组较大或者需要优化时间复杂度,则哈希表或双指针方法更合适。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值