Some simplifications (with a small performance improvement)
What I noticed first is that your recursive function has two terminating conditions: set.count == 0
and set.count == 0
. Why is that necessary?
Well, if we comment out the second terminating condition
//} else if set.count == 1 {
// return [set]
then it doesn't work anymore:
print(combinations(of: [1, 2, 3]))
// Output: []
But why is that? The reason is that adding the single-element set of the first element
allCombinations.insert([firstElement])
is done inside the loop which iterates of the combinations of the diminished set. If we move that statement before the loop then the code simplifies to
func combinations<T>(of set: Set<T>) -> Set<Set<T>> {
if set.isEmpty {
return []
}
var allCombinations: Set<Set<T>> = []
let (firstElement, diminishedSet) = set.removingFirst()
allCombinations.insert([firstElement])
for combinationOfDiminishedSet in combinations(of: diminishedSet) {
allCombinations.insert(
combinationOfDiminishedSet.union([firstElement])
)
allCombinations.insert(
combinationOfDiminishedSet
)
}
return allCombinations
}
This makes the code shorter, simpler, and a bit faster, since the single-element set is added only once, not repeatedly for each subset of the diminished set.
I also prefer isEmpty
to check for an empty collection. It makes no real difference for a Set
, but for general collection, count
can be a \$ O(N) \$ operation, where \$ N \$ is the number of elements in the collection.
In addition, I have omitted the else
statement because it is not necessary. That saves one level of indentation for the remaining larger code block.
Choosing the right data structure (and a large performance improvement)
You function returns a Set
of all combinations with elements from the given set. All those combinations are different, but each insert()
call has to check for duplicates. If we return an Array
instead then we can simply append()
each combination.
The same happens when building the combinations: in
combinationOfDiminishedSet.union([firstElement])
the firstElement
is distinct from all elements in combinationOfDiminishedSet
, but the union()
call still has to check for duplicates.
I would therefore suggest to return a nested array instead of a nested set.
Changing the input from a set to an array makes the code more flexible: The given elements need not conform to the Hashable
protocol anymore, and repeated elements are possible.
Actually it is even better to use an ArraySlice
as the input because then removing the first element becomes a \$ O(1) \$ operation. One can still have a wrapper function taking an Array
(or a Set
if you prefer).
This leads to the following implementation:
func combinations<T>(of elements: ArraySlice<T>) -> [[T]] {
guard let firstElement = elements.first else {
return []
}
var allCombinations: [[T]] = [[firstElement]]
let combinationsOfRemainingElements = combinations(of: elements.dropFirst())
for combination in combinationsOfRemainingElements {
allCombinations.append(combination + [firstElement])
}
allCombinations.append(contentsOf: combinationsOfRemainingElements)
return allCombinations
}
// Wrapper function taking an array:
func combinations<T>(of elements: [T]) -> [[T]] {
combinations(of: elements[...])
}
// Wrapper function taking a set:
func combinations<T>(of elements: Set<T>) -> [[T]] {
combinations(of: Array(elements))
}
Performance comparison
All tests were done on a MacBook Air (1.1 GHz Quad-Core Intel Core i5), with the code compiled as a Command Line application in “Release” configuration.
let mySet = Set(1...20)
let start = Date()
let c = combinations(of: mySet)
let end = Date()
print(c.count, end.timeIntervalSince(start))
Results:
Implementation |
Time |
Original code |
3.0 seconds |
First improvement |
2.5 seconds |
Second improvement |
0.2 seconds |
Reinvent the wheel?
The Swift Algorithms is “an open-source package of sequence and collection algorithms, along with their related types.”
It provides methods to compute the combinations of all sizes or of a given range of sizes from a collection:
for c in [1, 2, 3].combinations(ofCount: 1...3) {
print(c)
}
Output:
[1]
[2]
[3]
[1, 2]
[1, 3]
[2, 3]
[1, 2, 3]
Even if you want to implement your own code it may be instructive to look at the implementation from that library. It demonstrates how to return the combinations as a “lazy collection” – i.e. as a collection where each combination is computed (efficiently) only when accessed.