邻位交换的最小次数 图解题解
这道题到底在问什么
- 输入
- num="5489355142", k=4
- 输出
- 2
- 输入
- num="11112", k=4
- 输出
- 4
- 输入
- num="1234", k=3
- 输出
- 2
最优解:为什么这么做
一句话答案:LeetCode 1850 邻位交换的最小次数:先对 num 连做 k 次「下一个排列」求出第 k 个最小妙数,再数把 num 逐位挪成它的相邻交换次数(正好等于逆序对数),时间 O(k·n+n²)、空间 O(n)。
妙数是什么,这道题的两步分别求什么
给一个用字符串表示的大整数 num 和整数 k。把 num 各位数字重排,只要排出的数值比 num 大,就算一个「妙数」;妙数很多,只关心最小的几个。分两步:先找第 k 个最小妙数当目标串,再算把 num 变成它、每次只能换相邻两位、最少换几次。题面 num 为 1234、k 取 3 时,第 3 个最小妙数是 1342,答案 2 次。
把全部排列列出来挑第 k 个,为什么走不通
n 位数字全排列有 n! 种,全摊开挑比 num 大的、排序取第 k 个,n 才到 10 就三百多万种,二十位是天文数字,列不完。既然只要紧挨 num 往上的第 k 个,就该能从 num 一步步跳到下一个更大排列,不必铺开全部。
下一个排列怎么求,交换次数又凭什么等于逆序对
第一步靠「下一个排列」:求出比当前大、又是更大排列里最小的那个,即字典序上紧挨着的后一个。三步——从右往左找第一个比右邻小的位置叫拐点;再从右往左找第一个比拐点大的位置,两者交换;把拐点后的后缀反转成升序。从 num 连做 k 次,依次得第 1 到第 k 个最小妙数。
第二步只能换相邻两位。把目标每一位来自原串的哪个下标排成一串——逆序对,指这串里前面的数比后面的大的一对。一次相邻交换只调换相邻两个、至多消掉一个逆序对,所以最少次数正好等于这串下标的逆序对数。数字有重复时,相同数字要按原串里的先后去对应,才不会多造逆序对。
1234 连做三次下一个排列,怎样逼出 1342
从 1234 求第 1 个:拐点是下标 2 的 3,从右找第一个比 3 大的即下标 3 的 4,交换得 1243,后缀一位反转不变。在 1243 求第 2 个:拐点退到下标 1 的 2,和下标 3 的 3 交换得 1342,后缀 42 是降序,反转成 24 才得 1324——这步别省,不反转就跳过 1324。在 1324 求第 3 个:拐点是下标 2 的 2,和下标 3 的 4 交换得 1342,后缀不变即目标。
把 1234 挪成 1342,两次交换是怎么数出来的
目标 1342 的四位分别来自原串 1234 的下标 0、2、3、1。逐位对齐:第 0 位要 1,下标 0 已是 1;第 1 位要 3,3 在下标 2,换到下标 1,串变 1324,计 1 次;第 2 位要 4,4 在下标 3,换到下标 2,串变 1342,计 2 次;第 3 位要 2 已就位,共 2 次。换个角度数下标串 0、2、3、1 的逆序对:2 比 1、3 比 1 各一对,也是 2。
后缀忘反转、重复数字乱配,答案就跟着错
复杂度上,第一步 k 次下一个排列各扫一遍,是 O(k·n);第二步双重循环数逆序对,是 O(n²),合计 O(k·n+n²)。逆序对换树状数组可降到 O(n log n)。额外只有下标桶和位置数组,空间 O(n)。
下一个排列最阴的是反转后缀:交换完拐点直接用,会跳过更小的妙数、第 k 个整串错位。有重复也藏坑——像 11112 求第 4 个得 21111、答案 4,那几个 1 得按原串先后对应,乱配就多造逆序对。还有别混:数的是相邻交换、等于逆序对数,任意位置能换几步就排好,那是另一道题。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3两步套路:先用「下一个排列」做 k 次求出第 k 个最小妙数(找拐点 i、找 j 交换、反转后缀);再用冒泡把 num 逐位对齐到目标、累加相邻交换次数(等于逆序对数)。下面每帧都在套它。
- 4先看清画面。上面这排格子是 num 等于 1234,四位数字 1、2、3、4,下标 0 到 3。任务分两步:第一步反复求「下一个排列」3 次,拿到第 3 个最小妙数当目标;第二步再数从 1234 挪到那个目标,最少要几次相邻交换。右边面板先记第一步:已经求到第几个妙数、当前排列长什么样、这一步在做什么。
- 5求第 1 个最小妙数。从最右边往左看,找第一个「比它右邻小」的位置,这就是拐点。下标 3 是 4,它右边没有邻居;看下标 2 的 3,它右边是 4,3 小于 4,拐点就是它,i 等于 2,绿色标住。拐点意味着从这里开始往大调,能得到更大的排列。
- 6找到拐点后,再从最右边往左找第一个「大于拐点数字 3」的位置,把它拿来和拐点换。下标 3 是 4,4 大于 3,正好,j 等于 3,蓝色标住。为什么找刚好大于它的?这样换过去,增幅最小,才是紧挨着的下一个更大排列。
- 7把拐点 i 等于 2 的 3,和 j 等于 3 的 4 交换。1234 就变成了 1243。现在前面变大了一点,但拐点后面那一段还得处理:交换完拐点后面可能不是最小顺序,要再收拾一下。
- 8最后把拐点后面那一段整体反转,让后缀尽量小。这次拐点在下标 2,后缀只有下标 3 一位,反转以后还是它自己,所以结果就是 1243。这就是第 1 个最小妙数。右边面板记上:已求到第 1 个。接着求第 2 个。
- 9在 1243 上求下一个排列。还是从右往左找拐点。下标 2 是 4,它右邻是 3,4 大于 3,不是拐点,标红跳过;再看下标 1 的 2,右邻是 4,2 小于 4,拐点就是它,i 等于 1,绿色标住。
- 10再从最右往左找第一个大于拐点数字 2 的位置。下标 3 是 3,3 大于 2,就是它,j 等于 3,蓝色标住。注意后缀 43 是递减的,从右边第一个大于 2 的必然是恰好比 2 大一点的那个,换过去增幅最小。
- 11把拐点下标 1 的 2 和下标 3 的 3 交换,1243 变成 1342。这时拐点后面是下标 2 和 3 的 4、2,还是从大到小排的,不是最小,得反转一下才是真正的下一个排列。
- 12把拐点后面下标 2 到 3 的 42 整体反转成 24,1342 就变成了 1324。这一步很关键:反转前是 1342,直接用会跳过好几个更小的妙数;反转后的 1324 才是紧挨着 1243 的下一个。已求到第 2 个,再求第 3 个。
- 13在 1324 上求最后一次下一排列。从右往左找拐点:下标 2 是 2,右邻下标 3 是 4,2 小于 4,拐点立刻就是它,i 等于 2,绿色标住。这次拐点靠得很右,后面处理会很轻。
- 14从最右往左找第一个大于拐点数字 2 的位置。下标 3 是 4,4 大于 2,就是它,j 等于 3,蓝色标住。拐点后面只有这一位,待会儿交换完基本就成了。
- 15把拐点下标 2 的 2 和下标 3 的 4 交换,1324 变成 1342。拐点后面只剩一位,反转它还是自己,所以直接就是结果。第 3 个最小妙数就是 1342,这正是我们要够到的目标串。第一步大功告成。
- 16第二步换个目标记法。上面重新摆回原串 1234,右边面板改记第二步:目标串是 1342,已经对齐的前缀、以及累计交换了几次。规则是每次只能交换相邻两位。我们从左到右,一位一位把当前串对齐成目标,顺手把交换次数加起来。
- 17看目标第 0 位,要的是 1。当前串下标 0 正好就是 1,不用动,零次交换。绿色标住下标 0,它已经对齐好了。前缀「1」锁定,往后这一位不再碰。看第 1 位。
- 18目标第 1 位要的是 3。在当前串 1234 里,3 待在下标 2,可它该去下标 1。绿色标住这个 3,红色标下标 1,那是它的目的地。相邻交换只能一步步挪,所以要把 3 和它左边的 2 交换,往左顶一格。
- 19交换下标 1 和 2 的 2 和 3,当前串从 1234 变成 1324。3 顺利落到下标 1,和目标对上了。交换次数从 0 加到 1。绿色标住就位的 3。现在前缀「13」都对齐了,看第 2 位。
- 20目标第 2 位要的是 4。当前串 1324 里,4 待在下标 3,它该去下标 2。绿色标住这个 4,红色标下标 2 是目的地。同样用相邻交换,把 4 和它左边的 2 换一下,往左顶一格。
- 21交换下标 2 和 3 的 2 和 4,当前串从 1324 变成 1342。4 落到下标 2,对上目标。交换次数从 1 加到 2。绿色标住就位的 4。此时前缀「134」都对齐了,只剩最后一位。
- 22看目标最后一位,要的是 2。当前串 1342 的下标 3 已经是 2,自动就位,不用再换。绿色标住它。到这里当前串已经完全等于目标串 1342,交换次数停在 2。为什么最后一位总能自动对上?前三位都对齐了,剩下的那位没得选,一定也对。
- 23收尾。当前串已经完全变成目标 1342,一路数下来:对齐 3 用了 1 次交换,对齐 4 又用了 1 次,一共 2 次。这就是把 1234 变成第 3 个最小妙数所需的最少相邻交换次数。整个过程分两步:先用下一个排列求出目标,再用冒泡对齐数出代价。
- 24再从另一个角度复核一遍。目标串 1342 的每一位,分别来自原串的下标 0、2、3、1。把这串下标 0、2、3、1 看成一个排列,数它有几对「前面的比后面的大」的逆序对:2 比 1 大是一对,3 比 1 大又是一对,一共 2 对。逆序对数恰好等于相邻交换的最小次数,这也是参考代码直接数逆序对的道理。
⚠️ 容易写错的地方
✗ 错:下一个排列交换完忘了反转后缀
✓ 对:交换拐点与 j 之后,必须把拐点后面的后缀整体反转成最小
像 1243 求下一个,交换后是 1342,后缀 42 还是从大到小排的。不反转就跳过了 1324 这个更小的妙数,直接用 1342 会让第 k 个数错位、答案跟着错
✗ 错:有重复数字时,位置桶不按先后顺序取
✓ 对:每个数字的下标按原串出现顺序放进桶,取的时候从桶头依次往后取
像 11112 这种有重复的,几个 1 必须按原来的先后一一对应到目标里的 1,乱配会多算或少算逆序对。桶加下标顺序取,才能保证是最少交换的那种配法
✗ 错:把逆序对数和普通排序交换数混为一谈
✓ 对:数的是「相邻交换」次数,正好等于逆序对数,不是任意交换的次数
任意位置都能换的话,几次就能排好,那是另一个量。本题限定只能换相邻两位,每消掉一个逆序对刚好花一次相邻交换,所以答案就是逆序对数,别套错模型
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def getMinSwaps(self, num: str, k: int) -> int:
def next_permutation(nums: List[str]) -> bool:
n = len(nums)
i = n - 2
while i >= 0 and nums[i] >= nums[i + 1]:
i -= 1
if i < 0:
return False
j = n - 1
while j >= 0 and nums[j] <= nums[i]:
j -= 1
nums[i], nums[j] = nums[j], nums[i]
nums[i + 1 : n] = nums[i + 1 : n][::-1]
return True
s = list(num)
for _ in range(k):
next_permutation(s)
d = [[] for _ in range(10)]
idx = [0] * 10
n = len(s)
for i, c in enumerate(num):
j = ord(c) - ord("0")
d[j].append(i)
arr = [0] * n
for i, c in enumerate(s):
j = ord(c) - ord("0")
arr[i] = d[j][idx[j]]
idx[j] += 1
return sum(arr[j] > arr[i] for i in range(n) for j in range(i))C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
int getMinSwaps(string num, int k) {
string s = num;
for (int i = 0; i < k; ++i) {
next_permutation(begin(s), end(s));
}
vector<int> d[10];
int n = num.size();
for (int i = 0; i < n; ++i) {
d[num[i] - '0'].push_back(i);
}
int idx[10]{};
vector<int> arr(n);
for (int i = 0; i < n; ++i) {
arr[i] = d[s[i] - '0'][idx[s[i] - '0']++];
}
int ans = 0;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < i; ++j) {
if (arr[j] > arr[i]) {
++ans;
}
}
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int getMinSwaps(String num, int k) {
char[] s = num.toCharArray();
for (int i = 0; i < k; ++i) {
nextPermutation(s);
}
List<Integer>[] d = new List[10];
Arrays.setAll(d, i -> new ArrayList<>());
int n = s.length;
for (int i = 0; i < n; ++i) {
d[num.charAt(i) - '0'].add(i);
}
int[] idx = new int[10];
int[] arr = new int[n];
for (int i = 0; i < n; ++i) {
arr[i] = d[s[i] - '0'].get(idx[s[i] - '0']++);
}
int ans = 0;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < i; ++j) {
if (arr[j] > arr[i]) {
++ans;
}
}
}
return ans;
}
private boolean nextPermutation(char[] nums) {
int n = nums.length;
int i = n - 2;
while (i >= 0 && nums[i] >= nums[i + 1]) {
--i;
}
if (i < 0) {
return false;
}
int j = n - 1;
while (j >= 0 && nums[i] >= nums[j]) {
--j;
}
swap(nums, i++, j);
for (j = n - 1; i < j; ++i, --j) {
swap(nums, i, j);
}
return true;
}
private void swap(char[] nums, int i, int j) {
char t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
}复杂度
时间
O(k·n + n²)
n 是数字位数,k 是题目给的次数。第一步做 k 次下一个排列,每次最多扫一遍串,是 O(k·n);第二步数逆序对用的是双重循环,两两比较,是 O(n²)。两段相加就是 O(k·n + n²)。数逆序对若换成树状数组可以降到 O(n log n),但参考解用的是直观的双重循环
空间
O(n)
按峰值算。额外开了 10 个桶合起来存 n 个下标、一个长度为 n 的位置数组、还有目标字符数组,都是和 n 同阶的,没有更大的结构。所以额外空间是线性的 O(n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 邻位交换的最小次数 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么连做 k 次下一个排列,就能拿到第 k 个最小妙数?+
下一个排列每次返回的,都是「严格比当前大、又是所有更大排列里最小」的那个,也就是字典序上紧挨着的后一个。对同样长度的数字串来说,字典序大小就是数值大小,而妙数正是数值比 num 大的排列。所以从 num 出发,第 1 次得到第 1 个最小妙数,第 2 次得第 2 个,一路到第 k 次正好是第 k 个,不重不漏。
最少相邻交换次数,为什么恰好是逆序对数?+
把目标每位来自原串的下标排成一串,理成完全递增就等于对齐完成。一次相邻交换只调换这串里相邻的两个,至多消掉一个逆序对,绝不会一次消两个;而排成递增必须把所有逆序对都消干净。于是逆序对数是次数的下界,冒泡又能不多不少用这么多次达到,上下一夹,最少次数就是逆序对数。
数字串里有重复数字,怎么保证配对不出错?+
诀窍是位置桶按出现顺序取。先把每个数字在原串里的下标,按从左到右压进对应的桶;拼下标序列时,目标里每遇到一个数字,就从它那个桶的头部取下一个下标。这样相同数字之间保持原有先后,不会人为制造多余的逆序对,数出来的次数才是真正最少的。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 邻位交换的最小次数 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。