Next Permutation
给你一个整数数组 nums,其中的值可能重复。设想把这些值的所有不同排列按字典序排序:从第一个不同的索引开始比较两个排列,该位置值较小的排列排在前面。返回排序后紧接在 nums 之后的排列。
如果 nums 已经是最大的排列,那么它后面没有排列了:返回最小的排列,也就是按升序排列这些值。目标是用时 O(n),额外内存用量为 O(1)。
函数
- numsinteger-array
- 当前的值排列方式
- 返回值integer-array
- 字典序中的下一个排列;如果 nums 已经是最大的排列,则将这些值按升序排列
约束条件
1 ≤ nums.length ≤ 50000 ≤ nums[i] ≤ 1000
示例
- 输入
- nums = [2, 5, 4, 1]
- 输出
- [4, 1, 2, 5]
- 解释
- 尾部的
5, 4, 1正在下降,所以它前面的 2 必须增大。大于 2 的最小尾部值是 4;将它放到那里,其余数字按递增顺序排列:[4, 1, 2, 5]。
- 输入
- nums = [3, 2, 2, 1]
- 输出
- [1, 2, 2, 3]
- 解释
- 整个数组都在向下排列,所以它是由 3、2、2 和 1 组成的最大排列。答案会绕回最小的排列:
[1, 2, 2, 3]。
- 输入
- nums = [1, 5, 1]
- 输出
- [5, 1, 1]
- 解释
- 第一个 1 必须变大。最后一个 1 与它相等,并没有更大,因此只有 5 可以代替它,结果是
[5, 1, 1]。
进阶
如果要找前一个排列,也就是字典序中紧挨在 nums 前面的那个排列,你会怎么做?
提示
逐个打开提示,每个都会多透露一点。
要让数组变得更大一些,就尽可能把它改得更靠右。什么时候无法仅通过重新排列数组后缀中的值来让它变得更大?
从右侧开始向左遍历,只要数值不断增大就继续。当某个值小于它后面的值时,它就是枢轴:这是还能增大的最右侧位置。它后面的所有值都在递减。
将枢轴与它右侧严格大于它的最右边的值交换。枢轴后面的部分仍然是降序的,因此将它反转,使其尽可能小。如果没有枢轴,就反转整个数组。
题解
一个降序后缀已经是其元素所允许的最大排列,因此下一个排列会改变它前面的值,也就是枢轴。将枢轴增大到尽可能小的值,然后让后缀尽可能小。这需要从右向左扫描两次,并在数组内部反转后缀。
按顺序排列列表
结果正确,但无法在最大的测试中运行完毕
思路
从排好序的值开始,用深度优先的方式构建排列,每次都先尝试最小的未使用值。这样会按字典序生成排列,因此第一个严格大于 nums 的排列就是答案;如果没有,就返回排好序的值。重复值会让一些排列被构建两次,例如 [1, 1, 5] 会生成 115、151、115、151、511、511,但重复生成的排列只是重复了之前构建过的排列,因此第一个更大的排列仍然是下一个排列。
排列数量会按 n! 增长:12 个不同的值有 479,001,600 种排列。
算法
- 将
nums的副本排序后放入pool。 - 按深度优先的方式构建排列,从左到右选择
pool中尚未使用的元素。 - 排列完成后,逐个位置将它与
nums进行比较。 - 返回第一个大于
nums的排列。 - 如果没有更大的排列,就返回
pool。
def nextPermutation(nums):
n = len(nums)
pool = sorted(nums) # taking the smallest unused value first gives sorted order
used = [False] * n
current = []
def build():
# True once current holds the first arrangement greater than nums
if len(current) == n:
return current > nums # Python compares lists in dictionary order
for i in range(n):
if not used[i]:
used[i] = True
current.append(pool[i])
if build():
return True
current.pop()
used[i] = False
return False
if build():
return current
return pool # nothing is greater: nums was the largest arrangement找到基准值,交换位置,反转后缀
思路
支点是从右往左找到的第一个比右侧相邻值小的值,也就是 [2, 5, 4, 1] 中的 2。它是可以增大的最右位置,而尽可能增大最右侧的位置,可以让增幅最小。
将支点与后缀中严格大于它的最小值交换。后缀是递减的,因此这个值就是从右往左第一个大于支点的值:4,交换后得到 [4, 5, 2, 1]。后缀仍然是递减的,因为原支点可以放在 4 原来的位置。现在前面的值变大了,后缀就应该尽可能小,并按递增顺序排列;反转递减序列即可做到这一点:[4, 1, 2, 5]。
如果没有支点,数组就是最大的排列,反转整个数组即可得到最小的排列。每一步都是一次扫描或一次反转:时间复杂度为 O(n),内存复杂度为 O(1),不过 R 版本会返回一个 O(n) 的副本。
算法
- 将
i设为n-2,当nums[i] ≥ nums[i + 1]时向左移动它。 - 如果
i ≥ 0,将j设为n-1,当nums[j] ≤ nums[i]时向左移动它,然后交换nums[i]和nums[j]。 - 用两个索引向彼此移动,将从
i + 1到末尾的部分反转。 - 返回
nums。
def nextPermutation(nums):
n = len(nums)
# 1. the pivot is the last index whose value is smaller than the next one
i = n - 2
while i >= 0 and nums[i] >= nums[i + 1]:
i -= 1
if i >= 0:
# 2. swap it with the rightmost value that is larger than it
j = n - 1
while nums[j] <= nums[i]:
j -= 1
nums[i], nums[j] = nums[j], nums[i]
# 3. the suffix after the pivot is falling: reverse it to make it rising
left, right = i + 1, n - 1
while left < right:
nums[left], nums[right] = nums[right], nums[left]
left += 1
right -= 1
return nums
易错点和边界情况
- 在寻找枢轴时使用
>而不是≥。在[3, 2, 2, 1]中,2, 2这一对并不是上升序列,停在那里会选出一个无法增大的枢轴。 - 与等于枢轴的值交换。在
[1, 5, 1]中,交换两个 1 不会有任何变化;要寻找严格大于枢轴的值。 - 与后缀中最大的值交换。在
[2, 5, 4, 1]中,这样会把 5 放到前面,跳过所有以 4 开头的排列。 - 遗漏没有枢轴的情况。最大的排列必须通过反转整个数组,变回排序后的数组。
- 在 Rust 中,从
n-2开始的usize索引会在n = 1时触发 panic;如果没有枢轴,向左越过 0 也会触发 panic。请使用有符号索引,或从 1 开始计数。
常见问题3
下一个排列的时间复杂度是多少?
O(n)。从右侧扫描一次即可找到枢轴,再扫描一次找到要交换的值,最后反转一次即可修正后缀。额外内存为 O(1),因为所有操作都在数组内部完成。
为什么要反转后缀,而不是对它排序?
交换后,后缀仍然是降序的,因此将它反转就能使其按升序排列。反转需要 O(n),排序则需要 O(n log n),但结果相同。
下一个排列适用于重复值吗?
是的,只要两次比较都把相等的值视为不更大。查找枢轴时,遇到相等的相邻元素会用 ≥ 跳过;查找交换位置时,遇到等于枢轴的值会用 ≤ 跳过。这样,[1, 5, 1] 就会变成 [5, 1, 1],也就是下一个不同的排列。
相似题目
运用相同思路的题目。做两三道,才能真正掌握一种模式。
def nextPermutation(nums):
# 在这里编写代码测试 #1
测试 #2
测试 #3
参数
nums = [2, 5, 4, 1]
预期输出
[4, 1, 2, 5]