Please upgrade to NeetCode Pro to view company tags.
Prerequisites
Before attempting this problem, you should be comfortable with:
Backtracking - Building permutations by exploring choices and undoing them
Recursion - The recursive nature of generating all permutations
Hash Set / Hash Map - Tracking duplicates or counting element frequencies
Sorting - Arranging elements so duplicates are adjacent for easier handling
Duplicate Handling - Understanding how to skip duplicate elements to avoid repeated permutations
1. Backtracking (Hash Set)
Intuition
Since the input array may contain duplicates, generating all permutations naively would produce duplicate results. One straightforward way to handle this is to generate all permutations using standard backtracking and store them in a hash set, which automatically filters out duplicates.
We mark elements as "used" by temporarily replacing them with a sentinel value, ensuring each element is used exactly once per permutation.
Algorithm
Initialize an empty set to store unique permutations.
Use backtracking to build permutations:
If the current permutation has the same length as nums, add it to the set.
Otherwise, iterate through nums and for each unused element:
Mark it as used (replace with sentinel value).
Add it to the current permutation and recurse.
Restore the original value and remove it from the permutation.
funcpermuteUnique(nums []int)[][]int{
res :=make(map[string][]int)
perm :=[]int{}var backtrack func()
backtrack =func(){iflen(perm)==len(nums){
temp :=make([]int,len(perm))copy(temp, perm)
key := fmt.Sprint(temp)
res[key]= temp
return}for i :=0; i <len(nums); i++{if nums[i]!=-1001{
temp := nums[i]
perm =append(perm, nums[i])
nums[i]=-1001backtrack()
nums[i]= temp
perm = perm[:len(perm)-1]}}}backtrack()
result :=[][]int{}for_, v :=range res {
result =append(result, v)}return result
}
class Solution {funpermuteUnique(nums: IntArray): List<List<Int>>{val res = mutableSetOf<List<Int>>()val perm = mutableListOf<Int>()funbacktrack(){if(perm.size == nums.size){
res.add(perm.toList())return}for(i in nums.indices){if(nums[i]!= Int.MIN_VALUE){val temp = nums[i]
perm.add(nums[i])
nums[i]= Int.MIN_VALUE
backtrack()
nums[i]= temp
perm.removeAt(perm.size -1)}}}backtrack()return res.toList()}}
classSolution{funcpermuteUnique(_ nums:[Int])->[[Int]]{var res =Set<[Int]>()var nums = nums
var perm =[Int]()funcbacktrack(){if perm.count == nums.count {
res.insert(perm)return}for i in0..<nums.count {if nums[i]!=Int.min {let temp = nums[i]
perm.append(nums[i])
nums[i]=Int.min
backtrack()
nums[i]= temp
perm.removeLast()}}}backtrack()returnArray(res)}}
implSolution{pubfnpermute_unique(nums:Vec<i32>)->Vec<Vec<i32>>{letmut res =HashSet::new();letmut nums = nums;let n = nums.len();letmut perm =Vec::new();fnbacktrack(nums:&mutVec<i32>, perm:&mutVec<i32>, n:usize, res:&mutHashSet<Vec<i32>>){if perm.len()== n {
res.insert(perm.clone());return;}for i in0..n {if nums[i]!=i32::MIN{let temp = nums[i];
perm.push(nums[i]);
nums[i]=i32::MIN;backtrack(nums, perm, n, res);
nums[i]= temp;
perm.pop();}}}backtrack(&mut nums,&mut perm, n,&mut res);
res.into_iter().collect()}}
Time & Space Complexity
Time complexity: O(n!∗n)
Space complexity: O(n!∗n) for the hash set.
2. Backtracking (Hash Map)
Intuition
Instead of using a set to filter duplicates after the fact, we can prevent duplicates from being generated in the first place. By counting how many times each unique number appears, we only pick each distinct value once per position in the permutation.
When we decrement a count to zero, that number is no longer available for deeper recursion levels. This naturally avoids generating the same permutation multiple times.
Algorithm
Build a frequency map counting occurrences of each number in nums.
Use backtracking to build permutations:
If the current permutation has the same length as nums, add a copy to the result.
Otherwise, iterate through each unique number in the frequency map:
If its count is greater than 0, add it to the permutation and decrement the count.
Recurse to fill the next position.
Increment the count back and remove the number from the permutation.
classSolution:defpermuteUnique(self, nums: List[int])-> List[List[int]]:
res =[]
perm =[]
count ={n:0for n in nums}for num in nums:
count[num]+=1defdfs():iflen(perm)==len(nums):
res.append(perm.copy())returnfor num in count:if count[num]>0:
perm.append(num)
count[num]-=1
dfs()
count[num]+=1
perm.pop()
dfs()return res
classSolution{/**
* @param {number[]} nums
* @return {number[][]}
*/permuteUnique(nums){const res =[];const perm =[];const count ={};for(const num of nums){
count[num]=(count[num]||0)+1;}constdfs=()=>{if(perm.length === nums.length){
res.push([...perm]);return;}for(const num in count){if(count[num]>0){
perm.push(+num);
count[num]--;dfs();
count[num]++;
perm.pop();}}};dfs();return res;}}
publicclassSolution{publicList<List<int>>PermuteUnique(int[] nums){var res =newList<List<int>>();var perm =newList<int>();var count =newDictionary<int,int>();foreach(int num in nums){if(!count.ContainsKey(num)){
count[num]=0;}
count[num]++;}voidDfs(){if(perm.Count == nums.Length){
res.Add(newList<int>(perm));return;}foreach(var kvp in count){int num = kvp.Key;if(count[num]>0){
perm.Add(num);
count[num]--;Dfs();
count[num]++;
perm.RemoveAt(perm.Count -1);}}}Dfs();return res;}}
funcpermuteUnique(nums []int)[][]int{
res :=[][]int{}
perm :=[]int{}
count :=make(map[int]int)for_, num :=range nums {
count[num]++}var dfs func()
dfs =func(){iflen(perm)==len(nums){
temp :=make([]int,len(perm))copy(temp, perm)
res =append(res, temp)return}for num :=range count {if count[num]>0{
perm =append(perm, num)
count[num]--dfs()
count[num]++
perm = perm[:len(perm)-1]}}}dfs()return res
}
class Solution {funpermuteUnique(nums: IntArray): List<List<Int>>{val res = mutableListOf<List<Int>>()val perm = mutableListOf<Int>()val count = mutableMapOf<Int, Int>()for(num in nums){
count[num]= count.getOrDefault(num,0)+1}fundfs(){if(perm.size == nums.size){
res.add(perm.toList())return}for(num in count.keys){if(count[num]!!>0){
perm.add(num)
count[num]= count[num]!!-1dfs()
count[num]= count[num]!!+1
perm.removeAt(perm.size -1)}}}dfs()return res
}}
classSolution{funcpermuteUnique(_ nums:[Int])->[[Int]]{var res =[[Int]]()var perm =[Int]()var count =[Int:Int]()for num in nums {
count[num,default:0]+=1}funcdfs(){if perm.count == nums.count {
res.append(perm)return}for num in count.keys {if count[num]!>0{
perm.append(num)
count[num]!-=1dfs()
count[num]!+=1
perm.removeLast()}}}dfs()return res
}}
implSolution{pubfnpermute_unique(nums:Vec<i32>)->Vec<Vec<i32>>{letmut res =Vec::new();letmut count =HashMap::new();for&num in&nums {*count.entry(num).or_insert(0)+=1;}let keys:Vec<i32>= count.keys().copied().collect();let n = nums.len();letmut perm =Vec::new();fndfs(keys:&[i32], count:&mutHashMap<i32,i32>, perm:&mutVec<i32>, n:usize, res:&mutVec<Vec<i32>>){if perm.len()== n {
res.push(perm.clone());return;}for&num in keys {if*count.get(&num).unwrap()>0{
perm.push(num);*count.get_mut(&num).unwrap()-=1;dfs(keys, count, perm, n, res);*count.get_mut(&num).unwrap()+=1;
perm.pop();}}}dfs(&keys,&mut count,&mut perm, n,&mut res);
res
}}
Time & Space Complexity
Time complexity: O(n!∗n)
Space complexity: O(n!∗n) for the output list.
3. Backtracking (Boolean Array)
Intuition
By sorting the array first, duplicate values become adjacent. This allows us to apply a simple rule: when we encounter a duplicate, only use it if the previous identical element has already been used in the current permutation path. This ensures each unique arrangement is generated exactly once.
A boolean array tracks which indices are currently in use during backtracking.
Algorithm
Sort nums so duplicates are adjacent.
Create a boolean array visit to track used indices.
Use backtracking to build permutations:
If the current permutation has the same length as nums, add a copy to the result.
Otherwise, iterate through each index:
Skip if already visited.
Skip if this element equals the previous one and the previous one is not visited (to avoid duplicates).
Mark the index as visited, add the element to the permutation, and recurse.
Unmark the index and remove the element from the permutation.
classSolution{/**
* @param {number[]} nums
* @return {number[][]}
*/permuteUnique(nums){const res =[];const visit =newArray(nums.length).fill(false);const perm =[];
nums.sort((a, b)=> a - b);constdfs=()=>{if(perm.length === nums.length){
res.push([...perm]);return;}for(let i =0; i < nums.length; i++){if(
visit[i]||(i >0&& nums[i]=== nums[i -1]&&!visit[i -1]))continue;
visit[i]=true;
perm.push(nums[i]);dfs();
visit[i]=false;
perm.pop();}};dfs();return res;}}
publicclassSolution{publicList<List<int>>PermuteUnique(int[] nums){var res =newList<List<int>>();var perm =newList<int>();int n = nums.Length;bool[] visit =newbool[n];
Array.Sort(nums);voidDfs(){if(perm.Count == n){
res.Add(newList<int>(perm));return;}for(int i =0; i < n; i++){if(visit[i])continue;if(i >0&& nums[i]== nums[i -1]&&!visit[i -1])continue;
visit[i]=true;
perm.Add(nums[i]);Dfs();
perm.RemoveAt(perm.Count -1);
visit[i]=false;}}Dfs();return res;}}
funcpermuteUnique(nums []int)[][]int{
res :=[][]int{}
n :=len(nums)
visit :=make([]bool, n)
perm :=[]int{}
sort.Ints(nums)var dfs func()
dfs =func(){iflen(perm)== n {
temp :=make([]int,len(perm))copy(temp, perm)
res =append(res, temp)return}for i :=0; i < n; i++{if visit[i]{continue}if i >0&& nums[i]== nums[i-1]&&!visit[i-1]{continue}
visit[i]=true
perm =append(perm, nums[i])dfs()
visit[i]=false
perm = perm[:len(perm)-1]}}dfs()return res
}
class Solution {funpermuteUnique(nums: IntArray): List<List<Int>>{val res = mutableListOf<List<Int>>()val n = nums.size
val visit =BooleanArray(n)val perm = mutableListOf<Int>()
nums.sort()fundfs(){if(perm.size == n){
res.add(perm.toList())return}for(i in0 until n){if(visit[i])continueif(i >0&& nums[i]== nums[i -1]&&!visit[i -1])continue
visit[i]=true
perm.add(nums[i])dfs()
visit[i]=false
perm.removeAt(perm.size -1)}}dfs()return res
}}
classSolution{funcpermuteUnique(_ nums:[Int])->[[Int]]{var res =[[Int]]()var nums = nums.sorted()let n = nums.count
var visit =[Bool](repeating:false, count: n)var perm =[Int]()funcdfs(){if perm.count == n {
res.append(perm)return}for i in0..<n {if visit[i]{continue}if i >0&& nums[i]== nums[i -1]&&!visit[i -1]{continue}
visit[i]=true
perm.append(nums[i])dfs()
visit[i]=false
perm.removeLast()}}dfs()return res
}}
implSolution{pubfnpermute_unique(mut nums:Vec<i32>)->Vec<Vec<i32>>{
nums.sort();let n = nums.len();letmut res =Vec::new();letmut visit =vec![false; n];letmut perm =Vec::new();fndfs(nums:&[i32], visit:&mutVec<bool>, perm:&mutVec<i32>, n:usize, res:&mutVec<Vec<i32>>){if perm.len()== n {
res.push(perm.clone());return;}for i in0..n {if visit[i]{continue;}if i >0&& nums[i]== nums[i -1]&&!visit[i -1]{continue;}
visit[i]=true;
perm.push(nums[i]);dfs(nums, visit, perm, n, res);
visit[i]=false;
perm.pop();}}dfs(&nums,&mut visit,&mut perm, n,&mut res);
res
}}
Time & Space Complexity
Time complexity: O(n!∗n)
Space complexity: O(n!∗n) for the output list.
4. Backtracking (Optimal)
Intuition
This approach generates permutations by swapping elements in place rather than building a separate permutation array. At each position i, we try placing each element from index i onward. By sorting first and skipping swaps that would place a duplicate value at position i, we avoid generating duplicate permutations.
After each recursive call, we restore the array to its sorted state for position i by reverse-swapping all elements back.
Algorithm
Sort nums so duplicates are adjacent.
Use backtracking starting at index 0:
If index i equals the length of nums, add a copy of nums to the result.
Otherwise, for each index j from i to the end:
Skip if j > i and nums[j] equals nums[i] (duplicate at this position).
Swap nums[i] and nums[j], then recurse with i + 1.
After the loop, restore the array by reverse-swapping elements back from the end to i + 1.
funcpermuteUnique(nums []int)[][]int{
res :=[][]int{}
sort.Ints(nums)var dfs func(i int)
dfs =func(i int){if i ==len(nums){
temp :=make([]int,len(nums))copy(temp, nums)
res =append(res, temp)return}for j := i; j <len(nums); j++{if j > i && nums[j]== nums[i]{continue}
nums[i], nums[j]= nums[j], nums[i]dfs(i +1)}for j :=len(nums)-1; j > i; j--{
nums[i], nums[j]= nums[j], nums[i]}}dfs(0)return res
}
class Solution {funpermuteUnique(nums: IntArray): List<List<Int>>{val res = mutableListOf<List<Int>>()
nums.sort()fundfs(i: Int){if(i == nums.size){
res.add(nums.toList())return}for(j in i until nums.size){if(j > i && nums[j]== nums[i])continue
nums[i]= nums[j].also{ nums[j]= nums[i]}dfs(i +1)}for(j in nums.size -1 downTo i +1){
nums[i]= nums[j].also{ nums[j]= nums[i]}}}dfs(0)return res
}}
classSolution{funcpermuteUnique(_ nums:[Int])->[[Int]]{var res =[[Int]]()var nums = nums.sorted()funcdfs(_ i:Int){if i == nums.count {
res.append(nums)return}for j in i..<nums.count {if j > i && nums[j]== nums[i]{continue}
nums.swapAt(i, j)dfs(i +1)}for j instride(from: nums.count -1, to: i, by:-1){
nums.swapAt(i, j)}}dfs(0)return res
}}
implSolution{pubfnpermute_unique(mut nums:Vec<i32>)->Vec<Vec<i32>>{
nums.sort();letmut res =Vec::new();let n = nums.len();fndfs(i:usize, nums:&mutVec<i32>, n:usize, res:&mutVec<Vec<i32>>){if i == n {
res.push(nums.clone());return;}for j in i..n {if j > i && nums[j]== nums[i]{continue;}
nums.swap(i, j);dfs(i +1, nums, n, res);}for j in(i +1..n).rev(){
nums.swap(i, j);}}dfs(0,&mut nums, n,&mut res);
res
}}
Time & Space Complexity
Time complexity: O(n!∗n)
Space complexity: O(n!∗n) for the output list.
5. Iteration
Intuition
This approach uses the "next permutation" algorithm to iterate through all permutations in lexicographic order. Starting from the smallest permutation (sorted array), we repeatedly find the next lexicographically larger permutation until we cycle back to the beginning.
Since we generate permutations in strict order, duplicates in the input naturally produce unique permutations without extra handling.
Algorithm
Sort nums to get the lexicographically smallest permutation.
Add a copy of nums to the result.
Repeat until we return to the starting permutation:
Find the largest index i such that nums[i] < nums[i + 1]. If no such index exists, we have the last permutation.
Find the largest index j such that nums[j] > nums[i].
classSolution:defpermuteUnique(self, nums: List[int])-> List[List[int]]:
n =len(nums)
nums.sort()
res =[nums.copy()]whileTrue:
i = n -2while i >=0and nums[i]>= nums[i +1]:
i -=1if i <0:break
j = n -1while nums[j]<= nums[i]:
j -=1
nums[i], nums[j]= nums[j], nums[i]
l, r = i +1, n -1while l < r:
nums[l], nums[r]= nums[r], nums[l]
l, r = l +1, r -1
res.append(nums.copy())return res
publicclassSolution{publicList<List<Integer>>permuteUnique(int[] nums){int n = nums.length;Arrays.sort(nums);List<List<Integer>> res =newArrayList<>();
res.add(toList(nums));while(true){int i = n -2;while(i >=0&& nums[i]>= nums[i +1]) i--;if(i <0)break;int j = n -1;while(nums[j]<= nums[i]) j--;swap(nums, i, j);reverse(nums, i +1, n -1);
res.add(toList(nums));}return res;}privatevoidreverse(int[] nums,int l,int r){while(l < r){swap(nums, l++, r--);}}privatevoidswap(int[] nums,int i,int j){int temp = nums[i];
nums[i]= nums[j];
nums[j]= temp;}privateList<Integer>toList(int[] nums){List<Integer> list =newArrayList<>();for(int num : nums) list.add(num);return list;}}
classSolution{public:
vector<vector<int>>permuteUnique(vector<int>& nums){int n = nums.size();sort(nums.begin(), nums.end());
vector<vector<int>> res ={nums};while(true){int i = n -2;while(i >=0&& nums[i]>= nums[i +1]) i--;if(i <0)break;int j = n -1;while(nums[j]<= nums[i]) j--;swap(nums[i], nums[j]);reverse(nums.begin()+ i +1, nums.end());
res.push_back(nums);}return res;}};
classSolution{/**
* @param {number[]} nums
* @return {number[][]}
*/permuteUnique(nums){const n = nums.length;
nums.sort((a, b)=> a - b);const res =[nums.slice()];while(true){let i = n -2;while(i >=0&& nums[i]>= nums[i +1]) i--;if(i <0)break;let j = n -1;while(nums[j]<= nums[i]) j--;[nums[i], nums[j]]=[nums[j], nums[i]];let l = i +1,
r = n -1;while(l < r){[nums[l], nums[r]]=[nums[r], nums[l]];
l++;
r--;}
res.push(nums.slice());}return res;}}
publicclassSolution{publicList<List<int>>PermuteUnique(int[] nums){int n = nums.Length;
Array.Sort(nums);var res =newList<List<int>>();
res.Add(newList<int>(nums));while(true){int i = n -2;while(i >=0&& nums[i]>= nums[i +1]){
i--;}if(i <0)break;int j = n -1;while(nums[j]<= nums[i]){
j--;}Swap(nums, i, j);int left = i +1, right = n -1;while(left < right){Swap(nums, left++, right--);}
res.Add(newList<int>(nums));}return res;}privatevoidSwap(int[] nums,int i,int j){int temp = nums[i];
nums[i]= nums[j];
nums[j]= temp;}}
funcpermuteUnique(nums []int)[][]int{
n :=len(nums)
sort.Ints(nums)
res :=[][]int{}
temp :=make([]int, n)copy(temp, nums)
res =append(res, temp)for{
i := n -2for i >=0&& nums[i]>= nums[i+1]{
i--}if i <0{break}
j := n -1for nums[j]<= nums[i]{
j--}
nums[i], nums[j]= nums[j], nums[i]
l, r := i+1, n-1for l < r {
nums[l], nums[r]= nums[r], nums[l]
l++
r--}
temp :=make([]int, n)copy(temp, nums)
res =append(res, temp)}return res
}
class Solution {funpermuteUnique(nums: IntArray): List<List<Int>>{val n = nums.size
nums.sort()val res =mutableListOf(nums.toList())while(true){var i = n -2while(i >=0&& nums[i]>= nums[i +1]){
i--}if(i <0)breakvar j = n -1while(nums[j]<= nums[i]){
j--}
nums[i]= nums[j].also{ nums[j]= nums[i]}var l = i +1var r = n -1while(l < r){
nums[l]= nums[r].also{ nums[r]= nums[l]}
l++
r--}
res.add(nums.toList())}return res
}}
classSolution{funcpermuteUnique(_ nums:[Int])->[[Int]]{var nums = nums.sorted()let n = nums.count
var res =[nums]whiletrue{var i = n -2while i >=0&& nums[i]>= nums[i +1]{
i -=1}if i <0{break}var j = n -1while nums[j]<= nums[i]{
j -=1}
nums.swapAt(i, j)var l = i +1var r = n -1while l < r {
nums.swapAt(l, r)
l +=1
r -=1}
res.append(nums)}return res
}}
implSolution{pubfnpermute_unique(mut nums:Vec<i32>)->Vec<Vec<i32>>{
nums.sort();let n = nums.len();letmut res =vec![nums.clone()];loop{letmut i = n asi32-2;while i >=0&& nums[i asusize]>= nums[i asusize+1]{
i -=1;}if i <0{break;}let i = i asusize;letmut j = n -1;while nums[j]<= nums[i]{
j -=1;}
nums.swap(i, j);
nums[i +1..].reverse();
res.push(nums.clone());}
res
}}
Time & Space Complexity
Time complexity: O(n!∗n)
Space complexity: O(n!∗n) for the output list.
Common Pitfalls
Forgetting to Sort the Array
When using the boolean array approach to skip duplicates, the array must be sorted first so that duplicate values are adjacent. Without sorting, the condition nums[i] == nums[i-1] does not correctly identify duplicates, and the algorithm produces duplicate permutations.
Incorrect Duplicate Skipping Logic
The condition to skip duplicates is subtle: skip when i > 0 && nums[i] == nums[i-1] && !visit[i-1]. Mistakenly using visit[i-1] instead of !visit[i-1] (or vice versa) still produces correct results but may generate duplicates or miss valid permutations. Understanding why we check if the previous duplicate was NOT used is key to correctness.
Using Wrong Sentinel Values
When marking elements as "used" with a sentinel value (like Integer.MIN_VALUE or -Infinity), ensure the sentinel cannot appear as a valid input value. If the input can contain the sentinel value, the algorithm incorrectly skips valid elements or includes already-used elements.
Sign in to join the discussion