Hard
题目描述
给你一个字符串 num,表示一个很大整数的数字,和一个整数 k。你最多可以交换这个整数中任意两个相邻的数字 k 次。
请你返回你能得到的最小整数,并且以字符串形式返回。
示例 1:
输入:num = "4321", k = 4
输出:"1342"
解释:通过最多 4 次交换相邻数字的步骤如图所示,我们可以得到最小整数。
示例 2:
输入:num = "100", k = 1
输出:"010"
解释:输出可以包含前导零,但输入保证不含前导零。
示例 3:
输入:num = "36789", k = 1000
输出:"36789"
解释:我们可以保持原样,不进行任何交换。
约束条件:
1 <= num.length <= 3 * 10^4num只包含数字且不含前导零1 <= k <= 10^9
解题思路
这道题的核心思路是贪心算法:我们希望让较小的数字尽可能出现在高位。
基本思路:
- 从左到右遍历每一个位置,对于当前位置,我们希望在剩余交换次数允许的范围内,找到能够移动到当前位置的最小数字。
- 对于位置
i,我们可以在位置[i, i+k]范围内寻找最小的数字,因为最多用k次交换可以将该范围内的任何数字移动到位置i。 - 找到最小数字后,将其通过相邻交换移动到当前位置,更新剩余交换次数。
优化思路: 由于我们需要频繁地查找范围内的最小值,并且需要删除元素,可以使用以下数据结构:
- 方法1(推荐):使用队列存储每个数字的位置索引,贪心地从小到大处理
- 方法2:使用线段树或树状数组来维护区间查询和单点更新
具体算法流程:
- 为每个数字(0-9)维护一个队列,存储该数字在原字符串中的位置
- 对于结果字符串的每一位,从数字0开始查找,看能否在剩余交换次数内将其移动到当前位置
- 选择第一个满足条件的最小数字,计算移动成本,更新交换次数
- 由于前面的数字被移动,后续数字的实际位置会发生变化,需要用树状数组或其他数据结构维护偏移量
代码实现
class Solution {
public:
string minInteger(string num, int k) {
int n = num.size();
vector<queue<int>> pos(10);
// 为每个数字建立位置队列
for (int i = 0; i < n; i++) {
pos[num[i] - '0'].push(i);
}
string result = "";
vector<bool> used(n, false);
for (int i = 0; i < n; i++) {
// 尝试从最小的数字开始
for (int digit = 0; digit <= 9; digit++) {
if (pos[digit].empty()) continue;
// 移除已使用的位置
while (!pos[digit].empty() && used[pos[digit].front()]) {
pos[digit].pop();
}
if (pos[digit].empty()) continue;
int idx = pos[digit].front();
// 计算将这个数字移动到位置i需要的交换次数
int swaps_needed = 0;
for (int j = idx - 1; j >= 0; j--) {
if (!used[j]) swaps_needed++;
}
if (swaps_needed <= k) {
result += (char)('0' + digit);
used[idx] = true;
k -= swaps_needed;
pos[digit].pop();
break;
}
}
}
return result;
}
};
class Solution:
def minInteger(self, num: str, k: int) -> str:
from collections import deque
n = len(num)
pos = [deque() for _ in range(10)]
# 为每个数字建立位置队列
for i in range(n):
pos[int(num[i])].append(i)
result = ""
used = [False] * n
for i in range(n):
# 尝试从最小的数字开始
for digit in range(10):
if not pos[digit]:
continue
# 移除已使用的位置
while pos[digit] and used[pos[digit][0]]:
pos[digit].popleft()
if not pos[digit]:
continue
idx = pos[digit][0]
# 计算将这个数字移动到当前位置需要的交换次数
swaps_needed = sum(1 for j in range(idx) if not used[j])
if swaps_needed <= k:
result += str(digit)
used[idx] = True
k -= swaps_needed
pos[digit].popleft()
break
return result
public class Solution {
public string MinInteger(string num, int k) {
int n = num.Length;
Queue<int>[] pos = new Queue<int>[10];
for (int i = 0; i < 10; i++) {
pos[i] = new Queue<int>();
}
// 为每个数字建立位置队列
for (int i = 0; i < n; i++) {
pos[num[i] - '0'].Enqueue(i);
}
StringBuilder result = new StringBuilder();
bool[] used = new bool[n];
for (int i = 0; i < n; i++) {
// 尝试从最小的数字开始
for (int digit = 0; digit <= 9; digit++) {
if (pos[digit].Count == 0) continue;
// 移除已使用的位置
while (pos[digit].Count > 0 && used[pos[digit].Peek()]) {
pos[digit].Dequeue();
}
if (pos[digit].Count == 0) continue;
int idx = pos[digit].Peek();
// 计算将这个数字移动到当前位置需要的交换次数
int swapsNeeded = 0;
for (int j = 0; j < idx; j++) {
if (!used[j]) swapsNeeded++;
}
if (swapsNeeded <= k) {
result.Append((char)('0' + digit));
used[idx] = true;
k -= swapsNeeded;
pos[digit].Dequeue();
break;
}
}
}
return result.ToString();
}
}
var minInteger = function(num, k) {
if (k === 0) return num;
const n = num.length;
const digits = num.split('');
let swaps = k;
for (let i = 0; i < n && swaps > 0; i++) {
let minIdx = i;
let minDigit = digits[i];
// Find the smallest digit within swaps range
for (let j = i + 1; j < n && j <= i + swaps; j++) {
if (digits[j] < minDigit) {
minDigit = digits[j];
minIdx = j;
}
}
// Move the smallest digit to position i
while (minIdx > i && swaps > 0) {
[digits[minIdx], digits[minIdx - 1]] = [digits[minIdx - 1], digits[minIdx]];
minIdx--;
swaps--;
}
}
return digits.join('');
};
复杂度分析
| 复杂度类型 | 复杂度分析 |
|---|---|
| 时间复杂度 | O(n²) - 对于每个位置需要遍历10个数字,计算交换次数需要O(n)时间 |
| 空间复杂度 | O(n) - 需要存储位置队列和使用标记数组 |