Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a merge algorithm for combining sorted sequences #193

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions Guides/Merge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Merge

[[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Merge.swift) |
[Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/MergeTests.swift)]


## Detailed Design


### Complexity


### Comparison with other languages

**C++:** The `<algorithm>` library [defines a `merge` function](https://cplusplus.com/reference/algorithm/merge/) …
247 changes: 247 additions & 0 deletions Sources/Algorithms/Merge.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Algorithms open source project
//
// Copyright (c) 2022 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
//
//===----------------------------------------------------------------------===//

/// A merge of two sequences with the same element type both pre-sorted by the
/// same predicate.
public struct Merge2Sequence<Base1: Sequence, Base2: Sequence>
where Base1.Element == Base2.Element
{
/// The first sequence in this merged sequence
@usableFromInline
internal let base1: Base1

/// The second sequence in this merged sequence
@usableFromInline
internal let base2: Base2

/// A predicate that returns `true` if its first argument should be ordered
/// before its second argument; otherwise, `false`.
@usableFromInline
internal let areInIncreasingOrder: (Base2.Element, Base1.Element) -> Bool

@inlinable
internal init(
base1: Base1,
base2: Base2,
areInIncreasingOrder: @escaping (Base2.Element, Base1.Element) -> Bool
) {
self.base1 = base1
self.base2 = base2
self.areInIncreasingOrder = areInIncreasingOrder
}
}

extension Merge2Sequence: Sequence {
/// The iterator for a `Merge2Sequence` instance
public struct Iterator: IteratorProtocol {
@usableFromInline
internal var iterator1: Base1.Iterator

@usableFromInline
internal var iterator2: Base2.Iterator

@usableFromInline
internal let areInIncreasingOrder: (Base2.Element, Base1.Element) -> Bool

@usableFromInline
internal enum IterationState {
case iterating
case finished1
case finished2
case finished
}

@usableFromInline
internal var iterationState: IterationState = .iterating

@usableFromInline
internal var previousElement1: Base1.Element? = nil

@usableFromInline
internal var previousElement2: Base2.Element? = nil

@inlinable
internal init(_ mergedSequence: Merge2Sequence) {
iterator1 = mergedSequence.base1.makeIterator()
iterator2 = mergedSequence.base2.makeIterator()
areInIncreasingOrder = mergedSequence.areInIncreasingOrder
}

@inlinable
public mutating func next() -> Base1.Element? {
switch iterationState {
case .iterating:
switch (previousElement1 ?? iterator1.next(), previousElement2 ?? iterator2.next()) {
case (.some(let element1), .some(let element2)):
if areInIncreasingOrder(element2, element1) {
previousElement1 = element1
previousElement2 = nil
return element2
} else {
previousElement1 = nil
previousElement2 = element2
return element1
}

case (nil, .some(let element2)):
iterationState = .finished1
return element2

case (.some(let element1), nil):
iterationState = .finished2
return element1

case (nil, nil):
iterationState = .finished
return nil
}

case .finished1:
let element = iterator2.next()
if element == nil {
iterationState = .finished
}
return element

case .finished2:
let element = iterator1.next()
if element == nil {
iterationState = .finished
}
return element

case .finished:
return nil
}
}
}

@inlinable
public func makeIterator() -> Iterator {
Iterator(self)
}

@inlinable
public var underestimatedCount: Int {
return base1.underestimatedCount + base2.underestimatedCount
}
}

extension Merge2Sequence where Base1: Collection, Base2: Collection {
@inlinable
public var count: Int {
return base1.count + base2.count
}

@inlinable
public var isEmpty: Bool {
return base1.isEmpty && base2.isEmpty
}
}

//===----------------------------------------------------------------------===//
// merge(_:_:areInIncreasingOrder:)
//===----------------------------------------------------------------------===//

/// Returns a new sequence that iterates over the two given sequences,
/// alternating between elements of the two sequences, returning the lesser of
/// the two elements, as defined by a predicate, `areInIncreasingOrder`.
///
/// You can pass any two sequences or collections that have the same element
/// type as this sequence and are pre-sorted by the given predicate. This
/// example merges two sequences of `Int`s:
///
/// let evens = stride(from: 0, to: 10, by: 2)
/// let odds = stride(from: 1, to: 10, by: 2)
/// for num in merge(evens, odds, by: <) {
/// print(num)
/// }
/// // 0
/// // 1
/// // 2
/// // 3
/// // 4
/// // 5
/// // 6
/// // 7
/// // 8
/// // 9
///
/// - Parameters:
/// - s1: The first sequence.
/// - s2: The second sequence.
/// - areInIncreasingOrder: A closure that takes an element of `s2` and `s1`,
/// respectively, and returns whether the first element should appear before
/// the second.
/// - Returns: A sequence that iterates first over the elements of `s1` and `s2`
/// in a sorted order
///
/// - Complexity: O(1)
@inlinable
public func merge<S1: Sequence, S2: Sequence>(
_ s1: S1,
_ s2: S2,
areInIncreasingOrder: @escaping (S1.Element, S2.Element) -> Bool
) -> Merge2Sequence<S1, S2> where S1.Element == S2.Element {
Merge2Sequence(
base1: s1,
base2: s2,
areInIncreasingOrder: areInIncreasingOrder
)
}

//===----------------------------------------------------------------------===//
// merge(_:_:)
//===----------------------------------------------------------------------===//

/// Returns a new sequence that iterates over the two given sequences,
/// alternating between elements of the two sequences, returning the lesser of
/// the two elements, as defined by the elements `Comparable` implementation.
///
/// You can pass any two sequences or collections that have the same element
/// type as this sequence and are `Comparable`. This example merges two
/// sequences of `Int`s:
///
/// let evens = stride(from: 0, to: 10, by: 2)
/// let odds = stride(from: 1, to: 10, by: 2)
/// for num in merge(evens, odds) {
/// print(num)
/// }
/// // 0
/// // 1
/// // 2
/// // 3
/// // 4
/// // 5
/// // 6
/// // 7
/// // 8
/// // 9
///
/// - Parameters:
/// - s1: The first sequence.
/// - s2: The second sequence.
/// - Returns: A sequence that iterates first over the elements of `s1` and `s2`
/// in a sorted order
///
/// - Complexity: O(1)
@inlinable
public func merge<S1: Sequence, S2: Sequence>(
_ s1: S1,
_ s2: S2
) -> Merge2Sequence<S1, S2>
where S1.Element == S2.Element, S1.Element: Comparable {
Merge2Sequence(
base1: s1,
base2: s2,
areInIncreasingOrder: <
)
}
58 changes: 58 additions & 0 deletions Tests/SwiftAlgorithmsTests/MergeTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Algorithms open source project
//
// Copyright (c) 2022 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
//
//===----------------------------------------------------------------------===//

import XCTest
@testable import Algorithms

final class MergeTests: XCTestCase {
func testMergeArrays() {
let evens = [0, 2, 4, 6, 8]
let odds = [1, 3, 5, 7, 9]
let output = merge(evens, odds)
XCTAssertEqualSequences(output, 0...9)
}

func testMergeSequences() {
let evens = stride(from: 0, to: 10, by: 2)
let odds = stride(from: 1, to: 10, by: 2)
let output = merge(evens, odds)
XCTAssertEqualSequences(output, 0...9)
}

func testMergeMixedSequences() {
let evens = [0, 2, 4, 6, 8]
let odds = stride(from: 1, to: 10, by: 2)
let output = merge(evens, odds)
XCTAssertEqualSequences(output, 0...9)
}

func testMergeSequencesWithEqualElements() {
let a = [1, 2, 3, 4, 5]
let b = [1, 2, 3, 4, 5]
let output = merge(a, b)
XCTAssertEqualSequences(output, [1, 1, 2, 2, 3, 3, 4, 4, 5, 5])
}

func testMerge3Sequences() {
let a = [0, 3, 6, 9]
let b = [1, 5, 8, 10]
let c = [2, 4, 7, 11]
let output = merge(merge(a, b), c)
XCTAssertEqualSequences(output, 0...11)
}

func testNonDefaultSortOrder() {
let evens = [8, 6, 4, 2, 0]
let odds = stride(from: 9, to: 0, by: -2)
let output = merge(evens, odds, areInIncreasingOrder: >)
XCTAssertEqualSequences(output, (0...9).reversed())
}
}