Problem overview
subsets receives nums, an array of distinct integers, and returns every subset: every way to pick some of the numbers, from none of them (the empty subset) to all of them. Each subset is written in the order its numbers have in nums.
The order of the subsets is fixed too. The numbers are decided one at a time, and for each number skipping comes before taking. So every subset without nums[0] comes before every subset with it, and inside each group the same rule applies to nums[1], then nums[2], and so on. The empty subset is always first and all of nums is last.
For nums = [1, 2, 3] the result is:
[[], [3], [2], [2, 3], [1], [1, 3], [1, 2], [1, 2, 3]]
The first four subsets skip 1 and the last four take it. Among the first four, [] and [3] skip 2 while [2] and [2, 3] take it. For [3, -1] the result is [[], [-1], [3], [3, -1]]: the last subset keeps the order of nums, it isn't sorted.
Approach: decide about one number at a time
Every number has two options, skip or take, and the choices are independent, so an array of n numbers has 2^n subsets. A subset is just a string of n choices, like a binary string with 0 for skip and 1 for take. The required order is the order of those strings.
Backtracking generates them directly. Keep the numbers taken so far in a list current and walk through nums by index. At index i:
- Skip
nums[i]: continue with indexi + 1and leavecurrentalone. - Take
nums[i]: add it tocurrent, continue with indexi + 1, then remove it again.
When the index reaches the end of nums, every number has been skipped or taken, and current is a finished subset: save a copy. Because the skip branch runs before the take branch, the subsets come out in exactly the order the task wants. The removal at the end is the "back" in backtracking: the caller gets current back exactly as it was before the call.
Step by step
- Create
resultas an empty[[Int]], a list of subsets. - Create
currentas an empty[Int]: the subset being built. At the start nothing is taken. - Start the search with
backtrack(nums, 0, ¤t, &result). Index 0 is the first number to decide about, and the&lets the call changecurrentandresultthemselves. - Return
resultafter the call. The helper has added every subset to it. - Write the helper
backtrackinside the class. It takesnums, the indexi, andcurrentandresultasinoutparameters. - Base case: check
i == nums.count. Past the last index, nothing is left to decide. - Save and stop: inside that
if, appendcurrenttoresultandreturn. - Skip first: call
backtrackwithi + 1and leavecurrentas it is. This finds every subset that doesn't containnums[i]. - Take: append
nums[i]tocurrent. Numbers are added in index order, so each subset keeps the order ofnums. - Recurse again with
i + 1. This call finds every subset that containsnums[i]. - Undo the choice with
current.removeLast(), so the caller seescurrentunchanged.
The solution in Swift
class Solution {
func subsets(_ nums: [Int]) -> [[Int]] {
var result = [[Int]]()
var current = [Int]()
backtrack(nums, 0, ¤t, &result)
return result
}
func backtrack(_ nums: [Int], _ i: Int, _ current: inout [Int], _ result: inout [[Int]]) {
if i == nums.count {
result.append(current)
return
}
backtrack(nums, i + 1, ¤t, &result)
current.append(nums[i])
backtrack(nums, i + 1, ¤t, &result)
current.removeLast()
}
}
Swift details worth knowing:
inoutand&. Parameters are constants inside a function. Marking oneinoutlets the function change the caller's variable, and the caller writes&before the variable to say so. Withoutinout, callingappendonresultinside the helper is a compile error.- Arrays are values.
result.append(current)stores a copy ofcurrentas it is at that moment. Later changes tocurrentdon't touch the saved subset. Aftervar b = a; b.append(3),ais unchanged. - Variable declaration.
[[Int]]()and[Int]()create empty arrays of the stated element types.varis needed because they change. - Methods calling each other.
subsetscallsbacktrackwithout a prefix, and the order of the two methods in the class doesn't matter. removeLast()removes and returns the last element. Here the returned value isn't needed, and Swift accepts that for this method without a warning.
Complexity
Time: O(n · 2^n). There are 2^n subsets, and saving one copies up to n numbers. The recursion tree has about 2^(n+1) calls, which is smaller than the copying cost.
Space: O(n) extra, not counting the answer: current holds at most n numbers, and the recursion is n + 1 calls deep.
Edge cases and common mistakes
- One number.
[0]gives[[], [0]]: without it, then with it. The empty subset is always part of the answer. - Forgetting
removeLast(). Thencurrentkeeps growing across calls, so later subsets contain numbers that were supposed to be skipped. - Taking before skipping. Swapping the two branches gives all the right subsets in the wrong order, and the lesson's tests check the order.
- Saving at every call. Appending
currentonly wheni == nums.countgives each subset exactly once. Saving earlier produces duplicates. - Sorting the numbers. The subsets keep the order of
nums, so[3, -1]has the subset[3, -1], not[-1, 3]. - Counting. With 4 numbers there are 16 subsets and with 10 numbers there are 1,024, so the largest input in this lesson stays small.
Testing
[1, 2, 3]gives[[], [3], [2], [2, 3], [1], [1, 3], [1, 2], [1, 2, 3]].[0]gives[[], [0]].[3, -1]gives[[], [-1], [3], [3, -1]].[5, 6, 7, 8]gives 16 subsets.- The numbers 1 to 10 give 1,024 subsets.
