有趣的演算法題之移除 k 位數字後使剩下的數字最小
1 題目
給定一個整數,從該整數中去掉 k 個數字,使剩下的數字組成的新整數儘可能小,那麼應該選擇去掉的數字。
2 思路
感覺這是個挺有意思的問題,所以當時認真的讀了讀也認真的想了想,真是不想不知道,一想才發現演算法真的分優劣。首先這個題目是什麼意思呢?一個數字移除 1 位後肯定會變小,問題是變小多少,最簡單直接的方法是移除掉最後一位,那麼會變小 10 倍左右,假如有一個 5 位整數 54127,移除 1 位數字如果移除最後一位變成 5412,變小了 10 倍左右,但是這肯定不是最小的。移除 1 位後變成 4 位整數,既然位數一樣,那麼肯定是高位的數字越小結果就越小,對於 54127 顯然移除第一位變成 4127 是最小的,變小了 13倍左右。
那麼是不是移除第一位一定是最小的呢?也不是,假設有一個 5 位整數 45127,移除 1 位數字如果移除最後一位變成 4512,變小了 10 倍左右,如果移除第一位變成 5127,最高位反而升高了,只變小了 8.8 倍左右,還不如移除最後一位,所以我們選擇去掉的數字的原則應該是原整數的所有數字從左到右進行比較,如果發現某一位的數字大於它右面的數字,那麼在刪除該數字後,必然會使得該數位的值降低,因為右面比它小的數字頂替了它的位置。
再給一個例子,假如有一個整數 541270936,需要移除 3 個數字,按照上面的原則,第一個去掉的應該是 5,因為 5>4,;第二個去掉的應該是 4,因為 4>1,第三個去掉的應該是 7,因為 1<2,2<7,7>0,所以 541270936 去掉 3 個數字後得到的最小整數應該是 120936。
而且注意一下,假如有一個整數 30200,需要移除 1 個數字,按照上面的原則,應該去掉第一個數字 3,然後剩下的數字為 0200,我們應該記錄為 200,所以我們要考慮處理後的數字前面要去掉 0 的情況。
3 程式碼實現
理清思路後,就來簡單用程式碼實現一下這個演算法,
@Test public void testRemoveKDigits() { removeKDigits1("541270936", 3); } /** * author:MrQinshou * Description:初版,以 k 為外層迴圈,每次外迴圈再去遍歷全部字串,每次刪除一個數字 * date:2018/11/27 20:21 * param * return */ public static String removeKDigits1(String string, int k) { // 以刪除多少位為外層迴圈 for (int i = 0; i < k; i++) { // 定義一個標誌位記錄是否有高位的數字被刪除 boolean hasCut = false; // 每次迴圈遍歷目標字串的所有字元 for (int j = 0; j < string.length() - 1; j++) { char a = string.charAt(j); char b = string.charAt(j + 1); // 如果當前字元(高位數字)比下一個字元(低位數字)要大 // 則擷取字串,並置標誌位為 true if (a > b) { string = string.substring(0, j) + string.substring(j + 1, string.length()); hasCut = true; break; } } // 如果沒有高位數字被刪除,則刪除最後一個數字 if (!hasCut) { string = string.substring(0, string.length() - 1); } // 去掉前面的 0 string = removeZero(string); System.out.println("string--->" + string); } if (string.length() == 0) { System.out.println("刪除 " + k + " 個數字後的最小值--->" + 0); return "0"; } System.out.println("刪除 " + k + " 個數字後的最小值--->" + string); return string; } /** * author:MrQinshou * Description:去掉字串前面的 0 * date:2018/11/27 20:23 * param * return */ private static String removeZero(String string) { for (int i = 0; i < string.length() - 1; i++) { if (string.charAt(0) != '0') { break; } string = string.substring(1, string.length()); } return string; }
執行一下列印如下:
結果是木有任何問題的,LeeCode 上有這道題,移掉 K 位數字,放到上面是玩耍一下(記得註釋掉列印):
貌似效率是有點低呀,接著往下看發現智慧與美貌並存的小灰介紹了另外一種方式來解答該題目。
4 優化
原整數的長度記為 n,上面的演算法因為外層迴圈 k 次,內層迴圈每次為 n 次,時間複雜度為 O(kn),最壞的情況下 k=n,則時間複雜度為 O(n²)。我們可以換一種思路,不讓它巢狀迴圈,只遍歷原整數一遍,用一個棧來存放所有數字,讓數字一個個入棧,然後當入棧的數字比前面的數字要小時,則讓前面的數字出棧,最後把棧內元素去掉 0 再處理成字串,變成我們想要的結果。
/**
* author:MrQinshou
* Description:最終版,用棧來實現
* date:2018/11/27 20:23
* param
* return
*/
public static String removeKDigits(String string, int k) {
// 建立一個棧,用於接收所有的數字
char[] stack = new char[string.length()];
int top = 0;
// 定於一個 copyK 等於 k,用於記錄需要移除多少位數字,也就是需要
// 出棧多少次,k 在最後還要用於確定新整數的長度,所以不要直接
// 操作 k
int copyK = k;
for (int i = 0; i < string.length(); i++) {
// 前一個數字大於當前數字時並且還有剩餘次數,前一個數字出棧,棧頂指標前移
// 注意這裡是 while 不能是 if,如整數 45127,k=2 時,當指標
// 指向數字 1 時,如果用 if,只會比較一次,讓前一個數字 5 出
// 棧,但 1 仍小於 4,所以 4 也應該出棧,所以需要用 while
// 比較完前面所有數字
while (top > 0 && stack[top - 1] > string.charAt(i) && copyK > 0) {
top--;
copyK--;
}
stack[top] = string.charAt(i);
top++;
System.out.println("stack--->" + Arrays.toString(stack));
}
// 找到棧中第一個非 0 數字的位置,以此構建新的整數字符串
int offset = 0;
for (int i = 0; i < stack.length; i++) {
if (stack[offset] == '0') {
offset++;
}
// 加了 else 跳出後反而效率會降低,一臉懵逼
// else {
// break;
// }
}
// 新整數的長度為原整數的長度減去 k
// 如果 offset 大於等於新整數的長度,則返回 0,否則從第一個
// 非 0 數字開始擷取到與新整數長度相等的數字作為返回值
System.out.println("刪除 " + k + " 個數字後的最小值--->" + (offset >= string.length() - k ? "0" : new String(stack, offset, string.length() - k - offset)));
return offset >= string.length() - k ? "0" : new String(stack, offset, string.length() - k - offset);
}
上面的遍歷時的 while 要注意,不能是 if,這個問題我剛開始也糾結了好久,如整數 45127,k=2 時,當指標指向數字 1 時,如果用 if,只會比較一次,而且比 1 大的應該有 5 和 4 兩個數字。
執行結果如下:
接下來就可以在 LeeCode 上愉快地玩耍了(記得註釋掉列印),最後的去掉 0 我改成了 for 迴圈沒有用小灰文中的 while 迴圈,效率還高了一點點:
5 總結
以前在面試中沒有怎麼遇到過演算法題,剛開始看到這個題目的時候還覺得很有趣,等自己真的去理解思路的時候還是有點困難,特別是後面的優化,這也說明了棧這種資料結構也是蠻有用的,繼續努力。