leetcode 440. 字典序的第K小数字(精)

13 篇文章 1 订阅

题目:
给定整数 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。

解题思路:
艰难的一题,从昨晚9点开始脑子就一直在想这道题的解法,连睡觉我都感觉自己大脑没有关机,似乎把一直在努力把昨晚想出来的思路保存在大脑内存里,再加上今天一天的努力,终于解决了这道提交率只有17%的难题。没办法,我没有敏捷的思维,只有坚持不懈的毅力。

首先,leetcode里有一道medium题386,是要求将从1到n的数按字典序排列,理解那道题是 解决这道题的基础,也在我的博客里。

大概解释一下字典序排序的基本思想,就是将每个数字进行DFS,1之后是10,10之后是100,如果继续纵向增加超过了n的大小,就不再纵向增加,而是横向增加:101,102…等。

但是这道题,如果将1到n的数字按字典序排列好再输出第k个数字,是会TLE的,这就要求我们从中找到规律,用一些四则运算,在常数系数的复杂度内找到我们要找的数。所以这已经不是一道编程题,而是一道数学题。

我们可以从1到100,慢慢地将数字一个一个地插入到它们该插入的地方,以此来查找其规律之所在。
从1开始,1,2,3,4,5,6,7,8,9
从10开始,1,10…19,2,3,4,5,6,7,8,9
从20开始,1,10…19,2,20…29,3,4,5,6,7,8,9
以此类推,所有的两位数,都插入了到与他们10位数相等的个位数后面。

再让我们来看看百位数如何插入
从100开始,1,10,100-109,11,110-119,12,120-129…2,21-29…
从200开始,1,10,100-109…2,20,200-209,21,210-219…3,31-39…
以此类推,所有的三位数,都插入了到与他们百位至十位数相等的十位数后面。

这里我们就已经能得到整个数组的排列规律了:整个数组可以构成一棵树,树的先序遍历结果就是数组字典序排列顺序。
(下面只以1为例)

            1-9  
		   / \
		  10-19
		 / \    
	   100-109
	   /  \  
	 1000-1009
.....................

可以发现,每个个位数都分别对应一棵“十叉树”,并且随着n的增大,树的深度是越来越深的。我们想要有规律地找到第k个数字,就要先确定下来树的深度deep,deep确定下来,每一棵树的元素总数目就确定了下来,这样我们就可以根据除法及取余操作,确定下来第k个数在哪棵树下面,以及该数是这棵树的第几个元素(余数p)。

剩下的问题就是在这颗树中找到它的第p个数。这个问题也要用数学的方法去解决,不能暴力遍历。我的方法是不断地对p进行除法及取余操作,确定它归属的第二层结点,第三层结点…最终确定下来它的位置。以上面的图为例,从上至下每一层中的节点所附庸的节点数目(包括自己)分别是:1111,111,11,1。那么p/111的商result就是它归属于第二层节点的位置,余数p就代表它在该子树中是第几个元素,然后递归计算查找即可。

需要特别注意的是,我们的n可能不会恰好取到9、99、999这样“完满”的数从而生成“完全十叉树”,它可能是432,567等这样“不完满”的数,会使得总树群生成为“不完全十叉树”。这样树群就被分成了左右两部分,左边部分比右边部分的深度大1。因此我们得先确定第k个数分到了左侧的树中还是右侧的树中。这关系到我们查找第k个数时所用的deep值。因此我们就需要找到树群分界点---------也就是最后一个叶节点,它的字典序值。

以上面的树状图举例,假如n=1009,我们可以从底层向顶层,一层一层地确定该数节点左侧的节点总数(包括自己),分别是:10,1,1,1,相加起来,即是n所在的字典序值pos=13,也就是分界点的字典序值。然后判断如果k<=pos,那么k在左侧的树群中,k和deep值都无需变化。如果k>pos,那么k在右侧的树群中,就得将deep-1,并且将k减去最底层的元素数目,进行k的查找。(这里大家多思考思考为什么要减去底层元素数目,因为我将整个树群都视为了deep-1深度的树,就该削去最后一层元素,还原deep-1深度的树该有的值。下面配个图形象展示一下。)
在这里插入图片描述

下面是我写的代码,如果大家理解了我的思路,就很容易读懂了。大家也可以自己动手写一下,我所写的仅供参考。

4ms

class Solution {
public:
	int findKthNumber(int n, int k) {
	    //*************************
	    //第一步,得到树群的最大深度。
		if (n<10)
			return k;
		int sum = 9;
		int base = 1;
		int deep = 0;
		while (n>sum) {
			base *= 10;
			++deep;
			sum += 9 * base;
		}
		//*************************
		//第二步,确定k在左侧树群还是在右侧树群。
		int redund = n - (base - 1);
		int lisNum = redund;
		while (base != 1) {
			n /= 10;
			base /= 10;
			int tmp = n - (base - 1);
			cout << tmp << endl;
			lisNum += tmp;
		}
		if (k > lisNum) {
			--deep;
			k -= redund;
		}
		//*************************	
		//第三步,在k所归属的树群中找到其值。
		int partSum = 0;
		vector<int>work(deep + 1, 0);
		for (int i = 0; i <= deep; ++i) {
			partSum += pow(10, i);
			work[deep - i] = partSum;
		}
		int seq = (k - 1) / partSum + 1;
		int rem = (k - 1) % partSum;
		string result;
		result.push_back('0' + seq);
		for (int i = 1; i <= deep && rem != 0; ++i) {
			--rem;
			int order = rem / work[i];
			rem = rem % work[i];
			result.push_back('0' + order);
		}
		return stoi(result);
	}
};
  • 8
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值