3
\$\begingroup\$

This work is motivated by Sum of natural numbers below threshold, where several Swift solutions to Project Euler #1 (with arbitrary upper bound) are provided.

One way to think of it is to

  • start with two sequences (the multiples of 3 and the multiples of 5),
  • merge these two sequences into a new sequence, in increasing order, without duplicates,
  • finally add all elements of the merge sequence.

This question is about reviewing a generic solution for the second part:

Given two sequences (whose elements are strictly increasing), create a new “merged” sequence containing the elements of both, in increasing order, without duplicates.

With that increasingMerge() function, the solution to the above problem could be computed as

let upperBound = 10_000
print(increasingMerge(of: stride(from: 3, to: upperBound, by: 3),
                      and: stride(from: 5, to: upperBound, by: 5))
    .reduce(0, +))

This is surely not the most efficient way to solve Project Euler #1 (which in fact can be solved in O(1) time and O(1) space), but that is not the goal. The goal is to provide universally applicable function to merge two increasing sequences.

There seems to be no such function in Swift Algorithms. The overall approach with a global function, which returns an instance of IncreasingMergeSequence (conforming to the Sequence protocol), is inspired by Chain.swift. The code uses O(1) additional space.

Any and all feedback is welcome (coding style, naming, efficiency, access control, ...)

public struct IncreasingMergeSequence<Base1: Sequence, Base2: Sequence>: Sequence
    where Base1.Element == Base2.Element, Base1.Element: Comparable
{
    public typealias Element = Base1.Element // The common element type
    
    private let base1: Base1
    private let base2: Base2

    fileprivate init(base1: Base1, base2: Base2) {
        self.base1 = base1
        self.base2 = base2
    }
    
    public struct IncreasingMergeIterator: IteratorProtocol {
        private var iterator1: Base1.Iterator
        private var iterator2: Base2.Iterator

        private var elem1: Element? = nil
        private var elem2: Element? = nil
        
        fileprivate init(iterator1: Base1.Iterator, iterator2: Base2.Iterator) {
            self.iterator1 = iterator1
            self.iterator2 = iterator2
            elem1 = self.iterator1.next()
            elem2 = self.iterator2.next()
        }
        
        public mutating func next() -> Element? {
            switch (elem1, elem2) {
            case (let x?, let y?):
                if x < y {
                    elem1 = iterator1.next()
                    return x
                } else if y < x {
                    elem2 = iterator2.next()
                    return y
                } else {
                    elem1 = iterator1.next()
                    elem2 = iterator2.next()
                    return x
                }
            case (let x?, nil):
                elem1 = iterator1.next()
                return x
            case (nil, let y?):
                elem2 = iterator2.next()
                return y
            case (nil, nil):
                return nil
            }
        }
    }

    public func makeIterator() -> IncreasingMergeIterator {
        IncreasingMergeIterator(iterator1: base1.makeIterator(), iterator2: base2.makeIterator())
     }
}

/// Returns a new sequence containing the elements of two sequences, in increasing order,
/// without duplicates.
/// - Attention: It is assumed that the elements of both sequences are in strictly increasing order.
/// - Parameters:
///   - s1: The first sequence.
///   - s2: The second sequence.
/// - Returns: A sequence of the elements of `s1` and `s2`, in increasing order, without duplicates.

public func increasingMerge<S1, S2>(of s1: S1, and s2: S2) -> IncreasingMergeSequence<S1, S2> {
    return IncreasingMergeSequence(base1: s1, base2: s2)
}
\$\endgroup\$
5
  • \$\begingroup\$ Not gonna write up a full answer because I think this is merely personal taste, but I would replace that if/else if/else ladder with case (let x?, let y?) where x < y, case (let x?, let y?) where x > y, and case (let x?, let y?) \$\endgroup\$ Commented Aug 25, 2022 at 16:32
  • \$\begingroup\$ @Alexander: Yes, I had thought about it, and my personal tasted decided against it :) I did not like the repetition of (let x?, let y?) in three cases. \$\endgroup\$ Commented Aug 25, 2022 at 16:35
  • \$\begingroup\$ Pattern matching is pretty expensive. On TIO, optional binding gives a 22% speed gain. Here it is on TIO: original 9ms, vs 7ms for the modified version). Mind you, TIO is still using Swift 4. The difference is more pronounced on SwiftFiddle which uses Swift 5.6.2, but code optimizations are most certainly off. Readability and extensibility are sacrificed. \$\endgroup\$ Commented Aug 29, 2022 at 0:09
  • \$\begingroup\$ Bearing in mind that this could potentially be included in the Swift algorithms module, is there a good reason to avoid inlining? I'd be curious to see if forcing inlining with @_transparent would have as much of a speed boost on your machine as on SwiftFiddle \$\endgroup\$ Commented Sep 4, 2022 at 0:52
  • \$\begingroup\$ @ielyamani: I do not have much knowledge about possible (dis)advantages of inlining code from external modules, and I haven't made myself familiar with attributes like @usableFromInline or @_transparent, therefore I cannot say much about it. \$\endgroup\$ Commented Sep 4, 2022 at 10:39

0

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.