Permutations II (With Duplicates)
Imagine you have a collection of colored beads, some colors repeating, and you want to find all unique ways to arrange them on a string without repeating the same pattern twice.
Given a collection of numbers that might contain duplicates, return all possible unique permutations in any order. Input: An array of integers nums. Output: A list of lists, where each list is a unique permutation of nums.
1 ≤ nums.length ≤ 8-10 ≤ nums[i] ≤ 10"[1,1,2]"[[1,1,2],[1,2,1],[2,1,1]]The input has duplicates (two 1s). The output lists all unique permutations without repetition.
- All elements are the same → only one unique permutation
- Array with no duplicates → all permutations are unique
- Empty array → returns an empty list or list with empty permutation
- Array with negative and positive duplicates → handle sign correctly
Duplicate permutations are generated because duplicates are not adjacent and skipping logic fails
✅ Always sort input before backtracking to group duplicates
Valid permutations are skipped or duplicates remain
✅ Use 'used' array or 'seen' set at recursion level to correctly skip duplicates
Input array remains changed, causing incorrect permutations
✅ Always swap back after recursion to restore original state
Solution is correct but inefficient and may time out on large inputs
✅ Prune duplicates during recursion instead of filtering after generation
Intuition
Generate every possible permutation by swapping elements recursively, then use a set to filter out duplicates at the end.
Algorithm
- Recursively generate all permutations by swapping elements.
- At each recursion level, swap the current index with all possible indices.
- Once a permutation is complete, add it to a result list.
- After generating all permutations, convert the list to a set to remove duplicates.
from typing import List
def permuteUnique(nums: List[int]) -> List[List[int]]:
res = []
def backtrack(start=0):
if start == len(nums):
res.append(nums[:])
return
for i in range(start, len(nums)):
nums[start], nums[i] = nums[i], nums[start]
backtrack(start + 1)
nums[start], nums[i] = nums[i], nums[start]
backtrack()
# Remove duplicates by converting to set of tuples
unique_res = list(map(list, set(tuple(lst) for lst in res)))
return unique_res
# Driver code
if __name__ == '__main__':
print(permuteUnique([1,1,2]))def permuteUnique(nums: List[int]) -> List[List[int]]:Defines the function signature with type hints for clarity.def backtrack(start=0):Starts recursion from index 0 to generate permutations.if start == len(nums):Base case: when start reaches end, a permutation is complete.nums[start], nums[i] = nums[i], nums[start]Backtrack by swapping back to restore original state.backtrack(start + 1)Recurse to fix next element.unique_res = list(map(list, set(tuple(lst) for lst in res)))Remove duplicates by converting lists to tuples and using a set.import java.util.*;
public class Solution {
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
backtrack(nums, 0, res);
// Remove duplicates using a set of strings
Set<String> set = new HashSet<>();
for (List<Integer> perm : res) {
StringBuilder sb = new StringBuilder();
for (int num : perm) {
sb.append(num).append(",");
}
set.add(sb.toString());
}
List<List<Integer>> uniqueRes = new ArrayList<>();
for (String s : set) {
String[] parts = s.split(",");
List<Integer> list = new ArrayList<>();
for (String part : parts) {
if (!part.isEmpty()) {
list.add(Integer.parseInt(part));
}
}
uniqueRes.add(list);
}
return uniqueRes;
}
private void backtrack(int[] nums, int start, List<List<Integer>> res) {
if (start == nums.length) {
List<Integer> permutation = new ArrayList<>();
for (int num : nums) permutation.add(num);
res.add(permutation);
return;
}
for (int i = start; i < nums.length; i++) {
swap(nums, start, i);
backtrack(nums, start + 1, res);
swap(nums, start, i);
}
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
// Driver code
public static void main(String[] args) {
Solution sol = new Solution();
List<List<Integer>> result = sol.permuteUnique(new int[]{1,1,2});
System.out.println(result);
}
}public List<List<Integer>> permuteUnique(int[] nums)Function signature returning list of unique permutations.backtrack(nums, 0, res);Start recursive permutation generation from index 0.// Remove duplicates using a set of stringsConvert each permutation to a string and use a set to remove duplicates.Set<String> set = new HashSet<>();Initialize a set to store unique permutation strings.for (List<Integer> perm : res)Iterate over all generated permutations.StringBuilder sb = new StringBuilder();Build a string representation of the permutation.set.add(sb.toString());Add the string to the set to ensure uniqueness.List<List<Integer>> uniqueRes = new ArrayList<>();Prepare list to hold unique permutations.for (String s : set)Convert each unique string back to a list of integers.private void backtrack(int[] nums, int start, List<List<Integer>> res)Recursive helper to generate permutations by swapping.if (start == nums.length)Base case: permutation complete.swap(nums, start, i);Backtrack by swapping back to original.backtrack(nums, start + 1, res);Recurse to next position.private void swap(int[] nums, int i, int j)Helper method to swap two elements in array.// Driver codeMain method to test the solution.System.out.println(result);Print the list of unique permutations.#include <iostream>
#include <vector>
#include <set>
using namespace std;
class Solution {
public:
vector<vector<int>> permuteUnique(vector<int>& nums) {
vector<vector<int>> res;
backtrack(nums, 0, res);
set<vector<int>> unique_res(res.begin(), res.end());
return vector<vector<int>>(unique_res.begin(), unique_res.end());
}
void backtrack(vector<int>& nums, int start, vector<vector<int>>& res) {
if (start == nums.size()) {
res.push_back(nums);
return;
}
for (int i = start; i < nums.size(); i++) {
swap(nums[start], nums[i]);
backtrack(nums, start + 1, res);
swap(nums[start], nums[i]);
}
}
};
// Driver code
int main() {
Solution sol;
vector<int> nums = {1,1,2};
vector<vector<int>> result = sol.permuteUnique(nums);
for (auto &perm : result) {
for (int num : perm) cout << num << ' ';
cout << '\n';
}
return 0;
}vector<vector<int>> permuteUnique(vector<int>& nums)Function returns all unique permutations.backtrack(nums, 0, res);Start recursion from index 0.if (start == nums.size())Base case: permutation complete.swap(nums[start], nums[i]);Backtrack by swapping back.backtrack(nums, start + 1, res);Recurse to next position.set<vector<int>> unique_res(res.begin(), res.end());Remove duplicates by using a set.return vector<vector<int>>(unique_res.begin(), unique_res.end());Return unique permutations as vector.function permuteUnique(nums) {
const res = [];
function backtrack(start = 0) {
if (start === nums.length) {
res.push(nums.slice());
return;
}
for (let i = start; i < nums.length; i++) {
[nums[start], nums[i]] = [nums[i], nums[start]];
backtrack(start + 1);
[nums[start], nums[i]] = [nums[i], nums[start]];
}
}
backtrack();
// Remove duplicates
const unique = new Set(res.map(arr => arr.join(',')));
return Array.from(unique).map(str => str.split(',').map(Number));
}
// Driver code
console.log(permuteUnique([1,1,2]));function permuteUnique(nums) {Defines the main function.function backtrack(start = 0) {Recursive helper function starting at index 0.if (start === nums.length) {Base case: permutation complete.for (let i = start; i < nums.length; i++) {Loop to swap current index with all possible indices.[nums[start], nums[i]] = [nums[i], nums[start]];Backtrack by swapping back.backtrack(start + 1);Recurse to next position.const unique = new Set(res.map(arr => arr.join(',')));Remove duplicates by converting arrays to strings and using a set.O(n! * n) due to generating all permutations and copying arraysO(n! * n) for storing all permutations before deduplicationWe generate all permutations including duplicates, which is n! permutations. Each permutation is copied (O(n)) and stored. Deduplication uses extra space.
This approach is too slow for large inputs but is useful to understand the problem and recursion basics.
Intuition
Sort the array so duplicates are adjacent. Use a boolean array to track used elements. Skip elements that are the same as previous and not used to avoid duplicate permutations.
Algorithm
- Sort the input array to group duplicates together.
- Use a boolean array 'used' to track which elements are included in the current permutation.
- At each recursion level, iterate over nums; skip if used or if current equals previous and previous not used.
- Add the current number to the permutation, mark used, recurse, then backtrack.
from typing import List
def permuteUnique(nums: List[int]) -> List[List[int]]:
nums.sort()
res = []
used = [False] * len(nums)
def backtrack(path):
if len(path) == len(nums):
res.append(path[:])
return
for i in range(len(nums)):
if used[i]:
continue
if i > 0 and nums[i] == nums[i-1] and not used[i-1]:
continue
used[i] = True
path.append(nums[i])
backtrack(path)
path.pop()
used[i] = False
backtrack([])
return res
# Driver code
if __name__ == '__main__':
print(permuteUnique([1,1,2]))nums.sort()Sort input to bring duplicates together for easy skipping.used = [False] * len(nums)Track which elements are used in current permutation.if used[i]: continueSkip elements already included in current path.if i > 0 and nums[i] == nums[i-1] and not used[i-1]: continueSkip duplicates unless previous identical element is used.used[i] = TrueMark current element as used before recursion.path.append(nums[i])Add current element to permutation path.backtrack(path)Recurse to build permutation further.path.pop()Backtrack by removing last element.used[i] = FalseMark current element as unused after backtracking.import java.util.*;
public class Solution {
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(nums);
boolean[] used = new boolean[nums.length];
backtrack(nums, used, new ArrayList<>(), res);
return res;
}
private void backtrack(int[] nums, boolean[] used, List<Integer> path, List<List<Integer>> res) {
if (path.size() == nums.length) {
res.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
if (used[i]) continue;
if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) continue;
used[i] = true;
path.add(nums[i]);
backtrack(nums, used, path, res);
path.remove(path.size() - 1);
used[i] = false;
}
}
// Driver code
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.permuteUnique(new int[]{1,1,2}));
}
}Arrays.sort(nums);Sort input array to group duplicates.boolean[] used = new boolean[nums.length];Track usage of elements in current permutation.if (used[i]) continue;Skip elements already used in current path.if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) continue;Skip duplicates unless previous identical element is used.used[i] = true;Mark element as used before recursion.path.add(nums[i]);Add element to current permutation path.backtrack(nums, used, path, res);Recurse to build permutation.path.remove(path.size() - 1);Backtrack by removing last element.used[i] = false;Mark element as unused after backtracking.#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
vector<vector<int>> permuteUnique(vector<int>& nums) {
vector<vector<int>> res;
vector<int> path;
vector<bool> used(nums.size(), false);
sort(nums.begin(), nums.end());
backtrack(nums, used, path, res);
return res;
}
void backtrack(vector<int>& nums, vector<bool>& used, vector<int>& path, vector<vector<int>>& res) {
if (path.size() == nums.size()) {
res.push_back(path);
return;
}
for (int i = 0; i < nums.size(); i++) {
if (used[i]) continue;
if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) continue;
used[i] = true;
path.push_back(nums[i]);
backtrack(nums, used, path, res);
path.pop_back();
used[i] = false;
}
}
};
// Driver code
int main() {
Solution sol;
vector<int> nums = {1,1,2};
vector<vector<int>> result = sol.permuteUnique(nums);
for (auto &perm : result) {
for (int num : perm) cout << num << ' ';
cout << '\n';
}
return 0;
}sort(nums.begin(), nums.end());Sort input to group duplicates for skipping.vector<bool> used(nums.size(), false);Track which elements are used in current permutation.if (used[i]) continue;Skip elements already used in current path.if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) continue;Skip duplicates unless previous identical element is used.used[i] = true;Mark element as used before recursion.path.push_back(nums[i]);Add element to current permutation path.backtrack(nums, used, path, res);Recurse to build permutation.path.pop_back();Backtrack by removing last element.used[i] = false;Mark element as unused after backtracking.function permuteUnique(nums) {
nums.sort((a,b) => a - b);
const res = [];
const used = new Array(nums.length).fill(false);
function backtrack(path) {
if (path.length === nums.length) {
res.push(path.slice());
return;
}
for (let i = 0; i < nums.length; i++) {
if (used[i]) continue;
if (i > 0 && nums[i] === nums[i - 1] && !used[i - 1]) continue;
used[i] = true;
path.push(nums[i]);
backtrack(path);
path.pop();
used[i] = false;
}
}
backtrack([]);
return res;
}
// Driver code
console.log(permuteUnique([1,1,2]));nums.sort((a,b) => a - b);Sort input array to group duplicates.const used = new Array(nums.length).fill(false);Track usage of elements in current permutation.if (used[i]) continue;Skip elements already used in current path.if (i > 0 && nums[i] === nums[i - 1] && !used[i - 1]) continue;Skip duplicates unless previous identical element is used.used[i] = true;Mark element as used before recursion.path.push(nums[i]);Add element to current permutation path.backtrack(path);Recurse to build permutation.path.pop();Backtrack by removing last element.used[i] = false;Mark element as unused after backtracking.O(n! * n) due to pruning duplicates but still generating permutationsO(n) recursion stack + O(n! * n) for resultsSorting and skipping duplicates reduces redundant branches, but worst case remains factorial.
This approach balances clarity and efficiency, making it ideal for interviews.
Intuition
Sort the array and at each recursion level, skip swapping with duplicate elements that have already been fixed at this level to avoid duplicate permutations.
Algorithm
- Sort the input array to group duplicates.
- At each recursion level, use a set to track which elements have been fixed at this position.
- Iterate over indices from current start to end; skip if element already fixed at this level.
- Swap current index with chosen index, recurse, then swap back to backtrack.
from typing import List
def permuteUnique(nums: List[int]) -> List[List[int]]:
nums.sort()
res = []
def backtrack(start=0):
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]
backtrack()
return res
# Driver code
if __name__ == '__main__':
print(permuteUnique([1,1,2]))nums.sort()Sort input to group duplicates for easy skipping.def backtrack(start=0):Recursive function with current index to fix.if start == len(nums):Base case: permutation complete.seen = set()Track elements fixed at this recursion level to avoid duplicates.if nums[i] in seen: continueSkip duplicate elements at this level.seen.add(nums[i])Mark element as fixed at this level.nums[start], nums[i] = nums[i], nums[start]Backtrack by swapping back.backtrack(start + 1)Recurse to next position.import java.util.*;
public class Solution {
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(nums);
backtrack(nums, 0, res);
return res;
}
private void backtrack(int[] nums, int start, List<List<Integer>> res) {
if (start == nums.length) {
List<Integer> permutation = new ArrayList<>();
for (int num : nums) permutation.add(num);
res.add(permutation);
return;
}
Set<Integer> seen = new HashSet<>();
for (int i = start; i < nums.length; i++) {
if (seen.contains(nums[i])) continue;
seen.add(nums[i]);
swap(nums, start, i);
backtrack(nums, start + 1, res);
swap(nums, start, i);
}
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
// Driver code
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.permuteUnique(new int[]{1,1,2}));
}
}Arrays.sort(nums);Sort input to group duplicates.Set<Integer> seen = new HashSet<>();Track elements fixed at current recursion level.if (seen.contains(nums[i])) continue;Skip duplicate elements at this level.seen.add(nums[i]);Mark element as fixed at this level.swap(nums, start, i);Backtrack by swapping back.backtrack(nums, start + 1, res);Recurse to next position.#include <iostream>
#include <vector>
#include <algorithm>
#include <unordered_set>
using namespace std;
class Solution {
public:
vector<vector<int>> permuteUnique(vector<int>& nums) {
vector<vector<int>> res;
sort(nums.begin(), nums.end());
backtrack(nums, 0, res);
return res;
}
void backtrack(vector<int>& nums, int start, vector<vector<int>>& res) {
if (start == nums.size()) {
res.push_back(nums);
return;
}
unordered_set<int> seen;
for (int i = start; i < nums.size(); i++) {
if (seen.count(nums[i])) continue;
seen.insert(nums[i]);
swap(nums[start], nums[i]);
backtrack(nums, start + 1, res);
swap(nums[start], nums[i]);
}
}
};
// Driver code
int main() {
Solution sol;
vector<int> nums = {1,1,2};
vector<vector<int>> result = sol.permuteUnique(nums);
for (auto &perm : result) {
for (int num : perm) cout << num << ' ';
cout << '\n';
}
return 0;
}sort(nums.begin(), nums.end());Sort input to group duplicates.unordered_set<int> seen;Track elements fixed at current recursion level.if (seen.count(nums[i])) continue;Skip duplicate elements at this level.seen.insert(nums[i]);Mark element as fixed at this level.swap(nums[start], nums[i]);Backtrack by swapping back.backtrack(nums, start + 1, res);Recurse to next position.function permuteUnique(nums) {
nums.sort((a,b) => a - b);
const res = [];
function backtrack(start = 0) {
if (start === nums.length) {
res.push(nums.slice());
return;
}
const seen = new Set();
for (let i = start; i < nums.length; i++) {
if (seen.has(nums[i])) continue;
seen.add(nums[i]);
[nums[start], nums[i]] = [nums[i], nums[start]];
backtrack(start + 1);
[nums[start], nums[i]] = [nums[i], nums[start]];
}
}
backtrack();
return res;
}
// Driver code
console.log(permuteUnique([1,1,2]));nums.sort((a,b) => a - b);Sort input array to group duplicates.const seen = new Set();Track elements fixed at current recursion level.if (seen.has(nums[i])) continue;Skip duplicate elements at this level.seen.add(nums[i]);Mark element as fixed at this level.[nums[start], nums[i]] = [nums[i], nums[start]];Backtrack by swapping back.backtrack(start + 1);Recurse to next position.O(n! * n) with pruning duplicates earlyO(n) recursion stack + O(n! * n) for resultsUsing a set at each recursion level avoids duplicate swaps, pruning duplicate permutations early.
This is a preferred approach in interviews for its clarity and efficiency.
| Approach | Time | Space | Stack Risk | Reconstruct | Use In Interview |
|---|---|---|---|---|---|
| 1. Brute Force | O(n! * n) | O(n! * n) | Yes (deep recursion) | Yes | Mention only - never code |
| 2. Backtracking with Used Array and Skip | O(n! * n) | O(n! * n) | Yes | Yes | Code this for clarity and efficiency |
| 3. In-Place Swapping with Skip Set | O(n! * n) | O(n! * n) | Yes | Yes | Code this for optimal memory and speed |
How to Present
Step 1: Clarify input size and presence of duplicates.Step 2: Present brute force approach to generate all permutations and remove duplicates.Step 3: Improve by sorting and skipping duplicates using a used array.Step 4: Present in-place swapping with skip set for optimal memory and speed.Step 5: Discuss time and space complexity and edge cases.
Time Allocation
What the Interviewer Tests
Interviewer tests your understanding of backtracking, handling duplicates, pruning recursion, and coding clean, bug-free solutions.
Common Follow-ups
- What if input size is large? → Use iterative or heuristic methods.
- Can you generate permutations in lexicographical order? → Use next permutation algorithm.
When to Use
1) Need all permutations, 2) Input may have duplicates, 3) Output must be unique permutations, 4) Backtracking or recursion is natural approach
