LeetCode440. 字典序的第K小数字

给定整数 n 和 k,返回 [1, n] 中字典序第 k 小的数字。
题目很简单,但是题本身并不简单。
LeetCode440. 字典序的第K小数字

一、偷懒技巧

一个偷懒的技巧是将数字转化为字符串,利用字符串排序,可以直接得有字典序的顺序排列,随后直接索引即可。时间复杂度为O(nlog(n))。

二、正常做法

什么是字典序

要查找第k小的数字,首先我们要明白什么是字典序。简单来说,就是根据数字的前缀进行排序,比如11和31,他们的前缀
为1和3,所以很明显11的字典序比31小,同样的,比较11和12,他们的第一个前缀都为1相同,需要依靠下一位继续比较,而下一位前缀分别为1和2,或者我们可以吧11和12也理解为前缀,所以我们同样得到11的字典序比12小,同理有11的字典序比110的字典序小,110的字典序小于12。

如何查找

要查找第k个,首先我们需要明白的一点是,要找到第k个元素的第一个前缀,第二个前缀……这样一直下去。
举个例子,假如n = 13,需要查询第2个元素,于是,我们开始从最小前缀x=1开始,查找以x开头的元素有几个(cnt)。
(1)如果cnt >= k,那么我们便很清楚的知道了,第k个元素的第一个前缀为x。
(2)如果cnt < k,那么我们也很清楚,第k个元素肯定不在以x为前缀的值里,于是我们使得x++,并更新k=k-cnt,查询以新的x为前缀的元素个数cnt,并继续如此的比较。
最终,我们一定有cnt>=k,于是我们确定了元素的前缀为x,这时候,我们就需要更新我们的前缀,即x=x*10,这看似跳了很多位,但实际上只是挪到了值x的下一个字典序的元素,因此需要有k=k-1。
到了新的x后,利用新的x前缀,再次计算以新的x为前缀的元素有几个(cnt),一直这样操作下去。直到我们有k=0,这里我们不需要担心k<0,因为k的每次减法都是减去<=k的元素。

计算cnt

接下来,我们仔细说说cnt如何计算?
定义cnt为从1~n中以x为前缀的元素个数。
取x的位数为xn,n的位数为nn,取元素n的前xn为值为u。其中k = nn - xn。
A.位数小于n的合法数总数
当xn<nn时,对于位数为xn的元素只有xn符合要求
当xn+1<nn时,对于位数为xn+1的元素只有xn0~xn9符合要求。
因此我们有位数xn~nn-1之间,我们一共有10 ** 0+10 ** 1+……10 ** k-1个元素。可以使用等比数列求和公式加以简化。
B.位数等于n的合法数总数
当计数完前面所有数之后,我们可以明显知道此时可以分为三个情况:
前面不同位数已经计数完毕,此时x = x + k个0。
(1)u == x
说明n与x有着相同的前缀,于是我们可以计算得到,1~n中以x为前缀的元素个数cnt = n − x∗10 ** k +1。
举个实际的例子:
n = 17582,x = 175
于是我们有x00~x82以及x都属于其中的元素。使用公式计算有
17582 - 17500 + 1 = 83。
(2)u < x
说明此时x的字典序是大于n的,因此在1~n之间cnt=0。
比如n = 17582,x = 176。u = 175
(3)u > x
此时所有位数为 nn 的数均小于 n,合法个数为 10 ** k,具体来说,n = 17582,x = 174。 u = 175,x + k个0 ~ x + k个9均合理。

完整代码如下:

c++

class Solution {
public:
    //假定我们存在某个函数 int getCnt(int x, int limit),该函数实现了统计范围 [1, limit][1,limit] 内以 x 为前缀的数的个数。
    int getCnt(int x, int limit){
        //随后计算两者之间的位数,n和m分别为x和limit的位数
        int n = to_string(x).size(),m = to_string(limit).size();
        int u = limit/(pow(10,m-n));//保存目前情况下与x同位的前缀
        int ans = 0;
        //计算位数的差距大小,方便计算个数
        int k = m - n;
        //随后开始循环迭代
        for(int i = 0;i < k;i++){
            ans+=pow(10,i);//从0开始的意义在于先算自己的一个
        }
        //循环迭代中已经把差距的位数之间的数计算完毕了,接下来需要计算相近情况下的数了
        //此时可以分为三个情况,主要的情况差异来自于前缀,其中有一种情况不需要讨论,因为这种情况下计数不会+1
        if (x == u){
            ans += limit - x*pow(10,k) + 1;
        }else if(x <u ){
            ans += pow(10,k);
        }
        return ans;
    }
    int findKthNumber(int n, int k) {
        int ans = 1;//初始化为1,因为如果k为1那答案就是1,ans的值也代表了目前的前缀是什么
        while(k > 1){//循环遍历寻找目标前缀
            int cnt = getCnt(ans,n);//计算得到中间的差距量,如果两者的减法在10以内,那肯定返回为1,为什么?因为此时ans肯定小于等于n,前缀数相等,要想保持不变的情况下扩展,只能是向后面加位数扩展,而显然在10差距内,不可能允许加位数扩展。
            if (cnt < k){//如果这个前缀下的所有可能的都小于了需要的序号,说明还在后面
                k -= cnt;
                ans += 1;//此时ans也需要升级到下一个阶段的去,也就是直接+1即可
            }
            else{
                k -= 1;//此时已经找到了大范围,需要慢慢遍历该前缀下的去
                ans *=10;//看似ans变大了,实际上只是到了该前缀下的第二个元素罢了
            }
        }
        return ans;
    }
};

python

class Solution:
    def getSteps(self, cur: int, n: int) -> int:
        steps, first, last = 0, cur, cur
        while first <= n:
            steps += min(last, n) - first + 1
            first *= 10
            last = last * 10 + 9
        return steps

    def findKthNumber(self, n: int, k: int) -> int:
        cur = 1
        k -= 1
        while k:
            steps = self.getSteps(cur, n)
            if steps <= k:
                k -= steps
                cur += 1
            else:
                cur *= 10
                k -= 1
        return cur

作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/k-th-smallest-in-lexicographical-order/solution/zi-dian-xu-de-di-kxiao-shu-zi-by-leetcod-bfy0/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

时间复杂度为O(log(n))^2

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值