https://leetcode.com/problems/k-th-smallest-in-lexicographical-order/
这题乍一看有点吓人,主要因为数字的自然顺序和字典顺序之间没有什么非常简单的映射规则,但可以想到的是字典序这个东西在10以内的数字是有一定规律的,例如0,1,2,3,4,5,6,7,8,9既符合数字的自然顺序,又符合字典序,只是每当发生进位时会发生一些变化。例如9进位到10以后,10这个数字的字典序就要罗到2之前。模糊感觉这是一种层级的循环式的规律,为了能够更直观地发现规律,我尝试在纸上画了画图,最终画成了这个样子,为了省事,从左至右每一层我都只展开了第一个节点,大家只要明白从左到右的层级代表数字从左到右的每位值即可。
然后会发现如果我们从1开始一个个往图中添加数字,有一个规律,就是添加节点的顺序是逐层从上到下的,像是一个二叉树的层级遍历的情况,并且这会导致树的形状类似于一颗完全N叉树,这个顺序对应着数字的自然顺序,而字典序则不然,字典序更有点像自上而下的深度优先遍历(大家在纸上稍微画画就知道了)。大家记住这两种遍历顺序,后面就好做推理了,这种结构逻辑上就是一棵树,树的每个节点的分支最多10个(0~9)。
知道这些对获取字典序第K个节点有什么帮助呢?当然有,理论上,给你一棵树,指定一种遍历顺序,让你给出第K个节点不是很简单的事了吗?遍历一遍,取第K个即可。
但是再仔细一想,真的需要按顺序从头遍历一遍吗?我看不必吧?例如当我确定首层应该选1~9中的哪个数字时,我是不是可以计算一下以1为树根的树的节点数?如果这个节点树比K小,那么K对应的数字就肯定不在以1开头的数字中,那么我们直接继续往下检查2开头的数字(即以2位根节点的子树)即可,我称之为跳跃式遍历(即有时可以一次跳过一整棵子树),按照这种思路,结合类似回溯法的思想,逐层进行这种跳跃式遍历,最终必然可以找到某个数节点,对应第K个字典序的数字。
思路大概就是如此,并不复杂,不过代码写起来有点难。
我这里主要写了几个辅助函数,有了它们,整体的逻辑就简单清晰很多:
1:howManyNodesFromThisRoot, 这个函数用来计算以某个节点为根的子树的节点个数,这里“某个节点”我用一个List<Integer>来表示,代表从整棵树的树根到我们要找的子树树根的完整路径,例如[1,0]代表以1,0开头的子树
2: remainingNum, 对一个给定的数字,在固定相同的前几位的情况下,后面有多少种数字组合使得数字小于给定的数字,例如12345作为给定的数字,求以123开头的并且小于12345的所有数字的个数
3: compRootsNumber, 给定一个数字A和一个数字前缀prefix,比较A中同prefix长度相同的前缀子串对应的数字跟prefix代表的数字的大小,例如给定数字12345,和一个数字前缀53,则比较12和53的大小
4:digitCount, 计算一个十进制数字的位数,这个比较简单,不赘述
5: calcVariance, 给定位数以内的所有十进制数字组合数
6: joinAndCreateNewArray,临时创建(copy)一个Integer数组,用来做尝试方案时使用
有了以上辅助函数,结合上面的思路分析,完整代码逻辑如下,核心逻辑函数为trySolution:
package com.example.demo.leetcode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class KsmallestLexi {
int accumulate =0;
/**
* tested
* @param n
* @return
*/
private int digitCount(int n){
if(n<10){
return 1;
}
int ret = 0;
int k = n;
while(k>=10){
k = k/10;
ret++;
}
return ret+1;
}
private void trySolution(int maxNum, int kth, List<Integer> tryPath){
// System.out.println("Accumulated: "+tryPath+":"+ accumulate);
if(accumulate==kth){
System.out.println("Accumulated: "+tryPath);
return;
}
int maxLevel = digitCount(maxNum);
int tempAccumulate = accumulate;
if(tryPath.size()==0){
for(int i=1;i<=9;i++){
tempAccumulate+=howManyNodesFromThisRoot(Arrays.asList(i), maxNum, maxLevel);
if(tempAccumulate>=kth){
tryPath.add(i);
accumulate+=1;
break;
}
accumulate=tempAccumulate;
}
}else{
for(int i=0;i<=9;i++){
tempAccumulate+=howManyNodesFromThisRoot(joinAndCreateNewArray(tryPath, i), maxNum, maxLevel);
if(tempAccumulate>=kth){
tryPath.add(i);
accumulate+=1;
break;
}
accumulate=tempAccumulate;
}
}
trySolution(maxNum, kth, tryPath);
}
/**
* tested
* @param base
* @param el
* @return
*/
public List<Integer> joinAndCreateNewArray(List<Integer> base, int el){
List<Integer> ret = new ArrayList<>(base);
ret.add(el);
return ret;
}
public int howManyNodesFromThisRoot(List<Integer> position, int maxNum, int maxLevel){
int ret;
int leftMaxDigits = maxLevel - position.size();
int compRoots = compRootsNumber(position, maxNum);
if(compRoots==1){
ret = 1+calcVariance(leftMaxDigits-1);
}else if(compRoots==0){
ret = 1+remainingNum(maxNum, position.size());
}else{
ret = 1+calcVariance(leftMaxDigits);
}
System.out.println("========from here");
System.out.println(position);
System.out.println(ret);
System.out.println("======count above");
return ret;
}
/**
* tested
* @param position
* @param num
* @return
*/
public int compRootsNumber(List<Integer> position, int num){
int dgs = digitCount(num);
int rootNum = 0;
int psize = position.size();
for(int i=0;i<position.size();i++){
rootNum+=position.get(psize-i-1)*((int)Math.pow(10, i));
}
int num2divde = (int)Math.pow(10,dgs-position.size());
if(rootNum>num/num2divde){
return 1;
}else if(rootNum==num/num2divde){
return 0;
}else{
return -1;
}
}
/**
*
* @param num
* @param excludeLeadingNDigits
* @return
*/
public int remainingNum(int num, int excludeLeadingNDigits){
int dgs = digitCount(num);
if(dgs==excludeLeadingNDigits){
return 0;
}
int num2divide = (int)Math.pow(10, dgs-excludeLeadingNDigits);
int ret=0;
for(int i=1;i<dgs-excludeLeadingNDigits;i++){
ret += (int)Math.pow(10,i);
}
ret+=(1+num-(num/num2divide)*num2divide);
return ret;
}
/**
* tested
* @param digitCnt
* @return
*/
private int calcVariance(int digitCnt){
if(digitCnt==0){
return 0;
}
int ret = 0;
for(int i=1; i<=digitCnt;i++){
ret+=(int)Math.pow(10,i);
}
return ret;
}
public int findKthNumber(int n, int k) {
accumulate = 0;
List<Integer> tryPath = new ArrayList<>();
trySolution(n, k , tryPath);
int psize = tryPath.size();
int sum = 0;
for(int i=0;i<tryPath.size();i++){
sum+=tryPath.get(psize-i-1)*((int)Math.pow(10, i));
}
return sum;
}
public static void main(String[] args) {
KsmallestLexi demo = new KsmallestLexi();
int ret1 = demo.findKthNumber(13, 2);
int ret2 = demo.findKthNumber(100, 90);
int ret3 = demo.findKthNumber(10000, 10000);
System.out.println(ret1);
System.out.println(ret2);
System.out.println(ret3);
}
}
反思:这题做到通过OJ花了我半天时间,主要开始卡在了是否需要建树和如何遍历树上,后来发现这个树只要作为一个逻辑概念参考即可,不需要真正建出来,即使建出来了,也不必傻呵呵地从头遍历,而是可以运用一些数学运算来做跳跃式遍历,这里是比较考验代码技巧的部分