字典序的第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)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章