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

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

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

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

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

作者专栏每日更新:

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

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

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

题目描述

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

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

主要区别
  1. 输入元素的不同
  • 题目 46:输入的数组中的数字不重复,这意味着每个数字在每个排列中只会出现一次。
  • 题目 47:输入的数组中的数字可能重复,这意味着需要避免生成重复的排列,排列结果中可能会多次出现同一数字。
  1. 解题难度
  • 因为题目 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!)),但剪枝操作大大减少了无效的递归调用。

方法二(字典序算法)

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

方法三(递归交换法)

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

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

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

方法五(迭代法)

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

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


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

相关文章
|
10月前
|
算法 容器
【算法】——双指针算法合集(力扣)
移动零,复写零,快乐数,盛最多水的容器,有效三角形的个数,和为s的两个数(查找总价格为目标值的两个商品 ),三数之和,四数之和
|
12月前
|
Python
【10月更文挑战第18天】「Mac上学Python 29」基础篇10 - 循环结构与迭代控制
在Python中,循环结构是控制程序执行的重要工具。通过学习本篇内容,您将掌握如何使用for循环和while循环来高效地处理重复任务,并了解break、continue和else的使用方式。同时,我们还会探索嵌套循环和典型应用场景中的实际应用。
130 2
|
12月前
|
算法
【链表】算法题(二) ----- 力扣/牛客
【链表】算法题(二) ----- 力扣/牛客
|
12月前
|
算法
Leetcode第46题(全排列)
这篇文章介绍了LeetCode第46题“全排列”的解题方法,使用深度优先搜索(DFS)和回溯算法来生成给定数组的所有可能排列。
151 0
Leetcode第46题(全排列)
|
12月前
LeetCode第二十四题(两两交换链表中的节点)
这篇文章介绍了LeetCode第24题的解法,即如何通过使用三个指针(preNode, curNode, curNextNode)来两两交换链表中的节点,并提供了详细的代码实现。
121 0
LeetCode第二十四题(两两交换链表中的节点)
|
12月前
|
算法 数据挖掘
【栈和队列】算法题 ---- 力扣(二)
【栈和队列】算法题 ---- 力扣
|
12月前
|
存储 算法
【栈和队列】算法题 ---- 力扣(一)
【栈和队列】算法题 ---- 力扣
|
12月前
|
存储 大数据 数据处理
理解Python中的生成器:高效迭代的秘密
【10月更文挑战第8天】理解Python中的生成器:高效迭代的秘密
100 0
|
12月前
Leetcode第47题(全排列II)
LeetCode第47题要求返回一个包含重复数字序列的所有不重复全排列,通过深度优先搜索和去重策略来解决。
84 0

热门文章

最新文章

推荐镜像

更多