LeetCode #47 — Medium

Permutations II

Generate all unique permutations of a collection that may contain duplicates — using backtracking with pruning.

Solve on LeetCode
The Problem

Problem Statement

Given a collection of numbers, nums, that might contain duplicates, return all possible unique permutations in any order.

Example 1:

Input: nums = [1,1,2]
Output:
[[1,1,2],
 [1,2,1],
 [2,1,1]]

Example 2:

Input: nums = [1,2,3]
Output: [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

Constraints:

  • 1 <= nums.length <= 8
  • -10 <= nums[i] <= 10
Patterns Used

Roadmap

  1. Brute Force — Generate Then Deduplicate
  2. The Core Insight — Sort + Skip Rule
  3. Walkthrough with [1,1,2]
  4. Edge Cases
  5. Full Annotated Code
  6. Interactive Demo
  7. Complexity Analysis
Step 01

Brute Force — Generate Then Deduplicate

The simplest approach: generate all permutations (including duplicates), collect them into a Set, and return the unique ones.

Example: nums = [1, 1, 2]

Standard permutation generates 3! = 6 results, but only 3 are unique:

ALL 6 PERMUTATIONS
[1, 1, 2]
[1, 2, 1]
[1, 1, 2] duplicate
[1, 2, 1] duplicate
[2, 1, 1]
[2, 1, 1] duplicate
UNIQUE RESULTS
[1, 1, 2]
[1, 2, 1]
[2, 1, 1]

This works but wastes time generating duplicates we'll throw away. For inputs with many duplicates, the waste becomes enormous.

💡

Can we avoid generating duplicates in the first place? Yes — by sorting the input and adding a simple skip rule, we prune duplicate branches at the source.

Step 02

The Core Insight — Sort + Skip Rule

The strategy has two parts: sort the array first, then during backtracking, skip duplicate values at the same tree level.

The Skip Rule

When building a permutation position by position, we use a used[] boolean array to track which elements are already placed. The pruning condition is:

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

This means: if the current value equals the previous one, and the previous one is not in use (meaning we already explored it at this level and backtracked), then skip — because choosing this element would produce the same subtree.

Why Does This Work?

nums[i] == nums[i-1] && used[i-1] = true
Previous duplicate is in the current path. This is fine — we're building a permutation that legitimately uses both copies.
nums[i] == nums[i-1] && used[i-1] = false
Previous duplicate was not picked — it was already explored and backtracked. Picking this one would produce the same subtree. SKIP.

Think of it this way: among identical values, we enforce that they must be used in order (left to right in the sorted array). If the previous copy wasn't used, we can't jump ahead to this copy — that would be out of order and produce a duplicate permutation.

Step 03

Walkthrough with [1, 1, 2]

Let's trace through the backtracking tree for nums = [1, 1, 2] (already sorted). We label the positions as indices 0, 1, 2 corresponding to values 1, 1, 2.

Input Array (sorted)

1
i=0
1
i=1
2
i=2

Decision Tree (pruned branches shown in red)

[ ]
pick first element
1 (i=0)
1 (i=1)
skip: nums[1]==nums[0]
&& !used[0]
2 (i=2)
pick second element
from path [1]
1 (i=1) 2 (i=2)
pick third
[1,1,2] [1,2,1]
from path [2]
1 (i=0) 1 (i=1)
pick third
[2,1,1] pruned

Final Result: 3 Unique Permutations

1
1
2
1
2
1
2
1
1
🎯

Notice: Without pruning, choosing i=1 first (value 1) would generate the exact same subtree as choosing i=0 first (also value 1). The skip rule catches this at level 1 and prunes the entire branch, saving exponential work.

Step 04

Edge Cases

nums = [1]
Single element: only one permutation [[1]].
nums = [1, 1, 1]
All identical: only one permutation [[1,1,1]]. The skip rule prunes everything except one path.
nums = [1, 2, 3]
All distinct: no pruning occurs, produces all 3! = 6 permutations. Equivalent to Permutations I.
nums = [3, 3, 0, 3]
Multiple duplicates: sorted to [0,3,3,3]. Heavy pruning results in only 4 unique permutations.
Step 05

Full Annotated Code

class Solution {
    public List<List<Integer>> permuteUnique(int[] nums) {
        Arrays.sort(nums);                     // Step 1: sort so duplicates are adjacent
        List<List<Integer>> result = new ArrayList<>();
        backtrack(nums, new boolean[nums.length], new ArrayList<>(), result);
        return result;
    }

    private void backtrack(int[] nums, boolean[] used,
                           List<Integer> path, List<List<Integer>> result) {

        if (path.size() == nums.length) {       // Base case: full permutation built
            result.add(new ArrayList<>(path));  // Add a COPY (path will be modified)
            return;
        }

        for (int i = 0; i < nums.length; i++) {
            if (used[i]) continue;              // Already in current path

            // SKIP RULE: same value as previous, and previous wasn't used
            // → we already explored this choice at this tree level
            if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) continue;

            used[i] = true;                   // Choose
            path.add(nums[i]);
            backtrack(nums, used, path, result); // Explore
            path.remove(path.size() - 1);      // Un-choose (backtrack)
            used[i] = false;
        }
    }
}
Step 06

Interactive Demo

Enter numbers (may contain duplicates) and step through the backtracking process to see which branches are explored and which are pruned.

Backtracking Visualizer


Press "Step" or "Run All" to begin.
Step 07

Complexity Analysis

Time
O(n × n!)
Space
O(n)

In the worst case (all distinct elements), we generate n! permutations, each requiring O(n) to copy, giving O(n! × n) time. When duplicates exist, the pruning reduces the unique permutation count to n! / (d1! × d2! × ...) where dk is the count of each distinct value. Space is O(n) for the recursion stack, used[] array, and current path (not counting the output).

Approach Breakdown

BRUTE FORCE
O(n! × n) time
O(n! × n) space

Generate all n! permutations via standard backtracking (n branches at depth 1, n-1 at depth 2, etc.), then insert each into a HashSet for deduplication. Hashing each permutation costs O(n), and the set stores up to n! entries, wasting both time and memory on duplicates.

OPTIMAL
O(n! × n) time
O(n) space

Sorting puts duplicates adjacent. The skip rule (nums[i]==nums[i-1] && !used[i-1]) prunes duplicate branches at each tree level before recursing, so only unique permutations are generated. The used[] array and recursion stack use O(n). Actual output count drops to n!/(d1!×d2!×...).

🎯

Sorting + skip-same-value prevents duplicates inline -- same worst-case time complexity, but avoids post-filtering entirely. For [1,1,1,1,1] (five 1s), brute force generates 120 permutations then deduplicates to 1. Our approach generates exactly 1 permutation directly -- the skip rule prunes 119 branches at the root level.

Coach Notes

Common Mistakes

Review these before coding to avoid predictable interview regressions.

Missing undo step on backtrack

Wrong move: Mutable state leaks between branches.

Usually fails on: Later branches inherit selections from earlier branches.

Fix: Always revert state changes immediately after recursive call.