I just found out about this library while browsing through Apple's GitHub site for SwiftNIO. I was doing this to take a break from... writing algorithms! In fact, I did binary search and sort check. What about this interface:
extension Sequence {
/// Returns a sequence containing the longest prefix of elements that are
/// sorted according to the given predicate.
public func sortedPrefix(stopsAtDuplicate: Bool = false, by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows -> [Element]
/// Returns a sequence formed by skipping the longest prefix of elements that
/// were sorted according to the given predicate.
public func dropSortedPrefix(stopsAtDuplicate: Bool = false, by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows -> PrependedIteratorSequence<Iterator>
}
extension Collection {
/// Returns the past-the-end index for the longest prefix of the collection
/// that can be considered sorted according to the given predicate.
public func sortedPivot(stopsAtDuplicate: Bool = false, by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows -> Index
}
All three have a variant the drops the predicate to default it to <. The PrependedIteratorSequence type carries over a dropped suffix and the element just before it, since I can't test where a sort breaks without reading the first post-sort element from the iterator. (The standard DropWhileSequence does the same thing.)
In sorting, runs of the same value are OK. But I added an optional flag for strictly increasing sequences/collections, for advanced work.
extension Collection {
/// Returns the index of some element equivalent to the given value,
/// assuming the collection is (at least) partially sorted according to the
/// given predicate.
public func someIndex(of element: Element, by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows -> (index: Index, isMatch: Bool)
/// Returns the index of the earliest element whose order is equivalent to
/// the element at the given index, assuming the collection is (at least)
/// partially sorted according to the given predicate.
public func lowerBound(for pivot: Index, by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows -> Index
/// Returns the index of the earliest element whose order is greater than
/// the element at the given index, assuming the collection is (at least)
/// partially sorted according to the given predicate.
public func upperBound(for pivot: Index, by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows -> Index
/// Returns the range of indices for the elements of the collection that are
/// all equivalent to the given value, assuming the collection is (at least)
/// partially sorted according to the given predicate.
public func sortedBounds(of element: Element, by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows -> Range<Index>
}
All four have a variant that removes the predicate parameter to default it to <. I was inspired by the analogous functions from C++, but tried to improve their efficiency.
-
binary_search just tells you if a match exists or not. It doesn't tell you where it is (if successful) or the best place to insert (if failure). Common implementations also just punt the work to lower_bound. The someIndex(of: by:) method gives you both the location (so you can do further work), and whether it's a match (so you don't need to test for endIndex and for element equality before the main work) with a custom algorithm.
- Speaking of a custom algorithm,
someIndex(of: by:), lowerBound(for: by:), and upperBound(for: by:) do separate parts of the process, and so have slightly different algorithms. Like I said, unlike C++, someIndex never punts to lowerBound, so it's free to stop at any match, even in the middle. If you want the lower border, you must explicitly ask for it.
-
binary_search, lower_bound, and upper_bound base their searches off a value. While someIndex does the same thing, lowerBound and upperBound work off a known-good matching index. This is for efficiency; you have to call someIndex or your own substitute first. And there's no need to worry about what happens if you use lb or ub with a non-existent value; you couldn't get a matching index to use in the first place.
-
sortedBounds(of: by:) calls the other three methods for a basic user interface.