欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 科技 > 能源 > LeetCode 47.全排列 II

LeetCode 47.全排列 II

2025/6/20 8:53:45 来源:https://blog.csdn.net/weixin_45524582/article/details/148762164  浏览:    关键词:LeetCode 47.全排列 II

LeetCode 47.全排列 II 是一个经典的回溯算法问题,要求生成一个包含重复数字的数组的所有不重复的全排列。与普通的全排列问题(LeetCode 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]]

解题思路

• 排序:首先对数组进行排序,这样可以方便地处理重复元素。如果两个相邻的元素相同,那么在生成排列时可以跳过重复的分支。

• 回溯算法:使用回溯算法生成所有可能的排列。

• 使用一个布尔数组used来记录当前元素是否已经被使用。

• 在递归过程中,逐步构建当前排列,并在满足条件时将其加入结果列表。

• 对于重复元素,如果当前元素与前一个元素相同且前一个元素未被使用,则跳过当前分支,避免重复。

代码实现
以下是用 C++实现的完整代码:

#include <iostream>
#include <vector>
#include <algorithm>using namespace std;class Solution {
public:vector<vector<int>> permuteUnique(vector<int>& nums) {vector<vector<int>> result;vector<int> path;vector<bool> used(nums.size(), false);// 对数组进行排序,方便处理重复元素sort(nums.begin(), nums.end());// 回溯函数function<void()> backtrack = [&]() {if (path.size() == nums.size()) {result.push_back(path);return;}for (int i = 0; i < nums.size(); ++i) {// 跳过重复元素if (used[i] || (i > 0 && nums[i] == nums[i - 1] && !used[i - 1])) {continue;}// 选择当前元素path.push_back(nums[i]);used[i] = true;// 递归backtrack();// 回溯path.pop_back();used[i] = false;}};backtrack();return result;}
};int main() {Solution solution;vector<int> nums = {1, 1, 2};vector<vector<int>> result = solution.permuteUnique(nums);for (const auto& perm : result) {for (int num : perm) {cout << num << " ";}cout << endl;}return 0;
}

代码解释

• 排序:

   sort(nums.begin(), nums.end());

对数组进行排序,这样可以方便地处理重复元素。

• 回溯函数:

• 如果当前路径的长度等于数组的长度,说明已经生成了一个完整的排列,将其加入结果列表。

• 遍历数组,对于每个元素:

• 如果当前元素已经被使用,或者当前元素与前一个元素相同且前一个元素未被使用,则跳过当前分支。

• 否则,将当前元素加入路径,并标记为已使用。

• 递归调用回溯函数。

• 回溯时,移除当前元素,并标记为未使用。

• 跳过重复元素:

   if (used[i] || (i > 0 && nums[i] == nums[i - 1] && !used[i - 1])) {continue;}

这里有两个条件:

• 如果当前元素已经被使用,跳过。

• 如果当前元素与前一个元素相同,且前一个元素未被使用,跳过。这是因为前一个元素已经在之前的递归分支中被处理过,再处理当前元素会导致重复。

输出
对于输入[1, 1, 2],程序的输出为:

1 1 2
1 2 1
2 1 1

时间复杂度

• 由于数组中可能有重复元素,时间复杂度为(O(n!\times n)),其中(n)是数组的长度。

• 排序的时间复杂度为(O(n\log n)),但对总时间复杂度影响较小。

空间复杂度

• 递归调用的深度为(O(n))。

• 结果列表的空间复杂度为(O(n!\times n))。

关于判断条件 !used[i - 1]

当然可以!!used[i - 1]是一个关键的条件,用于避免生成重复的排列。我们来详细解释一下这个条件的作用。

背景
在处理包含重复元素的数组时,我们需要确保生成的排列是唯一的。例如,对于数组[1, 1, 2],我们希望生成的排列是:

[1, 1, 2]
[1, 2, 1]
[2, 1, 1]

而不是:

[1, 1, 2]
[1, 2, 1]
[2, 1, 1]
[1, 1, 2]  // 重复
[1, 2, 1]  // 重复

问题的核心
当数组中有重复元素时,如何避免生成重复的排列?

!used[i - 1]的作用
假设数组已经排序,例如[1, 1, 2]。在回溯算法中,我们逐个选择数组中的元素,并将其加入当前路径。为了防止重复,我们需要避免在相同层级的递归中重复选择相同的元素。

具体解释

nums[i] == nums[i - 1]:当前元素与前一个元素相同。

!used[i - 1]:前一个相同的元素尚未被使用。

如果这两个条件同时满足,说明当前元素是重复的,并且它的前一个相同元素还没有被使用。这意味着我们已经在之前的递归分支中处理过这个元素,因此当前分支会导致重复的排列。

举个例子
假设数组是[1, 1, 2],排序后仍然是[1, 1, 2]

• 第一层递归:

• 选择第一个1,路径变为[1],标记used[0] = true

• 递归进入下一层。

• 第二层递归:

• 选择第二个1,路径变为[1, 1],标记used[1] = true

• 递归进入下一层。

• 选择2,路径变为[1, 1, 2],这是一个有效的排列。

• 回溯:

• 回到第二层,撤销选择第二个1,路径变为[1],标记used[1] = false

• 选择2,路径变为[1, 2],标记used[2] = true

• 递归进入下一层。

• 选择第二个1,路径变为[1, 2, 1],这是一个有效的排列。

• 回到第一层:

• 撤销选择第一个1,路径为空,标记used[0] = false

• 选择第二个1,路径变为[1],标记used[1] = true

• 但是此时,nums[1] == nums[0]!used[0],即当前元素与前一个元素相同,且前一个元素未被使用。这意味着我们已经在之前的分支中处理过这个元素,因此跳过当前分支。

• 选择2

• 路径变为[2],标记used[2] = true

• 选择第一个1,路径变为[2, 1],标记used[0] = true

• 选择第二个1,路径变为[2, 1, 1],这是一个有效的排列。

总结
!used[i - 1]的作用是确保在相同层级的递归中,不会重复选择相同的元素。如果当前元素与前一个元素相同,且前一个元素尚未被使用,那么当前分支会导致重复的排列,因此需要跳过。

希望这个解释清楚地说明了!used[i - 1]的作用!如果有任何疑问,欢迎继续提问。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词