LeetCode 题目 31:下一个排列【python】

简介: LeetCode 题目 31:下一个排列【python】

作者介绍:10年大厂数据\经营分析经验,现任大厂数据部门负责人。

会一些的技术:数据分析、算法、SQL、大数据相关、python

欢迎加入社区:码上找工作

作者专栏每日更新:

LeetCode解锁1000题: 打怪升级之旅

python数据分析可视化:企业实战案例

备注说明:方便大家阅读,统一使用python,带必要注释,公众号 数据分析螺丝钉 一起打怪升级

问题描述

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

例如,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.单遍扫描

这个问题是典型的计算组合问题中的“下一个排列”配置,其目标是在字典序的规则下找到当前排列的下一个排列。核心思路可以总结为以下几步:

  1. 从右向左查找第一个升序的元素对:即找到第一个 nums[k] < nums[k+1] 的位置 k。这意味着从 k+1end 的序列是降序的。
  2. nums[k+1]nums[end] 中找到比 nums[k] 大的最小元素:这个元素与 nums[k] 交换。
  3. nums[k+1]nums[end] 排序为升序:实际上由于它们原来是降序的,所以只需将其翻转。

这种方法利用了字典序排列的性质,通过局部操作实现全局字典序的递增。

代码示例

def nextPermutation(nums):
    i = len(nums) - 2
    while i >= 0 and nums[i] >= nums[i + 1]:
        i -= 1
    if i >= 0:
        j = len(nums) - 1
        while nums[j] <= nums[i]:
            j -= 1
        nums[i], nums[j] = nums[j], nums[i]  # 交换
    
    # 翻转i之后的部分
    left, right = i + 1, len(nums) - 1
    while left < right:
        nums[left], nums[right] = nums[right], nums[left]
        left, right = left + 1, right - 1
 
# 测试代码
nums1 = [1,2,3]
nextPermutation(nums1)
print(nums1)  # Output: [1,3,2]
 
nums2 = [3,2,1]
nextPermutation(nums2)
print(nums2)  # Output: [1,2,3]
 
nums3 = [1,1,5]
nextPermutation(nums3)
print(nums3)  # Output: [1,5,1]

算法分析

  • 时间复杂度:O(n),其中 n 是列表的长度。算法中有两个主要的线性阶段:寻找 ij,以及反转子数组。
  • 空间复杂度:O(1),原地修改数组,不使用额外的存储空间。

2.全排列的递归生成

虽然此方法对于实际编程竞赛或面试来说效率可能不高,它提供了一种理解问题的框架,特别是在理解如何构造所有排列时。该方法依赖于递归生成所有排列,并在过程中检测到当前排列,然后返回下一个。

实现思路:

  1. 生成所有排列:使用递归函数按字典顺序生成数组的所有排列。
  2. 检测当前排列:在生成的过程中,当发现当前排列时,取下一次递归所生成的排列作为结果。
  3. 效率问题:这种方法将会生成数组的所有排列,直到找到当前排列的下一个,因此在最坏的情况下,其效率是非常低的(尤其是当数组大且接近最大排列时,如 [3, 2, 1])。

代码示例

def nextPermutation(nums):
    def generate_permutations(n, A):
        if n == 1:
            yield A[:]
        else:
            for i in range(n - 1):
                yield from generate_permutations(n - 1, A)
                j = 0 if n % 2 == 0 else i
                A[j], A[n - 1] = A[n - 1], A[j]
            yield from generate_permutations(n - 1, A)
 
    found = False
    prev = None
    for perm in generate_permutations(len(nums), nums[:]):
        if found:
            nums[:] = perm
            return
        if perm == nums:
            found = True
            prev = perm
    
    if not found or prev == nums:
        nums.sort()
 
# 测试代码
nums1 = [1,2,3]
nextPermutation(nums1)
print(nums1)  # Output: [1,3,2]
 
nums2 = [3,2,1]
nextPermutation(nums2)
print(nums2)  # Output: [1,2,3]
 
nums3 = [1,1,5]
nextPermutation(nums3)
print(nums3)  # Output: [1,5,1]

算法分析

  • 时间复杂度:O(N!),其中 N 是数组长度。递归生成排列的过程需要遍历所有可能的排列。
  • 空间复杂度:O(N),主要是递归栈空间的使用以及排列存储的空间。

3.直接模拟寻找和交换过程

这种方法仍然基于观察到的排列性质,即通过确定翻转的边界来模拟生成下一个排列的过程。

