title: Permutations II slug: permutations-ii difficulty: medium leetcode_id: 47 leetcode_url: https://leetcode.com/problems/permutations-ii/ categories: - arrays - sorting - recursion patterns: - backtracking description: | Given a collection of numbers, `nums`, that might contain duplicates, return *all possible unique permutations **in any order***. constraints: | - `1 <= nums.length <= 8` - `-10 <= nums[i] <= 10` examples: - input: "nums = [1,1,2]" output: "[[1,1,2], [1,2,1], [2,1,1]]" explanation: "The array contains a duplicate 1. We generate all unique permutations, avoiding duplicates like having two identical [1,1,2] arrangements." - input: "nums = [1,2,3]" output: "[[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]]" explanation: "With all unique elements, we get the standard 3! = 6 permutations." explanation: intuition: | Imagine you have a set of letter tiles, some of which are identical. You want to arrange them in every possible order, but you don't want to count the same arrangement twice just because you swapped two identical tiles. The core insight is that **sorting brings duplicates together**, making them easy to detect and skip. Think of it like organising your tiles alphabetically before arranging them — when you see two identical tiles next to each other, you know to use one and skip the other in the same position. The classic permutation approach uses backtracking: at each position, try placing each available element, then recursively fill the remaining positions. The challenge with duplicates is avoiding repeated work. If we've already tried placing a `1` at position 0, we shouldn't try placing the *other* `1` at position 0 — it would produce the same permutations. By sorting first and then skipping duplicates that would lead to repeated branches, we prune the search tree efficiently. We only use a duplicate element if we've already used the one before it in our current path. approach: | We solve this using **Backtracking with Duplicate Skipping**: **Step 1: Sort the input array** - Sorting groups duplicates together: `[1, 2, 1]` becomes `[1, 1, 2]` - This makes it easy to detect when we're about to make the same choice twice   **Step 2: Set up backtracking state** - `result`: List to store all unique permutations - `current`: The permutation being built - `used`: Boolean array tracking which elements are already in `current`   **Step 3: Define the backtracking function** - **Base case**: If `current` has `n` elements, we found a complete permutation — add a copy to `result` - **Recursive case**: For each index `i` in `nums`: - Skip if `used[i]` is `True` (element already in current permutation) - Skip if `nums[i] == nums[i-1]` AND `used[i-1]` is `False` (duplicate pruning) - Otherwise: mark `used[i] = True`, add `nums[i]` to `current`, recurse, then backtrack   **Step 4: The duplicate pruning logic** - The condition `nums[i] == nums[i-1] and not used[i-1]` means: "this element equals its predecessor, and the predecessor isn't currently in our path" - This ensures we only use the *first* occurrence of a duplicate value at each decision point - If the predecessor IS used, we're building a permutation that legitimately needs both duplicates   **Step 5: Return all collected permutations** - After backtracking completes, `result` contains all unique permutations common_pitfalls: - title: Forgetting to Sort description: | The duplicate-skipping logic relies on duplicates being adjacent. Without sorting, `[1, 2, 1]` won't have the duplicate `1`s next to each other, and the skip condition `nums[i] == nums[i-1]` won't work. Always sort first: `nums.sort()` before starting backtracking. wrong_approach: "Skip duplicates without sorting" correct_approach: "Sort first, then skip adjacent duplicates" - title: Wrong Duplicate Skip Condition description: | A common mistake is checking `nums[i] == nums[i-1]` without the `not used[i-1]` part. This would skip valid permutations that legitimately use both duplicate values. For `[1, 1, 2]`, we *need* both `1`s in the permutation. The condition `not used[i-1]` ensures we only skip when the duplicate would create a *redundant branch*, not when we're building a permutation that requires both. wrong_approach: "Skip whenever nums[i] == nums[i-1]" correct_approach: "Skip only when nums[i] == nums[i-1] AND used[i-1] is False" - title: Using a Set for Deduplication description: | You might think "just use a set to store results and remove duplicates". While this works, it's inefficient: - Converting lists to tuples for set storage has overhead - You generate duplicate permutations only to discard them - With many duplicates, you waste significant computation The sorting + skip approach prunes duplicates *before* generating them, which is far more efficient. wrong_approach: "Generate all permutations, deduplicate with a set" correct_approach: "Prune duplicate branches during backtracking" - title: Forgetting to Copy the Current Permutation description: | When adding to results, use `result.append(current[:])` or `result.append(list(current))`, not `result.append(current)`. The `current` list is mutated during backtracking. If you append the reference directly, all entries in `result` will point to the same (eventually empty) list. key_takeaways: - "**Sorting enables efficient duplicate handling**: Bringing duplicates together lets you detect and skip them with a simple adjacent comparison" - "**The skip condition has two parts**: `nums[i] == nums[i-1]` detects duplicates, but `not used[i-1]` ensures we only skip redundant branches, not legitimate uses of both duplicates" - "**Prune early, not late**: Avoiding duplicate work during generation is more efficient than deduplicating results afterward" - "**Backtracking template**: This pattern (choose, explore, unchoose) with constraint checking applies to many combinatorial problems like N-Queens, Sudoku, and subset generation" time_complexity: "O(n! * n). In the worst case (all unique elements), we generate n! permutations, each taking O(n) time to copy. With duplicates, the actual count is lower: n! / (k1! * k2! * ...) where ki is the count of each duplicate value." space_complexity: "O(n). The recursion depth is at most n, and we use a `used` array of size n. The output space for storing permutations is O(n! * n) but is typically not counted as auxiliary space." solutions: - approach_name: Backtracking with Duplicate Skipping is_optimal: true code: | def permute_unique(nums: list[int]) -> list[list[int]]: result = [] nums.sort() # Sort to bring duplicates together used = [False] * len(nums) def backtrack(current: list[int]) -> None: # Base case: found a complete permutation if len(current) == len(nums): result.append(current[:]) # Append a copy return for i in range(len(nums)): # Skip if already used in current permutation if used[i]: continue # Skip duplicate: same value as previous, but previous not used # This means we'd be starting a redundant branch if i > 0 and nums[i] == nums[i - 1] and not used[i - 1]: continue # Choose: add nums[i] to current permutation used[i] = True current.append(nums[i]) # Explore: recurse to fill remaining positions backtrack(current) # Unchoose: backtrack to try other options current.pop() used[i] = False backtrack([]) return result explanation: | **Time Complexity:** O(n! * n) — We generate up to n! permutations (fewer with duplicates), each requiring O(n) to copy. **Space Complexity:** O(n) — Recursion stack depth is n, plus the `used` array of size n. The key optimisation is the duplicate skip condition on line 17. By sorting first, duplicates are adjacent. When we encounter a duplicate that's the same as its unused predecessor, we skip it — the predecessor will handle all permutations starting with that value. - approach_name: Backtracking with Counter is_optimal: true code: | from collections import Counter def permute_unique(nums: list[int]) -> list[list[int]]: result = [] counter = Counter(nums) # Count occurrences of each number def backtrack(current: list[int]) -> None: # Base case: permutation complete if len(current) == len(nums): result.append(current[:]) return # Try each unique number that still has remaining count for num in counter: if counter[num] > 0: # Choose: use one instance of this number current.append(num) counter[num] -= 1 # Explore backtrack(current) # Unchoose: restore count current.pop() counter[num] += 1 backtrack([]) return result explanation: | **Time Complexity:** O(n! * n) — Same as the sorting approach. **Space Complexity:** O(n) — Counter uses O(k) space where k is the number of unique elements, plus O(n) recursion depth. This alternative approach uses a Counter to track available elements. By iterating over unique keys rather than indices, we naturally avoid considering the same value twice at the same decision point. Each unique value is tried exactly once per recursive level. - approach_name: Brute Force with Set Deduplication is_optimal: false code: | def permute_unique(nums: list[int]) -> list[list[int]]: result_set = set() def backtrack(current: list[int], remaining: list[int]) -> None: if not remaining: # Convert to tuple for set storage result_set.add(tuple(current)) return for i in range(len(remaining)): # Choose element at index i backtrack( current + [remaining[i]], remaining[:i] + remaining[i + 1:] ) backtrack([], nums) # Convert tuples back to lists return [list(perm) for perm in result_set] explanation: | **Time Complexity:** O(n! * n) — Generates all n! permutations regardless of duplicates, plus set operations. **Space Complexity:** O(n! * n) — Stores all permutations as tuples in a set. This naive approach generates all permutations and relies on set deduplication. While correct, it wastes computation by generating duplicate permutations that are later discarded. For input like `[1,1,1,1,1,1,1,1]` (8 identical elements), this generates 8! = 40,320 permutations but keeps only 1. The optimised approaches generate only the unique ones.