题目是LeetCode第196场周赛的第四题,链接:1505. 最多 K 次交换相邻数位后得到的最小整数。具体描述:给你一个字符串num
和一个整数k
。其中,num
表示一个很大的整数,字符串中的每个字符依次对应整数上的各个数位。你可以交换这个整数相邻数位的数字最多k
次。请你返回你能得到的最小整数,并以字符串形式返回。
示例1:
输入:num = "4321", k = 4
输出:"1342"
解释:4321 通过 4 次交换相邻数位得到最小整数的步骤:4321-->3412-->3142-->1342。
示例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"
这道题是一道贪心算法的题目,为了使数变小,我们需要贪心地找到较小的数给放到前面的位置上去。首先给出一种简单的暴力法,就是我们总是在未处理字符串上按照从0
到9
的顺序寻找数字的位置,只要发现找到出现了某个数字i
而且其相对于未处理字符串头部的位置idx
不超过剩余可交换次数k
,那么就把这个数给移动到字符串头部来,然后剩下的字符串(减去被交换到头部的这个数字字符)继续递归处理,同时剩余可交换次数也减小idx
次。时间复杂度为
O
(
n
2
)
O(n^{2})
O(n2),空间复杂度为
O
(
n
)
O(n)
O(n)。
JAVA版代码如下:
class Solution {
public String minInteger(String num, int k) {
if (k == 0 || num == null) {
return num;
}
for (int i = 0; i <= 9; ++i) {
int idx = num.indexOf(String.valueOf(i));
if (idx >= 0 && idx <= k) {
return String.valueOf(i) + minInteger(num.substring(0, idx) + num.substring(idx + 1, num.length()), k - idx);
}
}
return num;
}
}
JAVA版直接超时了,Python的不会,看下面的代码。
Python版代码如下:
class Solution:
def minInteger(self, num: str, k: int) -> str:
if k == 0 or not num:
return num
for i in range(10):
idx = num.find(str(i))
if 0 <= idx <= k:
return num[idx] + self.minInteger(num[0:idx] + num[idx + 1:], k - idx)
提交结果如下:

为了进一步减低复杂度,考虑每次我们在寻找一个比当前数字小的数字时需要计算从这个数字到当前数字的交换次数,一般这里需要 O ( n ) O(n) O(n),因为需要遍历这个数到当前数之间,看有多少个数还未被交换过,这个数量就是所需要的交换次数,而这正是一种区间查询的操作,所以可以用线段树来达到 O ( l o g n ) O(logn) O(logn)的复杂度,从而将总体时间复杂度降为 O ( n l o g n ) O(nlogn) O(nlogn),空间复杂度还是 O ( n ) O(n) O(n)。
JAVA版代码如下:
class Solution {
public String minInteger(String num, int k) {
char[] nums = num.toCharArray();
int n = nums.length;
SegmentTree segmentTree = new SegmentTree(n);
LinkedList[] position = new LinkedList[10];
for (int i = 0; i < 10; ++i) {
position[i] = new LinkedList<Integer>();
}
for (int i = n - 1; i >= 0; --i) {
position[nums[i] - '0'].addLast(i);
}
int i = 0;
StringBuilder sb = new StringBuilder();
while (i < n) {
int digit = nums[i] - '0';
if (position[digit].size() == 0 || (int)(position[digit].getLast()) > i) {
++i;
continue;
}
for (int j = 0; j <= digit; ++j) {
if (position[j].size() == 0) {
continue;
}
int originIdx = (int)position[j].getLast();
int count = originIdx - segmentTree.getSum(originIdx + 1);
if (count <= k) {
k -= count;
sb.append(nums[originIdx]);
position[j].removeLast();
segmentTree.update(originIdx + 1, 1);
break;
}
}
}
return sb.toString();
}
}
class SegmentTree {
int n;
int[] tree;
public SegmentTree(int val) {
n = val;
tree = new int[val + 1];
}
public int lowbit(int x) {
return x & (-x);
}
public void update(int idx, int x) {
while (idx <= n) {
tree[idx] += x;
idx += lowbit(idx);
}
}
public int getSum(int idx) {
int res = 0;
while (idx > 0) {
res += tree[idx];
idx -= lowbit(idx);
}
return res;
}
}
提交结果如下:

Python版代码如下:
class Solution:
def minInteger(self, num: str, k: int) -> str:
n = len(num)
# 线段树上0代表未被换到前面去,1代表被换了
# 所以getSum得到的就是当前位置前面被换过了的数量
# 等价于未处理字符串的开头位置
segmentTree = SegmentTree(n)
# 记录0~9各个数字的位置
position = [[] for _ in range(10)]
for i in range(n - 1, -1, -1):
position[int(num[i])].append(i)
res = []
i = 0
while i < n:
digit = int(num[i])
if not position[digit] or position[digit][-1] > i:
# 已经被交换过的了,忽略掉
i += 1
continue
for j in range(digit + 1):
if not position[j]:
continue
# 在num中的索引
originIdx = position[j][-1]
# 找到一个比num[i]小的了,但还需要看距离是否够
count = segmentTree.getSum(originIdx + 1)
# 实际需要多少次的交换
needCount = originIdx - count
if needCount <= k:
k -= needCount
position[j].pop()
res.append(str(j))
segmentTree.update(originIdx + 1, 1)
break
return ''.join(res)
class SegmentTree:
def __init__(self, n):
self.n = n
self.tree = [0] * (n + 1)
def lowbit(self, x):
return x & (-x)
def update(self, i, x):
while i <= self.n:
self.tree[i] += x
i += self.lowbit(i)
def getSum(self, i):
res = 0
while i > 0:
res += self.tree[i]
i -= self.lowbit(i)
return res
提交结果如下:
