This is follow-up on On Knuth's "Algorithm L" to generate permutations in lexicographic order, where I presented the following method to enumerate all permutations of array elements in lexicographic order:
extension Array where Element: Comparable {
/// Replaces the array by the next permutation of its elements in lexicographic
/// order.
///
/// It uses the "Algorithm L (Lexicographic permutation generation)" from
/// Donald E. Knuth, "GENERATING ALL PERMUTATIONS"
/// http://www-cs-faculty.stanford.edu/~uno/fasc2b.ps.gz
///
/// - Returns: `true` if there was a next permutation, and `false` otherwise
/// (i.e. if the array elements were in descending order).
mutating func permute() -> Bool {
// Nothing to do for empty or single-element arrays:
if count <= 1 {
return false
}
// L2: Find last j such that self[j] < self[j+1]. Terminate if no such j
// exists.
var j = count - 2
while j >= 0 && self[j] >= self[j+1] {
j -= 1
}
if j == -1 {
return false
}
// L3: Find last l such that self[j] < self[l], then exchange elements j and l:
var l = count - 1
while self[j] >= self[l] {
l -= 1
}
swap(&self[j], &self[l])
// L4: Reverse elements j+1 ... count-1:
var lo = j + 1
var hi = count - 1
while lo < hi {
swap(&self[lo], &self[hi])
lo += 1
hi -= 1
}
return true
}
}
This question is about using that method to create a Swift Sequence
with all permutations, so that one can
simply enumerate these with for .. in
loops and related techniques. My first approach is to use
the "type erasers" AnySequence
and AnyIterator
, which leads to this simple
implementation:
func permutations<T: Comparable>(of a: [T]) -> AnySequence<[T]> {
var current = a.sorted()
var done = false
return AnySequence {
return AnyIterator {
if done { return nil }
defer { done = !current.permute() }
return current
}
}
}
Example usage:
for p in permutations(of: [3, 2, 1]) {
print(p)
}
let allPerms = Array(permutations(of: ["B", "A", "C"]))
print(allPerms)
So this is easy to use, but what about performance? Here my benchmark code (on a 1.2 GHz Intel Core m5 MacBook, compiled in Release mode):
let N = 10
var count = 0
let start = Date()
for _ in permutations(of: Array(1...N)) { count += 1 }
let end = Date()
print(count, end.timeIntervalSince(start))
This takes about 1.3 seconds, i.e. it is much slower than calling permute()
on a mutable
array in a loop, which takes only 0.03 seconds (see On Knuth's "Algorithm L" to generate permutations in lexicographic order).
It gets a bit better if we implement our own sequence type instead of using the type erasers:
struct PermutationSequence<T: Comparable>: Sequence, IteratorProtocol {
private var current: [T]
private var done: Bool
init(elements: [T]) {
self.current = elements.sorted()
self.done = false
}
func makeIterator() -> PermutationSequence {
return self
}
mutating func next() -> [T]? {
if done { return nil }
let retval = current
done = !current.permute()
return retval
}
}
Benchmark:
let N = 10
var count = 0
let start = Date()
for _ in PermutationSequence(elements: Array(1...N)) { count += 1 }
let end = Date()
print(count, end.timeIntervalSince(start))
which takes about 0.8 seconds.
All feedback is welcome. In particular I am interested in making Sequence
-based
enumeration of all permutations as fast as possible.
next()
inPermutationSequence
will naturally induce, on each call, COW ofretval
when callingpermute
oncurrent
, but is it necessary to keep an extra copy of "next" state? (I.e., preparingcurrent
to be that copy for nextnext()
call). As an alternative, we could letnext()
returncurrent
, and leave the choice of inducing COW to the caller, if necessary. E.g. implemented as avoiding permutation ofcurrent
on first call tonext()
, and thereafter permuting and returning it (giventrue
-return frompermute()
) for each subsequent call tonext()
. \$\endgroup\$permute()
returnArray?
rather thanBool
(overhead only for COW on return?). On another note: interestingly, the 0.8s benchmark of the iteration over thePermutationSequence
instance is comparable to the immutable implementation ofpermute()
in the other thread, where, for the most part, the main difference is the extra copy on each call topermute()
. Also, sorry for not benchmarking myself, but currently no where near my Mac, so can't test this compiled in release mode ><. \$\endgroup\$makeIterator()
implementation in yourPermutationSequence
that just returnsself
, as there's already a default implementation for that :) \$\endgroup\$shouldContinue
a constant makes it a little bit faster. \$\endgroup\$