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!)),但剪枝操作大大减少了无效的递归调用。

方法二(字典序算法)

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

方法三(递归交换法)

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

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

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

方法五(迭代法)

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

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


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

相关文章
|
2月前
|
算法 容器
【算法】——双指针算法合集(力扣)
移动零,复写零,快乐数,盛最多水的容器,有效三角形的个数,和为s的两个数(查找总价格为目标值的两个商品 ),三数之和,四数之和
|
3月前
|
存储 算法 安全
2024重生之回溯数据结构与算法系列学习之串(12)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丟脸好嘛?】
数据结构与算法系列学习之串的定义和基本操作、串的储存结构、基本操作的实现、朴素模式匹配算法、KMP算法等代码举例及图解说明;【含常见的报错问题及其对应的解决方法】你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
2024重生之回溯数据结构与算法系列学习之串(12)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丟脸好嘛?】
|
3月前
|
算法 Python
在Python编程中,分治法、贪心算法和动态规划是三种重要的算法。分治法通过将大问题分解为小问题,递归解决后合并结果
在Python编程中,分治法、贪心算法和动态规划是三种重要的算法。分治法通过将大问题分解为小问题,递归解决后合并结果;贪心算法在每一步选择局部最优解,追求全局最优;动态规划通过保存子问题的解,避免重复计算,确保全局最优。这三种算法各具特色,适用于不同类型的问题,合理选择能显著提升编程效率。
76 2
|
3月前
|
算法 安全 搜索推荐
2024重生之回溯数据结构与算法系列学习(8)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第2.3章之IKUN和I原达人之数据结构与算法系列学习x单双链表精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
3月前
|
算法 安全 搜索推荐
2024重生之回溯数据结构与算法系列学习之单双链表精题详解(9)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第2.3章之IKUN和I原达人之数据结构与算法系列学习x单双链表精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
3月前
|
算法 安全 NoSQL
2024重生之回溯数据结构与算法系列学习之栈和队列精题汇总(10)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第3章之IKUN和I原达人之数据结构与算法系列学习栈与队列精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
3月前
|
算法 安全 搜索推荐
2024重生之回溯数据结构与算法系列学习之王道第2.3章节之线性表精题汇总二(5)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
IKU达人之数据结构与算法系列学习×单双链表精题详解、数据结构、C++、排序算法、java 、动态规划 你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
4月前
|
机器学习/深度学习 算法 搜索推荐
从理论到实践,Python算法复杂度分析一站式教程,助你轻松驾驭大数据挑战!
【10月更文挑战第4天】在大数据时代,算法效率至关重要。本文从理论入手,介绍时间复杂度和空间复杂度两个核心概念,并通过冒泡排序和快速排序的Python实现详细分析其复杂度。冒泡排序的时间复杂度为O(n^2),空间复杂度为O(1);快速排序平均时间复杂度为O(n log n),空间复杂度为O(log n)。文章还介绍了算法选择、分而治之及空间换时间等优化策略,帮助你在大数据挑战中游刃有余。
135 3
|
7月前
|
机器学习/深度学习 算法 搜索推荐
从理论到实践,Python算法复杂度分析一站式教程,助你轻松驾驭大数据挑战!
【7月更文挑战第22天】在大数据领域,Python算法效率至关重要。本文深入解析时间与空间复杂度,用大O表示法衡量执行时间和存储需求。通过冒泡排序(O(n^2)时间,O(1)空间)与快速排序(平均O(n log n)时间,O(log n)空间)实例,展示Python代码实现与复杂度分析。策略包括算法适配、分治法应用及空间换取时间优化。掌握这些,可提升大数据处理能力,持续学习实践是关键。
152 1
|
8月前
|
存储 机器学习/深度学习 算法
Python算法基础教程
Python算法基础教程
47 0

热门文章

最新文章