Framework Thinking - Neetcode 150 - Subsets With Duplicates
Here is solutions for Subsets II.
1. Understand the problem
-
You are given an integer array nums that may contain duplicates.
-
You must return all possible subsets (the power set) such that:
-
No duplicate subsets appear in the result.
-
Order of subsets does not matter.
-
Each subset is a list of integers.
-
2. Clarify constraints, asks 4 - 5 questions including edge cases.
- Size of input?
- Typically 0 ≤ len(nums) ≤ 10.
- Are duplicates allowed?
- Yes, this is the core difficulty.
- Does order inside a subset matter?
- No, [1,2] is the same as [2,1], but we usually keep sorted order.
- Can the input be empty?
- Yes → return [[]].
- A negative numbers allowed?
- Yes, but it doesn’t affect the logic.
3. Explore examples.
- Example 1:
Input: [1,2,2]
Output: [[], [1], [2], [1,2], [2,2], [1,2,2]]
- Example 2
Input: [0]
Output: [[], [0]]
- Example 3
Input: [2,2]
Output: [[], [2], [2,2]]
4. Brainstorm 2 - 3 solutions, naive solution first and optimize later. Explain the key idea of each solution.
4.1. Solution 1 — Naive (Generate All, Deduplicate with Set) - Time: O(2^n * n), Space: O(2^n)
-
Idea:
-
Generate all subsets like normal Subsets I.
-
Sort each subset.
-
Store in a set to remove duplicates.
-
-
Time: O(2^N * N)
-
Space: O(2^N)
4.2. Solution 2 — Optimized Backtracking with Pruning (Correct Interview Solution ✅) - Time: O(2^N), Space: O(N)
-
Key Idea:
- Sort the array first so duplicates are adjacent.
-
While backtracking:
-
If nums[i] == nums[i-1] and i > start, skip it.
-
This prevents duplicates at the same tree level.
-
✅ This avoids duplicates during generation, not after.
-
-
Time: O(2^N)
-
Space: O(N)
5. Implement solutions.
5.1. Brute Force - Time: O(2^N * N), Space: O(2^N)
class Solution:
def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
res = set()
def backtrack(i, subset):
if i == len(nums):
res.add(tuple(subset))
return
subset.append(nums[i])
backtrack(i + 1, subset)
subset.pop()
backtrack(i + 1, subset)
nums.sort()
backtrack(0, [])
return [list(s) for s in res]
-
Time: O(2^N * N)
-
Space: O(2^N)
5.2. Backtracking - I - Time: O(2^N * N), Space: O(2^N)
class Solution:
def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
res = []
nums.sort()
def backtrack(i, subset):
if i == len(nums):
res.append(subset[::])
return
# Step 1: Pick nums[i]
subset.append(nums[i])
backtrack(i + 1, subset)
subset.pop()
# Step 2: Skip nums[i]
while i + 1 < len(nums) and nums[i] == nums[i + 1]:
i += 1
backtrack(i + 1, subset)
backtrack(0, [])
return res
-
Time: O(2^N * N)
-
Space: O(2^N)
5.3. Backtracking - Pick like idea combination sum - II - Time: O(2^N * N), Space: O(N) extra space
class Solution:
def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
nums.sort()
res = []
def backtrack(i, subset):
res.append(subset[::])
for j in range(i, len(nums)):
if j > i and nums[j] == nums[j - 1]:
continue
subset.append(nums[j])
backtrack(j + 1, subset)
subset.pop()
backtrack(0, [])
return res
-
Time: O(2^N * N)
-
Space: O(2^N)
5.4. Iteration - Time: O(2^N * N), Space: O(1) extra space
- When a duplicate appears, only extend the subsets generated in the previous iteration to prevent duplicate subsets.
class Solution:
def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
nums.sort()
res = [[]]
prev_Idx = idx = 0
for i in range(len(nums)):
idx = prev_idx if i >= 1 and nums[i] == nums[i - 1] else 0
prev_idx = len(res)
for j in range(idx, prev_idx):
tmp = res[j].copy()
tmp.append(nums[i])
res.append(tmp)
return res
-
Time: O(2^N * N)
-
Space: O(2^N)
6. Dry run testcases.
- nums = [1,1,2,3] => Auto skip the second 1 if duplicates with first 1.