Python5种算法回溯+剪枝、字典序、递归交换、计数回溯、迭代法 实现全排列ll【力扣题47】

作者介绍:10年大厂数据\经营分析经验,现任大厂数据部门负责人。
会一些的技术:数据分析、算法、SQL、大数据相关、python
欢迎加入社区:码上找工作
作者专栏每日更新:
LeetCode解锁1000题: 打怪升级之旅
python数据分析可视化:企业实战案例
备注说明:方便大家阅读,统一使用python,带必要注释,公众号 数据分析螺丝钉 一起打怪升级

题目描述

给定一个可能包含重复数字的数组 nums,返回所有可能的唯一全排列。你可以以任意顺序返回答案。

LeetCode 题目 47 “全排列 II” 与题目 46 “全排列” 的主要区别在于输入数组中的元素是否可以包含重复数字。这个差异对解题策略和算法实现有重大影响。

主要区别
  1. 输入元素的不同

    • 题目 46:输入的数组中的数字不重复,这意味着每个数字在每个排列中只会出现一次。
    • 题目 47:输入的数组中的数字可能重复,这意味着需要避免生成重复的排列,排列结果中可能会多次出现同一数字。
  2. 解题难度

    • 因为题目 47 允许输入数组中包含重复数字,仅使用简单的回溯算法可能会生成重复的排列结果,因此需要加入额外的机制(如排序和剪枝)来避免重复排列的生成。
    • 而题目 46 由于保证输入数组中的数字不重复,可以直接进行回溯,不需要处理排列结果的去重问题。
输入格式
  • nums:一个整数数组。
输出格式
  • 返回一个列表,包含所有不重复的全排列。

示例

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

方法一:回溯算法 + 剪枝

解题步骤
  1. 排序:首先对数组进行排序,以方便后续剪枝操作。
  2. 回溯函数定义:定义一个回溯函数,使用当前序列 path 和动态的选择列表 used 来生成排列。
  3. 递归终止条件:如果 path 的长度等于 nums 的长度,则添加到结果列表。
  4. 循环选择:遍历 nums 中的每个数字,根据 used 状态决定是否可以选择当前数字。
  5. 剪枝条件:如果当前数字和前一个数字相同,且前一个数字未被使用,则跳过当前数字。
完整的规范代码
def permuteUnique(nums):
    """
    使用回溯算法加剪枝生成数组的所有不重复全排列
    :param nums: List[int], 输入的整数数组
    :return: List[List[int]], 所有不重复的全排列
    """
    def backtrack(path, used):
        if len(path) == len(nums):
            res.append(path[:])
            return
        for i in range(len(nums)):
            if used[i] or (i > 0 and nums[i] == nums[i - 1] and not used[i - 1]):
                continue
            used[i] = True
            path.append(nums[i])
            backtrack(path, used)
            path.pop()
            used[i] = False

    nums.sort()  # 排序以方便剪枝
    res = []
    backtrack([], [False] * len(nums))
    return res

# 示例调用
print(permuteUnique([1, 1, 2]))  # 输出: [[1, 1, 2], [1, 2, 1], [2, 1, 1]]
算法分析
  • 时间复杂度:(O(n!)),尽管使用了剪枝,但在最坏情况下,仍需尝试所有排列。
  • 空间复杂度:(O(n)),递归深度为 (n),使用了额外的空间存储当前排列和状态标记。

方法二:字典序排列法

解题步骤
  1. 排序:与方法一相同,首先排序数组。
  2. 查找下一个排列:实现一个函数,每次调用生成数组的下一个字典序排列。
  3. 迭代生成:从初始排序开始,持续生成下一个排列,直到回到初始排序。
完整的规范代码
def permuteUnique(nums):
    """
    使用字典序排列法生成数组的所有不重复全排列
    :param nums: List[int], 输入的整数数组
    :return: List[List[int]], 所有不重复的全排列
    """
    def next_permutation(nums):
        i = len(nums) - 2
        while i >= 0 and nums[i] >= nums[i + 1]:
            i -= 1
        if i == -1:
            nums.reverse()
            return False
        j = len(nums) - 1
        while nums[j] <= nums[i]:
            j -= 1
        nums[i], nums[j] = nums[j], nums[i]
        nums[i + 1:] = reversed(nums[i + 1:])
        return True

    nums.sort()
    res = [nums[:]]
    while next_permutation(nums):
        res.append(nums[:])
    return res

# 示例调用
print(permuteUnique([1, 1, 2]))  # 输出: [[1, 1, 2], [1, 2, 1], [2, 1, 1]]
算法分析
  • 时间复杂度:(O(n! ✖️ n)),生成每个排列需要 (O(n)) 时间用于找下一个排列。
  • 空间复杂度:(O(1)),除了输出数组外,空间使用是常数。

方法三:递归交换法

解题步骤
  1. 递归函数定义:定义一个递归函数,使用当前索引 start 来处理排列。
  2. 递归终止条件:如果 start 等于数组长度,记录当前排列。
  3. 递归交换:从 start 开始,将每个元素交换到 start 位置,然后递归处理 start + 1。注意剪枝重复元素。
完整的规范代码
def permuteUnique(nums):
    """
    使用递归交换方法生成数组的所有不重复全排列
    :param nums: List[int], 输入的整数数组
    :return: List[List[int]], 所有不重复的全排列
    """
    def backtrack(start):
        if start == len(nums):
            res.append(nums[:])
            return
        seen = set()
        for i in range(start, len(nums)):
            if nums[i] in seen:
                continue
            seen.add(nums[i])
            nums[start], nums[i] = nums[i], nums[start]
            backtrack(start + 1)
            nums[start], nums[i] = nums[i], nums[start]

    nums.sort()  # 排序以方便剪枝
    res = []
    backtrack(0)
    return res