实现思路:

  1. 从后向前查找第一个递减元素:寻找第一个满足 nums[k] < nums[k+1]k,这意味着从 nums[k+1]nums[end] 必然是降序。
  2. 从后向前查找第一个大于 nums[k] 的元素:从序列的末尾向前查找,找到第一个大于 nums[k] 的元素 nums[l]
  3. 交换 nums[k]nums[l]:交换这两个元素后,nums[k+1]nums[end] 仍然保持降序。
  4. 翻转 nums[k+1]nums[end]:将这部分序列翻转,使其变为升序,这是因为需要找到比当前排列稍大的下一个排列。

代码示例

def nextPermutation(nums):
    n = len(nums)
    if n <= 1:
        return
    
    # Step 1: Find the largest index k such that nums[k] < nums[k + 1]
    k = n - 2
    while k >= 0 and nums[k] >= nums[k + 1]:
        k -= 1
    
    if k >= 0:  # A valid k found
        # Step 2: Find the largest index l greater than k such that nums[k] < nums[l]
        l = n - 1
        while nums[l] <= nums[k]:
            l -= 1
        # Step 3: Swap nums[k] and nums[l]
        nums[k], nums[l] = nums[l], nums[k]
    
    # Step 4: Reverse from k + 1 to end
    nums[k + 1:] = reversed(nums[k + 1:])
 
# Example usage
nums = [1, 2, 3]
nextPermutation(nums)
print(nums)  # Output: [1, 3, 2]
 
nums = [3, 2, 1]
nextPermutation(nums)
print(nums)  # Output: [1, 2, 3]
 
nums = [1, 1, 5]
nextPermutation(nums)
print(nums)  # Output: [1, 5, 1]

算法分析

  • 时间复杂度:O(N)。尽管在寻找 kl 的时候各自需要 O(N) 的时间,以及翻转需要 O(N/2),整体操作仍视为 O(N)。
  • 空间复杂度:O(1)。所有操作都是在原数组上进行的,不需要额外空间。

总结

  1. 单遍扫描法:通过从后向前扫描,找到逆序的断点,进行交换和翻转,直观高效,是标准解法。
  2. 递归全排列法:生成所有排列直到当前排列之后的排列,理解深入但效率低,不适合实际应用。
  3. 模拟寻找和交换过程:基于数学的直接寻找并模拟交换的过程,操作简单明了,易于理解和实现。

这三种方法从实用性和教学价值两个角度提供了不同的解决策略,其中单遍扫描法因其效率和简洁性最为推荐。


欢迎关注微信公众号 数据分析螺丝钉

相关文章
|
4天前
|
C语言
详解Leetcode中关于malloc模拟开辟二维数组问题,涉及二维数组的题目所给函数中的各个参数的解读
详解Leetcode中关于malloc模拟开辟二维数组问题,涉及二维数组的题目所给函数中的各个参数的解读
6 1
|
15天前
|
存储 算法 调度
力扣中级算法(Python)
力扣中级算法(Python)
|
15天前
|
算法 Python
力扣初级算法(Python)(二)
力扣初级算法(Python)(二)
|
16天前
|
算法
【经典LeetCode算法题目专栏分类】【第10期】排序问题、股票问题与TOP K问题:翻转对、买卖股票最佳时机、数组中第K个最大/最小元素
【经典LeetCode算法题目专栏分类】【第10期】排序问题、股票问题与TOP K问题:翻转对、买卖股票最佳时机、数组中第K个最大/最小元素
|
16天前
|
容器
【LeetCode刷题】栈和队列题目练习~
【LeetCode刷题】栈和队列题目练习~
【42页动态规划学习笔记分享】动态规划核心原理详解及27道LeetCode相关经典题目汇总(4)
【42页动态规划学习笔记分享】动态规划核心原理详解及27道LeetCode相关经典题目汇总
|
16天前
|
存储 算法
【42页动态规划学习笔记分享】动态规划核心原理详解及27道LeetCode相关经典题目汇总(1)
【42页动态规划学习笔记分享】动态规划核心原理详解及27道LeetCode相关经典题目汇总
|
16天前
|
算法
【经典LeetCode算法题目专栏分类】【第11期】递归问题:字母大小写全排列、括号生成
【经典LeetCode算法题目专栏分类】【第11期】递归问题:字母大小写全排列、括号生成
|
16天前
|
算法
【经典LeetCode算法题目专栏分类】【第9期】深度优先搜索DFS与并查集:括号生成、岛屿问题、扫雷游戏
【经典LeetCode算法题目专栏分类】【第9期】深度优先搜索DFS与并查集:括号生成、岛屿问题、扫雷游戏
|
16天前
|
算法
【经典LeetCode算法题目专栏分类】【第8期】滑动窗口:最小覆盖子串、字符串排列、找所有字母异位词、 最长无重复子串
【经典LeetCode算法题目专栏分类】【第8期】滑动窗口:最小覆盖子串、字符串排列、找所有字母异位词、 最长无重复子串