题目描述
给定一堆整数,可能包含相同数,返回其所有不同的全排列。
样例
输入:
[1,1,2]
输出:
[
[1,1,2],
[1,2,1],
[2,1,1]
]
算法
(回溯) $O(n!)$
由于有重复元素的存在,这道题的枚举顺序和 Permutations 不同。
- 先将所有数从小到大排序,这样相同的数会排在一起;
- 从左到右依次枚举每个数,每次将它放在一个空位上;
- 对于相同数,我们人为定序,就可以避免重复计算:我们在dfs时记录一个额外的状态,记录上一个相同数存放的位置 $start$,我们在枚举当前数时,只枚举 $start+1, start+2, …, n$ 这些位置。
- 不要忘记递归前和回溯时,对状态进行更新。
时间复杂度分析:搜索树中最后一层共 $n!$ 个节点,前面所有层加一块的节点数量相比于最后一层节点数是无穷小量,可以忽略。且最后一层节点记录方案的计算量是 $O(n)$,所以总时间复杂度是 $O(n \times n!)$。
C++ 代码
class Solution {
public:
vector<bool> st;
vector<int> path;
vector<vector<int>> ans;
vector<vector<int>> permuteUnique(vector<int>& nums) {
sort(nums.begin(), nums.end());
st = vector<bool>(nums.size(), false);
path = vector<int>(nums.size());
dfs(nums, 0, 0);
return ans;
}
void dfs(vector<int>& nums, int u, int start)
{
if (u == nums.size())
{
ans.push_back(path);
return;
}
for (int i = start; i < nums.size(); i ++ )
if (!st[i])
{
st[i] = true;
path[i] = nums[u];
if (u + 1 < nums.size() && nums[u + 1] != nums[u])
dfs(nums, u + 1, 0);
else
dfs(nums, u + 1, i + 1);
st[i] = false;
}
}
};
只用一个变量dfs可能更直观一些?
可以的,选择自己最习惯的方式即可。
这代码咋想出来的 这题要是第一次做能写出这样的代码 真的太可怕了
多练练就好了hh
过了三年再看当初的自己 哈哈 好菜啊 这么简单的问题都害怕 y总帮我提升了太多
这个解法中,如果path没指定长度,用pushback放。 popback还原。结果就错了,因为是i位置 放了nums[u]后u+1可能放在0,肯定不一定是i+1的位置,自己mark一下。数分别放在哪个位置上!
感谢,正在纠结错在哪了🤔
对滴,总结的不错
可以把nums看作物品栏,然后选择数字放入列中,如果path指定长度,则是放入具体的位置中,如果不指定长度,这是放入特定的列中,核心思路就是现在放入列中的数,如果之前存在相同的数,必须要被放在前面,这样才保证不重复。如果之前相同的数不在前面,说明已经被算过了,就只能剪枝,continue了
不错。
看懂了,本质就是对于重复的点之间相对位置不变。如[1,1,1,3,3],在放第一个1的位置后,后面的1不能放在其前面,这样就避免了重复,但有一个影响代码性能的点是,如果第一个1放在了path[3]的位置,那么对于第三个1就没有位置可以放了,可能这么点冗余的遍历数组导致性能稍微下降吧。
这里可以稍微优化一下,不过不影响时间复杂度。
很棒!
很好理解,感谢!
大佬总结的真好
我的理解是类似数组排序中的稳定性,相等的数字也要保持相对的顺序,就不会出现重复的排列了。
不错,这里很灵活,只要能避免重复即可。排序也是不影响最坏时间复杂度的,因为 $(nlogn)$ 相比于 $(n!)$ 是无穷小量。
基于permutationI y总的代码改的,在枚举一个位置的情况时用一个单独变量记录这次用过的数,以后的枚举会跳过这个数,代码如下:
不错!这种搜索顺序也是可以的hh
用一个map记录每个数出现几次,枚举的时候遍历map,只要保证枚举每一个位置的时候可选择的数没有重复的就可以了,这样是不是空间复杂度比较大?
这样怎么保证答案不会有重复呢?
每个位置的候选元素互不相同,应该就没有重复了。
不错!这种搜索顺序也是可以的~
而且map数据结构内部有序,=可以保证我们的搜索结果也是字典序的
要保证字典序的话,可以从搜索顺序入手,如果用map来保证字典序,效率会低一些。
请问如果这道题,要求输出字典序,那应该怎么做?
上面的代码会优先枚举值较小的数字,得到的结果就是按字典序排好序的。
不是字典序的,输入为1,2,3,4的话,上面代码结果返回[[1,2,3,4],[1,2,4,3],[1,3,2,4],[1,4,2,3],[1,3,4,2],[1,4,3,2],[2,1,3,4],[2,1,4,3],[3,1,2,4],[4,1,2,3],[3,1,4,2],[4,1,3,2],[2,3,1,4],[2,4,1,3],[3,2,1,4],[4,2,1,3],[3,4,1,2],[4,3,1,2],[2,3,4,1],[2,4,3,1],[3,2,4,1],[4,2,3,1],[3,4,2,1],[4,3,2,1]]
啊对,确实不是字典序。可以换一种搜索顺序:从前往后依次枚举每一位填哪个数字,枚举数字的时候从小到大即可。
请问if (u + 1 < nums.size() && nums[u + 1] != nums[u])中,为什么要加u + 1 < nums.size()这个限制条件
当
u + 1 == nums.size()
时,nums[u + 1]` 就越界了。如果全排列,再用哈希去掉重复的排列,这样的时空复杂度会大多少?
想了一下,如果重复的数字太多,那会多出很多额外的计算量
时间复杂度和空间复杂度都是 $O(n \times n!)$。
谢谢你的回复,这个网站太有用了,每天都能学到很多新知识
加油hh
#### 我认为我的方法看起来好一些
棒!
python 看着就是爽
请问 回溯时是不是忘记将path[i] remove了?
我们这里用
st[i]
表示path[i]
是否存在:st[i] == false
,表示path[i]
存在;st[i] == true
,表示path[i]
不存在;当
st[i] == false
时,path[i]
下次会被新的值覆盖掉,所以就不需要再remove
了。请问一下,这里nums[u+1]==nums[u]的情况下为什么把start 设成i+1呢?
是为了避免枚举重复方案。
比如 $[1_0, 1_1, 2]$ 和 $[1_1, 1_0, 2]$ 是一种方案,为了避免重复,我们给在搜索过程中限定相同数的相对位置不变,这样就只会枚举到第一种方案了。
哦哦哦,明白了!非常感谢🙏