# 示例调用
print(permuteUnique([1, 1, 2]))  # 输出: [[1, 1, 2], [1, 2, 1], [2, 1, 1]]
算法分析
  • 时间复杂度:(O(n!)),通过剪枝减少了不必要的递归调用。
  • 空间复杂度:(O(n)),递归深度为 (n),使用了额外的空间存储状态。

方法四:基于计数的回溯法

解题步骤
  1. 计数统计:使用字典统计每个数字的出现次数。
  2. 回溯生成:基于数字的计数来生成排列,当某个数字的计数用尽时,不再考虑该数字。
完整的规范代码
def permuteUnique(nums):
    """
    使用基于计数的回溯法生成数组的所有不重复全排列
    :param nums: List[int], 输入的整数数组
    :return: List[List[int]], 所有不重复的全排列
    """
    def backtrack(comb, counter):
        if len(comb) == len(nums):
            res.append(comb[:])
            return
        for num in counter:
            if counter[num] > 0:
                comb.append(num)
                counter[num] -= 1
                backtrack(comb, counter)
                comb.pop()
                counter[num] += 1

    counter = {}
    for num in nums:
        if num in counter:
            counter[num] += 1
        else:
            counter[num] = 1

    res = []
    backtrack([], counter)
    return res

# 示例调用
print(permuteUnique([1, 1, 2]))  # 输出: [[1, 1, 2], [1, 2, 1], [2, 1, 1]]
算法分析
  • 时间复杂度:(O(n!)),尽管使用了计数来优化,但在最坏情况下仍需尝试所有排列。
  • 空间复杂度:(O(n)),递归深度和计数存储共同决定了空间复杂度。

方法五:迭代法

解题步骤
  1. 迭代构建:从一个空排列开始,逐步在排列中插入新的数字,同时保证插入后的排列仍然是有效的。
  2. 避免重复:使用集合来避免在同一位置插入重复数字。
完整的规范代码
def permuteUnique(nums):
    """
    使用迭代法生成数组的所有不重复全排列
    :param nums: List[int], 输入的整数数组
    :return: List[List[int]], 所有不重复的全排列
    """
    perms = [[]]
    for num in nums:
        new_perms = []
        for perm in perms:
            for i in range(len(perm) + 1):
                new_perms.append(perm[:i] + [num] + perm[i:])
                if i < len(perm) and perm[i] == num:
                    break
        perms = new_perms
    return perms

# 示例调用
print(permuteUnique([1, 1, 2]))  # 输出: [[1, 1, 2], [1, 2, 1], [2, 1, 1]]
算法分析
  • 时间复杂度:(O(n! ✖️ n)),每次插入操作均涉及到复制和插入操作,这在元素数量较大时尤为显著。
  • 空间复杂度:(O(n! ✖️ n)),存储所有的排列需要的空间。

总结

为了直观地比较解决LeetCode题目47 "全排列 II"的五种方法,下面的表格将展示它们在不同技术维度的性能和特点,包括时间复杂度、空间复杂度以及各自的优势和劣势。

特征方法一:回溯 + 剪枝方法二:字典序算法方法三:递归交换法方法四:基于计数回溯方法五:迭代法
时间复杂度(O(n!))(O(n! ✖️ n))(O(n!))(O(n!))(O(n! ✖️ n))
空间复杂度(O(n))(O(1))(O(n))(O(n))(O(n! ✖️ n))
优势- 有效避免重复
- 直观易懂
- 生成排列效率高
- 无需递归
- 简单易实现
- 直接操作数组
- 处理重复元素高效
- 直观实现
- 不使用递归
- 易于理解和实现
劣势- 需要额外的剪枝逻辑- 实现复杂
- 不直观
- 时间复杂度较高
- 重复排列处理复杂
- 空间复杂度相对较高- 空间占用较大
- 效率可能较低
适用场景- 数组较小时
- 需要去重排列
- 大数据处理
- 需字典顺序排列
- 喜欢直观操作
- 适合小规模数据
- 数组中重复元素多时- 非递归需求
- 理解迭代处理
综合分析

方法一(回溯 + 剪枝)

  • 通过排序和剪枝避免重复,是解决包含重复元素的全排列问题的标准方法。虽然时间复杂度为 (O(n!)),但剪枝操作大大减少了无效的递归调用。

方法二(字典序算法)

  • 通过迭代计算下一个字典序的排列,该方法适用于需要按顺序生成排列的场景。其主要缺点是实现相对复杂,可能不太直观,尤其是对初学者。

方法三(递归交换法)

  • 直接在数组上进行操作,通过递归交换生成所有排列。该方法简单直观,但在数组中存在重复数字时需要额外的逻辑来避免生成重复排列。

方法四(基于计数的回溯法)

  • 利用计数来控制元素使用次数,适合处理含有多重复元素的数组。该方法可以有效地减少重复排列的生成,但空间复杂度略高,因为需要额外的计数存储。

方法五(迭代法)

  • 使用迭代而非递归来生成排列,适用于希望避免递归或栈溢出的情况。尽管易于实现,但其在处理大规模数据时可能效率不高,特别是空间占用较大。

在选择合适的方法时,应考虑实际的需求和问题规模。例如,对于需要

  • 34
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

数据分析螺丝钉

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值