LeetCode 字典序的第K小数字-代码注释详解

题目:给定整数 nk,返回 [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

题目来源:力扣(LeetCode)

思路总结:

拿到题目,看到字典序,想到了python直接用str排序:

s=sorted(str(i) for i in rang(1,n+1))

return eval(s[k-1])

但n的数据10的九次方,肯定超时,要想别的办法。

看到1,10,11,12,13,14.。。。。。。想到了哈夫曼编码,有点类似,然后再画一幅图

可以发现这样的字典序正好是以1,2,3,4,5,6,7,8,9为前缀的前缀编码树,而树的前序遍历正好是题目要求的字典序,但难以下手。

参考了大佬的思路和代码,写一下详细的代码注解。

想象一下  n无限大,那么这个字典序是不是可以从1,10,100,1000,10000,10000...... 这样不断的进行先序遍历,直到限制这个n的大小,使得到了某个值(例如100000)这个值已经比n大了,是不是就可以把100000以后的值扔掉了。再回去回溯。

举个具体的例子体会一下:

 假设 getCnt(int x, int limit)为求以x为前缀,小于limit的数的个数

假设x=1  limit=134

看上图:1为前缀 可以从1开始走,走到10,并且两位数这一层从10-19都可以 要,因为limit是三位数,以1为前缀的两位数这一层一定可以取完,而走到100这个时候,能不能取完100-199这些数呢,显然到了134的时候就已经不能取了,并且更加不可能往下去取四位数的。所以x与limit的位数相差直接影响了能从前缀编码往下走多少层。

再举个例子:如果x=1,limit=245,显然可以走完10-19,但是100-199能不能走完呢,245>199,所以三位数这一层也可以走完,也就是三位数这一层能不能走完与limit的第一个数字有关系,如果limit的第一个数字大于x,那么一定可以走完与limit位数相同的那一层。同样的,因为limit是三位数,所以不可能往下走到四位数。

那么以x=1为前缀编码,小于limit的数一共有多少个就求出来了,如果k还大于这个数,那么就说明以x=1为前缀编码不够列举,需要编码为2开始列举了,如果前缀编码为2仍然不够,则到前缀编码3。直到某个以x为前缀编码的数够列举,则能知道要找的数一定是以x为前缀的,至于是哪一个,我们要按照先序遍历的顺序去找,缩小前缀编码的范围,也就是从第一个孩子开始找,这个时候不必担心sum的值会不会超过题目给的limit(也就是题目给的n值),因为前面已经判断了以x为前缀编码小于limit的数已经超过了第k个,所以要找的第k个一定在这个以x为前缀编码的树当中,则一定不会超出这个n值。在利用先序遍历去缩小范围。建议用  x=1,limit=134,和x=1,limit=234去体会如何从树根开始往下寻找的过程。

#include<iostream>
#include<string>
using namespace std;
// 以x为前缀,寻找小于limit的数的个数  这里返回值要用long 用int会溢出
long getCnt(int x, int limit) {
	string a = to_string(x);
	string b = to_string(limit);
	int n = a.size();
	int m = b.size();
	int k = m - n; // a,b相差多少位数决定了可以往下最多走多少层
	int ans = 0;
	int u = stoi(b.substr(0, n));//u 为limit取与x等长的前缀
	for (int i = 0; i < k; i++) {
		ans += pow(10, i);//举个例子:  x=1  limit=345 则在树中  至少可以走到三位数这一层,但是以能不能 走完这一层,得看u
	}
	if (u > x)		ans += pow(10, k);//接着上个例子  x=1 limit=345 走到了三位数这一层,因为3>1 所以可以走完100-199这一层
	else if (u == x) {
		ans = ans + limit - x * pow(10, k) + 1; //如果u=x   例如x=1, limit=123 则要看三位数这一层 从100开始 能走多少个 ,在与ans相加
	}
	return ans;
}
int findNumber(int n, int k) {
	int sum = 1;
	while (k > 1) {
		int cnt = getCnt(sum, n); //取得以sum为前缀,n为限制的数的个数
		if (cnt < k) { //cnt小于k  则代表 以这个为前缀且不大于n的数全部都可以跳过
			k -= cnt;
			sum++;// 前缀从1开始 递增
		}
		//如果cnt>=k 则代表第k个数一定在以ans为前缀的编码中  然后逐渐缩小这个编码 
		//比如找到了第k个数在以1为前缀的数中,往下走一个就是10,再看看以10为前缀有多少个数,再与k比较,判断是否在以10为前缀的树中
		//如果不在,则10找完了 递增去11找
		else {
			k--;
			sum = sum * 10;//因为是字典序,所以应该一直前序遍历去找这个数,例如:如果都小于n 则10后一个数为100,100后一个数为1000等等
		}
	}
	return sum;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值