主题
引入
整数
你可以轻松地答出:
怎么算的?拆解步骤:
- 从右到左,找第一个可以增大的数字(小于
的数字),即 。 - 把
加一,变成 。 - 把
右边的数都变成 。
本题要计算「下一个排列」,这类似于计算「下一个不含重复数字的整数」,计算框架应该和「下一个整数」是类似的,具体要怎么做呢?
例子
排列
从右到左,找第一个可以增大的数。什么是「可以增大的数」?
- 如果
保持不变,只重新排列 ,得到 。排列变小了,这不行。 - 如果
保持不变,只重新排列 ,由于 的右边都是小于 的数,所以重排后, 这个位置上的数必然变小,导致排列变小,这也不行。 - 如果
保持不变,只重新排列 ,这是可以的,因为 右边有比 大的数,重排后可以让 这个位置上的数变大,从而得到更大的排列。 - 由于要求的是下一个排列,所以
这个位置上的数只要「大一点点」就好了。找到 右边最小的大于 的数,也就是 ,放到 这个位置上。于是,下一个排列是 。 - 剩余的三个数是
。由于只看前两位 就已经比 大了,所以后三位填最小的排列就行,即按照 的顺序填,得到 。
回顾我们是怎么算的:
- 从右到左,找第一个可以增大的数
,也就是 右边有大于 的数。我们找到的数是 。由于 右边的数是递减的(证明见答疑),所以 右侧相邻数就是 右边最大的数。如果 右侧相邻数小于 ,那么 右边必然没有大于 的数。因此,这一步可以简化为,从右到左,找第一个小于右侧相邻数的数 。 - 找
右边最小的大于 的数。由于 右边的数是递减的,所以再遍历一遍,从右到左找第一个大于 的数,就是 右边最小的大于 的数。我们找到的数是 。然后把 放到 的位置上,把 放到右边的三个位置中。这一步可以简化为交换 和 。交换后得到 。注意交换后 变成 ,仍然是递减的(证明见答疑)。 - 把
右边的数从小到大排序。由于第二步交换后, 右边的数 是递减的,所以只需把 反转,就得到了答案 。
算法
把上述过程一般化,同时对比「下一个整数」的算法:
| 下一个排列 | 下一个整数 | 相同点 |
|---|---|---|
| 从右到左,找第一个小于右侧相邻数的数 | 从右到左,找第一个小于 | 都是找第一个小于右侧相邻数的数 |
| 找 | 把 | 增大这个数 |
| 反转 | 把 | 把右边的数变到最小 |
特别地,如果第一步没有找到这样的
答疑
问:第一步找到
答:反证法。假设
问:为什么第二步交换后,右边的序列仍然是递减的?
答:设交换前
问:如果
答:例如
python
class Solution:
def nextPermutation(self, nums: List[int]) -> None:
n = len(nums)
# 第一步:从右到左找到第一个小于 nums[i+1] 的数 nums[i]
i = n - 2
while i >= 0 and nums[i] >= nums[i + 1]:
i -= 1
# 如果找到了,进入第二步;否则跳过第二步,反转整个数组
if i >= 0:
# 第二步:从右到左找到 nums[i] 右边最小的大于 nums[i] 的数 nums[j]
j = n - 1
while nums[j] <= nums[i]:
j -= 1
# 交换 nums[i] 和 nums[j]
nums[i], nums[j] = nums[j], nums[i]
# 第三步:反转 nums[i+1:](如果上面跳过第二步,此时 i = -1)
# nums[i+1:] = nums[i+1:][::-1] 这样写也可以,但空间复杂度不是 O(1) 的
left, right = i + 1, n - 1
while left < right:
nums[left], nums[right] = nums[right], nums[left]
left += 1
right -= 1cpp
// C++ 版待补充cpp
class Solution {
public:
void nextPermutation(vector<int>& nums) {
int n = nums.size();
// 第一步:从右到左找到第一个小于 nums[i+1] 的数 nums[i]
int i = n - 2;
while (i >= 0 && nums[i] >= nums[i + 1]) {
i--;
}
// 如果找到了,进入第二步;否则跳过第二步,反转整个数组
if (i >= 0) {
// 第二步:从右到左找到 nums[i] 右边最小的大于 nums[i] 的数 nums[j]
int j = n - 1;
while (nums[j] <= nums[i]) {
j--;
}
// 交换 nums[i] 和 nums[j]
swap(nums[i], nums[j]);
}
// 第三步:反转 [i+1, n-1](如果上面跳过第二步,此时 i = -1)
reverse(nums.begin() + i + 1, nums.end());
}
};cpp
class Solution {
public:
void nextPermutation(vector<int>& nums) {
ranges::next_permutation(nums);
}
};复杂度分析
- 时间复杂度:
,其中 是 的长度。最坏情况下需要遍历整个 数组。 - 空间复杂度:
。
变形题
改成求「上一个排列」,怎么做?
欢迎在评论区分享你的思路/代码。
相关问题
- 给定
和 ,求 到 的所有排列中,字典序第 小的排列。见 60. 排列序列。 - 求
是字典序第几小的排列。见 3109. 查找排列的下标(会员题)。 - 给定
,如何生成下下个排列?如何生成下 个排列?提示:结合前两个问题。
分类题单
- 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
- 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
- 单调栈(基础/矩形面积/贡献法/最小字典序)
- 网格图(DFS/BFS/综合应用)
- 位运算(基础/性质/拆位/试填/恒等式/思维)
- 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
- 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
- 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
- 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
- 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
- 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
- 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)
欢迎关注 B站@灵茶山艾府