47. Permutations II
Problem Description
You are given a collection of numbers nums
that might contain duplicates. Your task is to return all possible unique permutations of these numbers. The permutations can be returned in any order.
A permutation is a rearrangement of all the elements in the collection. Since the input may contain duplicate numbers, you need to ensure that duplicate permutations are not included in your result.
For example:
- If
nums = [1,1,2]
, the unique permutations would be[[1,1,2], [1,2,1], [2,1,1]]
- Notice that even though there are two
1
s in the input, we don't include duplicate permutations like having[1,1,2]
appear twice in our result
The key challenge here is handling the duplicate elements properly to avoid generating the same permutation multiple times while still ensuring all unique permutations are generated.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- No: The problem involves generating permutations of an array, not traversing nodes and edges in a graph structure.
Need to solve for kth smallest/largest?
- No: We need to generate all unique permutations, not find a specific kth element.
Involves Linked Lists?
- No: The problem works with an array/list of numbers, not linked list data structures.
Does the problem have small constraints?
- Yes: Generating permutations has factorial time complexity (n!), which means the problem typically has small constraints. For n elements, there are at most n! permutations, which grows extremely fast (e.g., 10! = 3,628,800), so practical problems usually have n ≤ 10-12.
Brute force / Backtracking?
- Yes: This is the perfect fit! We need to systematically explore all possible arrangements of the numbers, making choices at each position, and backtracking when we've completed a permutation or need to try a different choice. The backtracking approach allows us to:
- Build permutations incrementally
- Skip duplicate permutations by careful pruning
- Explore all valid possibilities systematically
Conclusion: The flowchart correctly leads us to use a Backtracking approach. This makes sense because we need to generate all possible unique arrangements while handling duplicates, which requires systematic exploration with the ability to undo choices (backtrack) and try alternatives.
Intuition
When we need to generate all unique permutations with possible duplicates, we face two main challenges: generating all permutations and avoiding duplicates.
Let's think about how we build permutations manually. We pick one element for the first position, then pick another for the second position from the remaining elements, and so on. This naturally suggests a recursive approach where at each step, we decide which element to place at the current position.
The key insight for handling duplicates comes from recognizing that duplicate permutations arise when we place identical elements in the same position across different recursive branches. For example, if we have [1, 1, 2]
and we're filling the first position, placing the first 1
or the second 1
there would lead to the same set of permutations.
To avoid this, we can sort the array first, bringing duplicate elements together. Then, when choosing an element for a position, we skip over duplicates that haven't been used yet in the current path. The crucial observation is: if we haven't used nums[j-1]
(the previous duplicate), we shouldn't use nums[j]
either, because this would create the same permutation that would be generated when nums[j-1]
is chosen first.
The backtracking pattern fits perfectly here because:
- We need to explore all possible choices systematically
- After placing an element at position
i
, we recursively fill positioni+1
- We need to "undo" our choice (backtrack) to try other elements at the same position
- We maintain a
visited
array to track which elements are currently being used in our permutation path
By combining sorting with a careful duplicate-skipping condition (j > 0 && nums[j] == nums[j-1] && !vis[j-1])
, we ensure each unique permutation is generated exactly once. This condition says: "Skip this element if it's the same as the previous one and the previous one hasn't been used yet in the current permutation being built."
Learn more about Backtracking and Sorting patterns.
Solution Approach
The implementation uses sorting combined with backtracking to generate all unique permutations efficiently.
Step 1: Sort the array
We start by sorting nums
to group duplicate elements together. This is crucial for our duplicate-skipping logic to work correctly.
Step 2: Initialize data structures
ans
: Stores all unique permutationst
: A temporary array of sizen
to build the current permutationvis
: A boolean array to track which elements fromnums
are currently used in the permutation being built
Step 3: The DFS function
The core logic is in the dfs(i)
function, which fills the i
-th position of the current permutation:
-
Base case: When
i == n
, we've filled all positions. We add a copy of the current permutationt
to our answer list. -
Recursive case: For each position
i
, we try every elementnums[j]
wherej
ranges from0
ton-1
:-
Skip condition 1: If
vis[j]
isTrue
, elementnums[j]
is already used in the current permutation, so skip it. -
Skip condition 2: The critical duplicate-avoiding condition:
j > 0 and nums[j] == nums[j-1] and not vis[j-1]
This means: if the current element equals the previous element AND the previous element hasn't been used, skip the current element. This ensures we only use duplicates in a fixed order (always use the first occurrence before the second).
-
-
Make choice: If both conditions pass:
- Place
nums[j]
at positioni
:t[i] = nums[j]
- Mark it as used:
vis[j] = True
- Recursively fill the next position:
dfs(i + 1)
- Place
-
Backtrack: After exploring all permutations with
nums[j]
at positioni
:- Unmark the element:
vis[j] = False
- This allows
nums[j]
to be used in different positions in other permutation branches
- Unmark the element:
Why the duplicate-skipping works:
Consider nums = [1, 1, 2]
after sorting. When filling a position:
- If we haven't used the first
1
(at index 0), we shouldn't use the second1
(at index 1) - This forces us to always use duplicates in order (first
1
before second1
) - This ordering constraint eliminates duplicate permutations while ensuring all unique ones are generated
The algorithm systematically explores all valid placement choices, using backtracking to undo decisions and try alternatives, ultimately generating all unique permutations in O(n × n!)
time complexity.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through the algorithm with nums = [1, 1, 2]
.
Initial Setup:
- Sort the array:
nums = [1, 1, 2]
(already sorted) - Initialize:
t = [_, _, _]
,vis = [False, False, False]
,ans = []
Building the first permutation [1, 1, 2]:
Starting with dfs(0)
- filling position 0:
- Try
j=0
(nums[0]=1): Not visited, no duplicate issue → Choose itt = [1, _, _]
,vis = [True, False, False]
- Call
dfs(1)
- filling position 1:- Try
j=0
: Skip (already visited) - Try
j=1
(nums[1]=1): Not visited, butnums[1] == nums[0]
andvis[0] = True
→ Can use itt = [1, 1, _]
,vis = [True, True, False]
- Call
dfs(2)
- filling position 2:- Try
j=0
: Skip (visited) - Try
j=1
: Skip (visited) - Try
j=2
(nums[2]=2): Not visited → Choose itt = [1, 1, 2]
,vis = [True, True, True]
- Call
dfs(3)
: Base case reached → Add[1, 1, 2]
to ans - Backtrack:
vis = [True, True, False]
- Try
- Backtrack:
vis = [True, False, False]
- Try
j=2
(nums[2]=2): Not visited → Choose itt = [1, 2, _]
,vis = [True, False, True]
- Call
dfs(2)
- filling position 2:- Try
j=0
: Skip (visited) - Try
j=1
(nums[1]=1): Not visited,nums[1] == nums[0]
butvis[0] = True
→ Can use itt = [1, 2, 1]
,vis = [True, True, True]
- Call
dfs(3)
: Base case → Add[1, 2, 1]
to ans - Backtrack:
vis = [True, False, True]
- Try
- Backtrack:
vis = [True, False, False]
- Try
- Backtrack:
vis = [False, False, False]
Key moment - Duplicate skipping:
Continue with dfs(0)
, now trying j=1
:
- Try
j=1
(nums[1]=1): Not visited, BUTnums[1] == nums[0]
andvis[0] = False
- Skip this! This is the crucial duplicate-avoiding condition
- If we used the second 1 before the first 1, we'd generate the same permutations
Building the third permutation [2, 1, 1]:
Continue with dfs(0)
, trying j=2
:
- Try
j=2
(nums[2]=2): Not visited → Choose itt = [2, _, _]
,vis = [False, False, True]
- Call
dfs(1)
- filling position 1:- Try
j=0
(nums[0]=1): Not visited → Choose itt = [2, 1, _]
,vis = [True, False, True]
- Call
dfs(2)
:- Only
j=1
is available → Choose it t = [2, 1, 1]
,vis = [True, True, True]
- Base case → Add
[2, 1, 1]
to ans
- Only
- Try
Final Result: ans = [[1, 1, 2], [1, 2, 1], [2, 1, 1]]
The algorithm successfully generates all 3 unique permutations by:
- Using backtracking to systematically explore all valid placements
- Preventing duplicates by enforcing an order when using identical elements (always use the first duplicate before the second)
Solution Implementation
1class Solution:
2 def permuteUnique(self, nums: List[int]) -> List[List[int]]:
3 """
4 Generate all unique permutations of a list that may contain duplicates.
5
6 Args:
7 nums: List of integers that may contain duplicates
8
9 Returns:
10 List of all unique permutations
11 """
12 def backtrack(index: int) -> None:
13 """
14 Recursive helper function to build permutations.
15
16 Args:
17 index: Current position being filled in the permutation
18 """
19 # Base case: complete permutation formed
20 if index == length:
21 result.append(current_permutation[:])
22 return
23
24 # Try placing each unused number at the current position
25 for j in range(length):
26 # Skip if number is already used
27 if visited[j]:
28 continue
29
30 # Skip duplicate: if current number equals previous number
31 # and previous number hasn't been used yet in this branch
32 # This ensures we only use duplicates in order (left to right)
33 if j > 0 and nums[j] == nums[j - 1] and not visited[j - 1]:
34 continue
35
36 # Place number at current position
37 current_permutation[index] = nums[j]
38 visited[j] = True
39
40 # Recurse to fill next position
41 backtrack(index + 1)
42
43 # Backtrack: mark as unvisited for other branches
44 visited[j] = False
45
46 # Initialize variables
47 length = len(nums)
48 nums.sort() # Sort to group duplicates together
49 result = []
50 current_permutation = [0] * length # Temporary array to build permutations
51 visited = [False] * length # Track which elements are used
52
53 # Start the backtracking process
54 backtrack(0)
55
56 return result
57
1class Solution {
2 // Store all unique permutations
3 private List<List<Integer>> result = new ArrayList<>();
4 // Store current permutation being built
5 private List<Integer> currentPermutation = new ArrayList<>();
6 // Input array
7 private int[] numbers;
8 // Track which elements have been used in current permutation
9 private boolean[] used;
10
11 /**
12 * Generate all unique permutations of the given array that may contain duplicates
13 * @param nums - input array that may contain duplicate elements
14 * @return list of all unique permutations
15 */
16 public List<List<Integer>> permuteUnique(int[] nums) {
17 // Sort array to group duplicates together
18 Arrays.sort(nums);
19 this.numbers = nums;
20 this.used = new boolean[nums.length];
21
22 // Start backtracking from position 0
23 backtrack(0);
24 return result;
25 }
26
27 /**
28 * Recursive backtracking to build permutations
29 * @param currentLength - current length of the permutation being built
30 */
31 private void backtrack(int currentLength) {
32 // Base case: permutation is complete when its length equals array length
33 if (currentLength == numbers.length) {
34 result.add(new ArrayList<>(currentPermutation));
35 return;
36 }
37
38 // Try each element as the next element in permutation
39 for (int index = 0; index < numbers.length; index++) {
40 // Skip if element is already used
41 if (used[index]) {
42 continue;
43 }
44
45 // Skip duplicates: if current element equals previous element
46 // and previous element hasn't been used yet in this branch,
47 // skip to avoid duplicate permutations
48 if (index > 0 && numbers[index] == numbers[index - 1] && !used[index - 1]) {
49 continue;
50 }
51
52 // Choose current element
53 currentPermutation.add(numbers[index]);
54 used[index] = true;
55
56 // Explore with this choice
57 backtrack(currentLength + 1);
58
59 // Backtrack: undo the choice
60 used[index] = false;
61 currentPermutation.remove(currentPermutation.size() - 1);
62 }
63 }
64}
65
1class Solution {
2public:
3 vector<vector<int>> permuteUnique(vector<int>& nums) {
4 // Sort the input array to group duplicates together
5 sort(nums.begin(), nums.end());
6
7 int n = nums.size();
8 vector<vector<int>> result;
9 vector<int> currentPermutation(n);
10 vector<bool> visited(n, false);
11
12 // Define recursive function to generate permutations
13 function<void(int)> backtrack = [&](int position) {
14 // Base case: if we've filled all positions, add current permutation to result
15 if (position == n) {
16 result.push_back(currentPermutation);
17 return;
18 }
19
20 // Try placing each unused number at the current position
21 for (int i = 0; i < n; ++i) {
22 // Skip if number is already used
23 if (visited[i]) {
24 continue;
25 }
26
27 // Skip duplicates: if current number equals previous number
28 // and previous number hasn't been used yet in this branch
29 // This ensures we only use duplicates in the order they appear
30 if (i > 0 && nums[i] == nums[i - 1] && !visited[i - 1]) {
31 continue;
32 }
33
34 // Choose: place nums[i] at current position
35 currentPermutation[position] = nums[i];
36 visited[i] = true;
37
38 // Explore: recurse to fill next position
39 backtrack(position + 1);
40
41 // Unchoose: backtrack for next iteration
42 visited[i] = false;
43 }
44 };
45
46 // Start the backtracking process from position 0
47 backtrack(0);
48
49 return result;
50 }
51};
52
1/**
2 * Generates all unique permutations of an array that may contain duplicates
3 * @param nums - Array of numbers that may contain duplicate values
4 * @returns Array of all unique permutations
5 */
6function permuteUnique(nums: number[]): number[][] {
7 // Sort the array to group duplicate elements together
8 nums.sort((a, b) => a - b);
9
10 const arrayLength: number = nums.length;
11 const result: number[][] = [];
12 const currentPermutation: number[] = new Array(arrayLength);
13 const visited: boolean[] = new Array(arrayLength).fill(false);
14
15 /**
16 * Depth-first search to build permutations
17 * @param currentIndex - Current position being filled in the permutation
18 */
19 const buildPermutation = (currentIndex: number): void => {
20 // Base case: complete permutation formed
21 if (currentIndex === arrayLength) {
22 result.push([...currentPermutation]);
23 return;
24 }
25
26 // Try each number for the current position
27 for (let candidateIndex = 0; candidateIndex < arrayLength; candidateIndex++) {
28 // Skip if number is already used
29 if (visited[candidateIndex]) {
30 continue;
31 }
32
33 // Skip duplicates: if current number equals previous number
34 // and previous number hasn't been used yet in current branch
35 if (candidateIndex > 0 &&
36 nums[candidateIndex] === nums[candidateIndex - 1] &&
37 !visited[candidateIndex - 1]) {
38 continue;
39 }
40
41 // Place number at current position
42 currentPermutation[currentIndex] = nums[candidateIndex];
43 visited[candidateIndex] = true;
44
45 // Recursively build rest of the permutation
46 buildPermutation(currentIndex + 1);
47
48 // Backtrack: mark as unvisited for other branches
49 visited[candidateIndex] = false;
50 }
51 };
52
53 // Start building permutations from index 0
54 buildPermutation(0);
55
56 return result;
57}
58
Time and Space Complexity
Time Complexity: O(n! × n)
The algorithm generates all unique permutations using backtracking with pruning for duplicates. In the worst case where all elements are distinct, there are n!
permutations. For each permutation:
- The DFS traverses through
n
positions (depth of recursion tree) - At each position, it iterates through all
n
elements to find valid candidates - When a complete permutation is formed (
i == n
), it creates a copy of the current permutationt[:]
which takesO(n)
time
The pruning condition (j and nums[j] == nums[j - 1] and not vis[j - 1])
helps skip duplicate permutations, but doesn't change the worst-case complexity. Therefore, the total time complexity is O(n! × n)
.
Space Complexity: O(n)
The space complexity consists of:
t
array:O(n)
to store the current permutation being builtvis
array:O(n)
to track visited elements- Recursion call stack:
O(n)
maximum depth ans
array:O(n! × n)
to store all permutations, but this is typically not counted as auxiliary space since it's the required output
Excluding the space for the output, the auxiliary space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting to Sort the Input Array
One of the most common mistakes is forgetting to sort the input array before starting the backtracking process. Without sorting, duplicate elements are scattered throughout the array, making it impossible to reliably detect and skip duplicate permutations.
Wrong approach:
def permuteUnique(self, nums: List[int]) -> List[List[int]]:
# Missing nums.sort() here!
result = []
# ... rest of the code
Why it fails: Without sorting, the condition nums[j] == nums[j-1]
won't correctly identify all duplicates since identical elements might not be adjacent.
Solution: Always sort the array first:
nums.sort() # Critical step - group duplicates together
2. Incorrect Duplicate-Skipping Logic
Another frequent error is using the wrong condition to skip duplicates. A common mistake is:
Wrong condition:
if j > 0 and nums[j] == nums[j-1] and visited[j-1]: # Wrong! continue
Why it fails: This skips using the second duplicate when the first IS used, which is the opposite of what we want. This would generate duplicate permutations.
Correct condition:
if j > 0 and nums[j] == nums[j-1] and not visited[j-1]: # Correct! continue
The logic is: "Skip the current duplicate if the previous duplicate hasn't been used yet." This ensures duplicates are always used in order (first before second).
3. Creating References Instead of Copies
When adding a complete permutation to the result, forgetting to create a copy leads to all permutations in the result pointing to the same array:
Wrong approach:
if index == length: result.append(current_permutation) # Wrong - adds reference! return
Why it fails: Since current_permutation
is reused and modified throughout the recursion, all entries in result
will end up with the same final values.
Solution: Always append a copy:
if index == length: result.append(current_permutation[:]) # Correct - creates a copy return
4. Not Properly Backtracking the Visited State
Forgetting to reset the visited
array after exploring a branch prevents elements from being reused in other permutation branches:
Wrong approach:
visited[j] = True backtrack(index + 1) # Missing: visited[j] = False
Why it fails: Once an element is marked as visited, it remains unavailable for all subsequent branches, drastically reducing the number of permutations generated.
Solution: Always reset the state after recursion:
visited[j] = True backtrack(index + 1) visited[j] = False # Critical backtracking step
5. Using Wrong Index in Backtrack Function
Confusing the iteration index j
(which element from nums to use) with the position index index
(which position in the permutation to fill):
Wrong approach:
def backtrack(index):
for j in range(length):
current_permutation[j] = nums[j] # Wrong - should use index, not j
Solution: Keep the indices straight:
def backtrack(index):
for j in range(length):
current_permutation[index] = nums[j] # Correct - fill position 'index' with nums[j]
These pitfalls can lead to incorrect results, infinite recursion, or missing permutations. Understanding the role of sorting and the duplicate-skipping logic is crucial for this algorithm to work correctly.
How many times is a tree node visited in a depth first search?
Recommended Readings
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!