轉載請以鏈接形式標明出處:
本文出自:103style的博客
原題鏈接 -- https://leetcode-cn.com/problems/minimum-possible-integer-after-at-most-k-adjacent-swaps-on-digits/
給你一個字符串 num
和一個整數 k
。其中,num
表示一個很大的整數,字符串中的每個字符依次對應整數上的各個 數位 。
你可以交換這個整數相鄰數位的數字 最多 k
次。
請你返回你能得到的最小整數,並以字符串形式返回。
示例 1:
輸入:num = "4321", k = 4
輸出:"1342"
解釋:4321 通過 4 次交換相鄰數位得到最小整數的步驟如上圖所示。
示例 2:
輸入:num = "100", k = 1
輸出:"010"
解釋:輸出可以包含前導 0 ,但輸入保證不會有前導 0 。
示例 3:
輸入:num = "36789", k = 1000
輸出:"36789"
解釋:不需要做任何交換。
示例 4:
輸入:num = "22", k = 22
輸出:"22"
示例 5:
輸入:num = "9438957234785635408", k = 23
輸出:"0345989723478563548"
提示:
1 <= num.length <= 30000
-
num
只包含 數字 且不含有 前導 0 。 1 <= k <= 10^9
題目來源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/minimum-possible-integer-after-at-most-k-adjacent-swaps-on-digits
目錄: 理解題意 → 遞歸解法 → 優化遞歸 → 優化到O(N logN)
理解題意
首先根據題目描述,我們可以得到:
要想在移動 k 次之後得到最小的數, 必須每次移動儘可能的在K次內,把從 0 開始到 9 的數移動到使前面沒有比它大的數字的位置。
比如: num = 4321
, k = 4
.
我們能找到的最小的數是 1
, 把 1
移動到開頭需要移動 3
次。
所以移動之後得到: num = 1432
, k = 4 - 3 = 1
;
然後從 1 後面開始,我們能找到的 2
, 把 2
移動到 1
後面需要移動 2 次, 但是 k = 1, 所以我們得找下一個小的數。 我們找到了 3
,然後把 3
移動到 1 後面需要 移動 1 次, k = 1, 剛好可以。
所以移動之後得到: num = 1342
, k = 1 - 1 = 0
;
因爲 k = 0
不能移動了, 所以我們直接返回 1342
。
遞歸解法(超出時間限制)
所以代碼我們可以用 遞歸 直接這樣寫, 但是在第48個測試用例的時候會提示 超出時間限制。
我們來分析下時間複雜度,
num.indexOf(c)
是 O(N)
, subString
也是 O(N)
,
一共執行了 N 次, 所以時間複雜度是 O(N^2)
.
//代碼來自評論區
public String minInteger(String num, int k) {
if (k == 0) return num;
for (char c = '0'; c <= '9'; c++) {
int i = num.indexOf(c);
if (i >= 0) {
if (i <= k) {
return c + minInteger(num.substring(0, i) + num.substring(i + 1), k - i);
}
}
}
return num;
}
優化遞歸
對於遞歸方法的 indexOf
和 substring
我們可以怎麼優化呢?
因爲我們每次都要從 0 到 9 去獲取其在 num
對應的位置, 所以我們可以先記錄他們的位置, 可以通過下面的代碼一次遍歷就能獲取到 0 - 9 在 num中的所有位置。可以把每次查找位置的時間複雜度從 O(N)
降到 O(1)
.
LinkedList<Integer>[] list = new LinkedList[10];
for (int i = 0; i < 10; i++) {
list[i] = new LinkedList<>();
}
int len = num.length();
char[] arr = num.toCharArray();
for (int i = 0; i < len; i++) {
list[arr[i] - '0'].add(i);
}
這樣我們保存的是 0 - 9 在 num中的原始位置。
我們再來看個示例 num = 4132
, k = 4
.
把 4
移動到最前面是 0 次, 1
是1次,3
是 2次,2
是 3次
當我們移動 1
之後 得到 num = 1432
, k = 3
.
此時 1
是已經確定的相當於只是處理 432
, 此時我們和開始的 4132
相比把每個數 移動到最前面的次數變成了:
把 4
移動到最前面是 0 次, 3
是 1次,2
是 2次.
我們發現, 每一移動完一個字符,他後面的字符最前面的次數就會 減1。
這樣我們就可以記錄每次移動的字符後面字符讓後面的字符的移動次數減一。
對於substring
我們可以記錄那些字符已經用過了, 這樣就可以直接在原字符上操作了,不需要利用substring
了。
代碼如下, 這樣能通過所有的測試用例了,但是 執行用時:1131 ms, 還是比較慢的, 我們還可以繼續優化。
// O(N^2)time
// O(N)space
public String minInteger(String num, int k) {
//記錄0 - 9 在 num中的位置
LinkedList<Integer>[] list = new LinkedList[10];
for (int i = 0; i < 10; i++) {
list[i] = new LinkedList<>();
}
int len = num.length();
char[] arr = num.toCharArray();
for (int i = 0; i < len; i++) {
list[arr[i] - '0'].add(i);
}
//記錄結果, 添加已經移動過的字符
StringBuilder res = new StringBuilder();
//記錄 當前位置 前面由多少個字符已經移動過
int[] offset = new int[len];
outer:
// k > 0 說明我們 可以移動, res.length() < len 說明還有字符未被移動
while (k > 0 && res.length() < len) {
for (int i = 0; i < 10; i++) {
if (list[i].isEmpty()){
//num中沒有這個字符
continue;
}
//獲取字符的下標 減去 前面已經移動過的字符 得到 它移動到最前面需要的次數
int move = list[i].getFirst() - offset[list[i].getFirst()];
if (move > k) {
//比 K 大,則找下一個數字
continue ;
}
//更新 k的值
k -= move;
//獲取這個字符的首個位置, 並把它從保存位置的鏈表中移除,因爲我們已經用過了,不能再用
int index = list[i].removeFirst();
//添加到結果中
res.append(arr[index]);
//修改num中這個位置爲 字符 0, 表示我們已經用過了。
arr[index] = 0;
//將 index 後面的字符的需要減去的移動次數 + 1
for (int j = index + 1; j < len; j++) {
offset[j]++;
}
//繼續從 0 開始找 移動次數小於 k 的字符
continue outer;
}
}
//如果 k 比較小, 就會存在 還有字符未被移動, 我們按原順序依次添加
for (int i = 0; i < len; i++) {
if (arr[i] != 0) {
res.append(arr[i]);
}
}
return res.toString();
}
優化到 O(N*logN)
上面的代碼, 每次移動一個字符之後,需要對後面的所有字符記錄前面移動的字符 加 1。
那我們也可以直接保存 移動過字符的位置,找字符時,通過二分查找 已經移動過的位置記錄中 有多少個 比當前位置小。
而且我們字符最多移動 1 + 2 + ... + n-1 = (n - 1) * n / 2 次, 所以當 k >= (n -1) * n / 2 時, 我們可以直接對字符按升序排序。
代碼如下:執行用時:57 ms
//O(N * logN)time
//O(N)space
public String minInteger(String num, int k) {
//記錄0 - 9 在 num中的位置
LinkedList<Integer>[] list = new LinkedList[10];
for (int i = 0; i < 10; i++) {
list[i] = new LinkedList<>();
}
int len = num.length();
char[] arr = num.toCharArray();
for (int i = 0; i < len; i++) {
list[arr[i] - '0'].add(i);
}
if (k >= (len - 1) * len / 2) {
Arrays.sort(arr);
return new String(arr);
}
//記錄移動的字符位置
List<Integer> record = new ArrayList<>();
//記錄結果, 添加已經移動過的字符
StringBuilder res = new StringBuilder();
outer:
while (k > 0 && res.length() < len) {
for (int i = 0; i < 10; i++) {
if (list[i].isEmpty()) {
continue;
}
//找到移動的字符位置中有 多少個比當前位置小
int index = findIndex(record, list[i].getFirst());
int move = list[i].getFirst() - index;
if (move > k) {
continue;
}
//更新 k的值
k -= move;
//獲取這個字符的首個位置, 並把它從保存位置的鏈表中移除,因爲我們已經用過了,不能再用
int pos = list[i].removeFirst();
//把當前 位置 添加到 已經移動過的位置列表中
record.add(index, pos);
res.append(i);
arr[pos] = 0;
continue outer;
}
}
for (int i = 0; i < len; i++) {
if (arr[i] != 0) {
res.append(arr[i]);
}
}
return res.toString();
}
/**
* 二分查找比 value小的個數
*/
int findIndex(List<Integer> list, int value) {
int l = 0, r = list.size();
while (l < r) {
int mid = (l + r) >> 1;
if (list.get(mid) < value) {
l = mid + 1;
} else {
r = mid;
}
}
return l;
}
以上,如果有描述錯誤的,請路過的大佬指出來。