字典序的第K小数字

题目

给定整数 n 和 k,找到 1 到 n 中字典序第 k 小的数字。

注意:1 ≤ k ≤ n ≤ 109。

示例输入:

n: 13 k: 2

输出:
10

解释:
字典序的排列是 [1, 10, 11, 12, 13, 2, 3, 4, 5, 6, 7, 8, 9],所以第二小的数字是 10。

题解思路

1.什么是字典序?

字典序我们的常见的升降序不一样,它是一个树形结构,如下图所示,例如当输入n为110时,这个数组用字典序排序后前面数字为:1,10,100,101,102,103,104,105,106,107,108,109,11,110…所以,一个数乘十和一个数加一在字典序中,前者会排在更前,最终我们可以把它归纳成一棵十叉树,每个节点最多可以有十个子节点。

image

2.分解问题

在我们理解了什么是字典序之后,我们就可以把这道题的问题分解成下面几个问题:

  • 计算前缀为1~9的节点下面各有多少个节点。
  • 第k个数位于哪个前缀下。
  • 找到排序为k的数。

(1)计算前缀为1~9的节点下面各有多少个节点。

这个问题比较简单,就是计算前缀为i+1节点的第一个元素和前缀为i节点的第一个元素的差值,然后遍历所有这个节点下的所有层级,就可以得到该前缀下所有的节点数。举个例子,例如n为20的数组对应字典序的十叉树如下:

image

它的层数是2层,所以我们需要计算两次,首先计算第一层,2减1为1,再计算第二层,20减10为10,然后累加得到前缀为1的所有节点个数为1+10=11个。我们把它用代码表示出来如下:

public long getPrefixNum(long prefix, long n) {
	long curValue = prefix;        // 前缀为prefix每一层元素的第一个数
	long nextPrefix = prefix + 1;  // 前缀为prefix+1的每一层的第一个数
	long count = 0;                // 计算前缀为prefix下节点总数的变量
	while(curValue <= n) {        
		count += nextPrefix - curValue; // 计算每一层的第一个数的差值,并累加
		curValue *= 10;                 // 切换到下一层
		nextPrefix *= 10;                
	}
	return count;
}

这里的代码看似没有问题,但是我们还把遗漏的case忽略掉了,假如当n大于100而小于200时,上面这段代码就会有问题。例如当n等于100的时候,实际上前缀为1的树里面已经达到了三层,而前缀为2的树还是只有两层,如果我们继续按照上面的代码,计算到第三次while循环的时候,nextPrefix就已经变成了300,显然300已经比n的值(299)大,所以我们不能用200-100来算,因为前缀1不是一个全十叉树,相反应该用n+1和nextPrefix的较小值来和100来相减,通过优化代码修改如下:

public long getPrefixNum(long prefix, long n) {
	long curValue = prefix;        // 前缀为prefix每一层元素的第一个数
	long nextPrefix = prefix + 1;  // 前缀为prefix+1的每一层的第一个数
	long count = 0;                // 计算前缀为prefix下节点总数的变量
	while(curValue <= n) {
		count += Math.min(n+1, nextPrefix) - curValue; // 计算每一层的第一个数的差值,并累加
		curValue *= 10;                 // 切换到下一层
		nextPrefix *= 10;                
	}
	return count;
}

(2)第k个数位于哪个前缀下

分别遍历各个前缀的个数,然后比较k和该前缀以前的元素个数大小,如果k大的话,就将前缀加一,相反的话就落入该前缀里面。

(3)找到排序为k的数

确定了是哪个前缀之后,继续遍历层数,直到找到第k个数。

题解代码

public class Question440 {
	
	public static void main(String[] args) {
		System.out.println(findKthNumber(10, 100));
	}

	public static long getPrefixNum(long prefix, long n) {
		long curValue = prefix;        // 前缀为prefix每一层元素的第一个数
		long nextPrefix = prefix + 1;  // 前缀为prefix+1的每一层的第一个数
		long count = 0;                // 计算前缀为prefix下节点总数的变量
		while(curValue <= n) {
			count += Math.min(n+1, nextPrefix) - curValue; // 计算每一层的第一个数的差值,并累加
			curValue *= 10;                 // 切换到下一层
			nextPrefix *= 10;                
		}
		return count;
	}
	
	public static int findKthNumber(int k, int n) {
		int curPrefix = 1;      //当前指向的数字
		long curIndex = 1;       //当前指向数字在字典序中的位置
		
		while(curIndex < k) {
			long prefixNum = getPrefixNum(curPrefix, n);
			if (prefixNum + curIndex > k) {
				curPrefix *= 10;       //比如前缀1就变成10,10就变成了100,缩小区间
				curIndex++;			   //当前数的位置就变成了原来的加1	
			} else {
				curPrefix++;           //比如前缀1变成了2,前缀10的话就变成了11,右移前缀
				curIndex += prefixNum; //当前数的位置就变成了原来位置加上上一个前缀的子节点个数
			}
		}
		return curPrefix;
	}
}

复杂度

时间复杂度:O(n^2)
空间复杂度:O(1)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值