題目
給定整數 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…所以,一個數乘十和一個數加一在字典序中,前者會排在更前,最終我們可以把它歸納成一棵十叉樹,每個節點最多可以有十個子節點。
2.分解問題
在我們理解了什麼是字典序之後,我們就可以把這道題的問題分解成下面幾個問題:
- 計算前綴爲1~9的節點下面各有多少個節點。
- 第k個數位於哪個前綴下。
- 找到排序爲k的數。
(1)計算前綴爲1~9的節點下面各有多少個節點。
這個問題比較簡單,就是計算前綴爲i+1節點的第一個元素和前綴爲i節點的第一個元素的差值,然後遍歷所有這個節點下的所有層級,就可以得到該前綴下所有的節點數。舉個例子,例如n爲20的數組對應字典序的十叉樹如下:
它的層數是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)