作者介绍:10年大厂数据\经营分析经验,现任大厂数据部门负责人。
会一些的技术:数据分析、算法、SQL、大数据相关、python
欢迎加入社区:码上找工作
作者专栏每日更新:
备注说明:方便大家阅读,统一使用python,带必要注释,公众号 数据分析螺丝钉 一起打怪升级
题目描述
给定一个可能包含重复数字的数组 nums
,返回所有可能的唯一全排列。你可以以任意顺序返回答案。
LeetCode 题目 47 “全排列 II” 与题目 46 “全排列” 的主要区别在于输入数组中的元素是否可以包含重复数字。这个差异对解题策略和算法实现有重大影响。
主要区别
- 输入元素的不同:
- 题目 46:输入的数组中的数字不重复,这意味着每个数字在每个排列中只会出现一次。
- 题目 47:输入的数组中的数字可能重复,这意味着需要避免生成重复的排列,排列结果中可能会多次出现同一数字。
- 解题难度:
- 因为题目 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]]
方法一:回溯算法 + 剪枝
解题步骤
- 排序:首先对数组进行排序,以方便后续剪枝操作。
- 回溯函数定义:定义一个回溯函数,使用当前序列
path
和动态的选择列表used
来生成排列。 - 递归终止条件:如果
path
的长度等于nums
的长度,则添加到结果列表。 - 循环选择:遍历
nums
中的每个数字,根据used
状态决定是否可以选择当前数字。 - 剪枝条件:如果当前数字和前一个数字相同,且前一个数字未被使用,则跳过当前数字。
完整的规范代码
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),使用了额外的空间存储当前排列和状态标记。
方法二:字典序排列法
解题步骤
- 排序:与方法一相同,首先排序数组。
- 查找下一个排列:实现一个函数,每次调用生成数组的下一个字典序排列。
- 迭代生成:从初始排序开始,持续生成下一个排列,直到回到初始排序。
完整的规范代码
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)),除了输出数组外,空间使用是常数。
方法三:递归交换法
解题步骤
- 递归函数定义:定义一个递归函数,使用当前索引
start
来处理排列。 - 递归终止条件:如果
start
等于数组长度,记录当前排列。 - 递归交换:从
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),使用了额外的空间存储状态。
方法四:基于计数的回溯法
解题步骤
- 计数统计:使用字典统计每个数字的出现次数。
- 回溯生成:基于数字的计数来生成排列,当某个数字的计数用尽时,不再考虑该数字。
完整的规范代码
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)),递归深度和计数存储共同决定了空间复杂度。
方法五:迭代法
解题步骤
- 迭代构建:从一个空排列开始,逐步在排列中插入新的数字,同时保证插入后的排列仍然是有效的。
- 避免重复:使用集合来避免在同一位置插入重复数字。
完整的规范代码
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!)),但剪枝操作大大减少了无效的递归调用。
方法二(字典序算法):
- 通过迭代计算下一个字典序的排列,该方法适用于需要按顺序生成排列的场景。其主要缺点是实现相对复杂,可能不太直观,尤其是对初学者。
方法三(递归交换法):
- 直接在数组上进行操作,通过递归交换生成所有排列。该方法简单直观,但在数组中存在重复数字时需要额外的逻辑来避免生成重复排列。
方法四(基于计数的回溯法):
- 利用计数来控制元素使用次数,适合处理含有多重复元素的数组。该方法可以有效地减少重复排列的生成,但空间复杂度略高,因为需要额外的计数存储。
方法五(迭代法):
- 使用迭代而非递归来生成排列,适用于希望避免递归或栈溢出的情况。尽管易于实现,但其在处理大规模数据时可能效率不高,特别是空间占用较大。
在选择合适的方法时,应考虑实际的需求和问题规模。例如,对于需要
欢迎关注微信公众号 数据分析螺丝钉