题目:给定整数 n
和 k
,返回 [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;
}