下一个排列

标签: 数组 双指针

难度: Medium

整数数组的一个 排列  就是将其所有成员以序列或线性顺序排列。

  • 例如,arr = [1,2,3] ,以下这些都可以视作 arr 的排列:[1,2,3][1,3,2][3,1,2][2,3,1]

整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。

  • 例如,arr = [1,2,3] 的下一个排列是 [1,3,2]
  • 类似地,arr = [2,3,1] 的下一个排列是 [3,1,2]
  • arr = [3,2,1] 的下一个排列是 [1,2,3] ,因为 [3,2,1] 不存在一个字典序更大的排列。

给你一个整数数组 nums ,找出 nums 的下一个排列。

必须 原地 修改,只允许使用额外常数空间。

示例 1:

输入:nums = [1,2,3]
输出:[1,3,2]

示例 2:

输入:nums = [3,2,1]
输出:[1,2,3]

示例 3:

输入:nums = [1,1,5]
输出:[1,5,1]

提示:

  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 100

Submission

运行时间: 40 ms

内存: 14.7 MB

class Solution:
    def nextPermutation(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        n = len(nums)
        if n < 2:
            return

        i = n - 2
        j = n - 1
        while i >= 0 and nums[i] >= nums[j]:
            i -= 1
            j -= 1

        if i >= 0:
            for k in range(n-1, j-1, -1):
                if nums[k] > nums[i]:
                    nums[k], nums[i] = nums[i], nums[k]
                    break

        nums[j:n] = nums[j:n][::-1]


Explain

这个题解采用了两遍扫描的方法来找出下一个排列:第一次从后往前扫描找到第一个较小的元素,第二次从后往前找到第一个比较小元素大的元素,交换它们,然后反转后面的序列。具体步骤如下: 1. 从数组末尾开始,找到第一个比前一个元素小的元素 nums[i],表明从 i 开始的子序列是降序的。 2. 在 i 后面的降序子序列中,从后向前找到第一个比 nums[i] 大的元素 nums[k]。 3. 交换 nums[i] 和 nums[k]。 4. 反转 i 后面的子序列,使其变为升序。 这样就得到了下一个字典序更大的排列。

时间复杂度: O(n)

空间复杂度: O(1)

class Solution:
    def nextPermutation(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        n = len(nums)
        if n < 2:
            return
        
        # 第一次从后往前扫描,找到较小的元素
        i = n - 2 
        j = n - 1
        while i >= 0 and nums[i] >= nums[j]:
            i -= 1
            j -= 1
        
        if i >= 0:
            # 第二次从后往前扫描,找到比较小元素大的元素
            for k in range(n-1, j-1, -1):
                if nums[k] > nums[i]:
                    # 交换两个元素
                    nums[k], nums[i] = nums[i], nums[k] 
                    break
        
        # 反转后面的子序列
        nums[j:n] = nums[j:n][::-1]

Explore

这一步的目的是找到当前排列中可以被改变来生成下一个排列的点。从数组末尾开始寻找,是因为我们需要找到尽可能靠右的、能产生更大排列的改变点,以确保这是'下一个'排列。当我们找到第一个比后一个元素小的数字nums[i]时,它就标志着从i位置开始到数组结束的部分是降序的。这意味着,从i位置开始的子序列已经是该子序列能达到的最大排列,要找到整个数组的下一个排列,我们需要调整的就是这个位置。

选择交换nums[i]和子序列中第一个比nums[i]大的元素nums[k]是为了确保得到的新排列尽可能小,且比当前排列大。通过这种交换,nums[i]位置被一个稍大的值替换,而这个值是从i位置之后可用的最小值中选择的,这保证了新排列在字典序上仅次于当前排列。

在交换nums[i]和nums[k]后,i位置之后的子序列仍然保持降序。这是因为在i之后的部分原来是降序的,交换两个元素并不会改变其他元素的顺序。反转i之后的子序列,将其变为升序,是为了得到从这一位置开始的最小可能排列。这样做保证了整体排列是所有大于原始排列的排列中最小的,即字典序的下一个排列。

如果数组nums是降序的,那么它已经是可能的最大排列。在这种情况下,第一次扫描将无法找到满足nums[i] < nums[j]的i,因为不存在这样的i。此时,整个数组将被反转变为升序,这是所有可能排列中字典序最小的排列。这样确保了算法能够从最大排列转到最小排列,完成一个循环。