Problem overview
The function subsets(nums []int) [][]int receives a slice of distinct integers and returns every subset: every way to pick some of the numbers, from none of them (the empty subset) up to all of them. Each subset keeps the order its numbers have in nums.
The order of the answer is fixed too. The search decides about the numbers one at a time, skipping before taking. Every subset without nums[0] comes before every subset with it. Inside each of those two groups, the subsets without nums[1] come first, and so on. The empty subset is always first and all of nums is last.
For nums = [1, 2, 3] the answer 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. For [3, -1] the answer is [[], [-1], [3], [3, -1]]: the last subset is written in the order of nums, not sorted.
Approach: skip or take, one number at a time
A subset is a series of yes/no decisions, one per number. With n numbers there are 2^n series, so there are 2^n subsets: 8 for three numbers, 1,024 for ten. The answer is that big whatever the method, so the goal is to generate each subset exactly once with little extra work.
Backtracking does it with a recursive function that handles one index i at a time and keeps the numbers taken so far in a slice called current:
- At index
i, first skipnums[i]: recurse oni + 1withcurrentunchanged. - Then take it: add it to
current, recurse oni + 1, and remove it again. - When
ireacheslen(nums), every number has been decided:currentis a finished subset, and a copy of it goes in the result.
Trying skip before take produces exactly the order the statement describes. It is the same order as counting in binary with 0 for skip and 1 for take: 000, 001, 010, and so on.
Taking, recursing and then removing is the "back" in backtracking. Each call gives back what it took, so the caller finds current as it left it.
Step by step
- Create
resultas an empty[][]int, written[][]int{}. Each element is a subset, so it holds slices. - Start the search with
backtrack(nums, 0, []int{}, &result). Nothing is decided yet, so the index is 0 and the subset so far is empty. The last argument is the address ofresult:appendcan return a new slice, and the helper has to store it wheresubsetscan see it. - Return
resultafter the call. - Declare the helper
backtrack(nums []int, i int, current []int, result *[][]int). It returns nothing. The type*[][]intis a pointer to a slice of slices. - Base case. If
i == len(nums), every number has been skipped or taken. - Save a copy.
*result = append(*result, append([]int{}, current...))appends a fresh copy ofcurrentto the slice the pointer points to, thenreturn. - Skip. Call
backtrack(nums, i+1, current, result)without touchingcurrent. Doing this first puts all subsets withoutnums[i]before those with it. - Take. Append
nums[i]tocurrentand store the result back incurrent. - Recurse again with the same arguments. Now
currentholdsnums[i], so this call finds every subset that contains it. - Undo. Set
currenttocurrent[:len(current)-1], the slice without its last element.
The solution in Go
func subsets(nums []int) [][]int {
result := [][]int{}
backtrack(nums, 0, []int{}, &result)
return result
}
func backtrack(nums []int, i int, current []int, result *[][]int) {
if i == len(nums) {
*result = append(*result, append([]int{}, current...))
return
}
backtrack(nums, i+1, current, result)
current = append(current, nums[i])
backtrack(nums, i+1, current, result)
current = current[:len(current)-1]
}
The Go details that matter here:
- Why a pointer to
result. Go passes slices by value: the callee gets a copy of the slice header (pointer, length, capacity). Whenappendgrows the slice, the new header exists only in the variable that received it. Passing*[][]intletsbacktrackwrite the grown slice through*result, andsubsetssees it. - Why copy
current. Slices that come fromappendcan share one backing array, andcurrentkeeps changing as the search goes on.append([]int{}, current...)builds a new slice with its own array, so a saved subset never changes later. Without the copy, the results for[1, 2, 3]happen to look right, but for[1, 2, 3, 4, 5]the output lists[1 2 3 4]twice and has no[1 2 3 5]. - The
current...spread. It passes the elements of a slice as separate arguments to the variadicappend. - The last line is a habit more than a need.
currentis a parameter, so the caller's slice header keeps its old length whatever this call does. Trimming it is still the natural way to write "give back what you took", and it keeps the code correct if the structure changes. - Slice expressions.
current[:len(current)-1]takes the elements from index 0 up to, but not including, the last one. It doesn't copy anything.
Complexity
Time: O(n · 2^n). The recursion has 2^n leaves, one per subset, and about 2^(n+1) calls in all. Each leaf copies a subset of up to n numbers, which costs up to n steps. For n = 10 that is 1,024 subsets.
Space: O(n) besides the answer. The recursion is n + 1 calls deep, and current holds at most n numbers. The answer itself needs O(n · 2^n), but that is the output, not extra work.
Edge cases and common mistakes
- One number.
[0]gives[[], [0]]: without it, then with it. - The empty subset is part of the answer. It is always first. Forgetting it, or starting the result with only the single-number subsets, loses it.
- Saving
currentitself. Appendingcurrentwithout copying stores a slice that later calls can overwrite. It can pass on small inputs, which makes it a hard bug to see. - Taking before skipping. That gives the same subsets in a different order, and the lesson's tests expect skip-first.
- Sorting each subset. The numbers of a subset stay in the order of
nums: for[3, -1]the last subset is[3, -1], not[-1, 3]. Adding numbers in index order gives that for free. - Forgetting the pointer. If
resultwere passed by value, the appended subsets would be lost whenbacktrackreturns, andsubsetswould return an empty slice. - Wrong base case. Stopping at
len(nums) - 1misses the decision about the last number.
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]].[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]gives 1,024 subsets.
