Every element is either in a subset or out of it, so a size-n array has 2^n subsets. The two approaches below differ in how they enumerate them.
Bitmask Enumeration
Time O(n Β· 2^n)Space O(n Β· 2^n)Every subset corresponds to one of the 2^n binary numbers from 0 to 2^n - 1: bit i set means nums[i] is included.
class Solution: def subsets(self, nums: list[int]) -> list[list[int]]: n = len(nums) result = [] for mask in range(1 << n): result.append([nums[i] for i in range(n) if mask & (1 << i)]) return resultSimple and iterative, but it is really the same brute force as backtracking β it just encodes the same βin or outβ decisions as bits instead of recursive calls. 2^n masks, each built in O(n), giving O(n Β· 2^n) time and space for the output.
Backtracking (Include/Exclude via a Start Index)
OptimalTime O(n Β· 2^n)Space O(n)Treat the current path as a subset that is already valid on its own β add it to the result immediately, then extend it by trying every element after start. Each recursive call decides βadd this element, recurse, then remove itβ (backtrack) before moving to the next candidate.
class Solution: def subsets(self, nums: list[int]) -> list[list[int]]: result = [] path = []
def backtrack(start): result.append(path[:]) for i in range(start, len(nums)): path.append(nums[i]) backtrack(i + 1) path.pop()
backtrack(0) return resultTracing nums = [1,2,3] as a decision tree β every node visited is itself a complete subset, recorded the instant we arrive:
Start with the empty path. It is a valid subset on its own β record it immediately.
Complexity: the recursion tree has exactly 2^n nodes (one per subset), each doing O(n) work to copy path into the result β O(n Β· 2^n) time. The path list and recursion depth are bounded by n β O(n) auxiliary space (excluding the output itself).