30

I have a sorted array and want to do binary search on it.

So I'm asking if something is already available in Swift library like sort etc.? Or is there a type independend version available?

Of course I could write it by my own, but I like to avoid reinventing the wheel again.

Jeehut
  • 20,202
  • 8
  • 59
  • 80
Peter71
  • 2,180
  • 4
  • 20
  • 33
  • you need sort function or binary search function? – Daniel Krom Aug 09 '15 at 12:49
  • Binary search. I have a sorted array. – Peter71 Aug 09 '15 at 13:00
  • This might be close to what you are looking for: http://stackoverflow.com/a/26679191/1187415. – Martin R Aug 09 '15 at 13:04
  • What is the purpose of the binary search? For example to find a specific element in the array the `filter` function might be suitable. – vadian Aug 09 '15 at 13:08
  • 2
    I don't know how fast filter is. It works for every array. But I know that my array is sorted. So I can use binary for improved speed. – Peter71 Aug 09 '15 at 13:26
  • Do you need the "improved speed"? Will it even be noticeable? If not don't make the code more complicated. – zaph Aug 09 '15 at 13:43
  • 4
    Sure, That's the reason I switched. I have > 1.700.000 strings in my array. And I use this search another ten thousand times. – Peter71 Aug 09 '15 at 14:25
  • 5
    If you use any of the implementations on this page, it's worth emphasising that [**Binary Search is _notoriously_ hard to get right**](https://en.wikipedia.org/wiki/Binary_search_algorithm#Implementation_issues). I'd strongly advise getting tests in place for any code you use. As an example, Java's `Arrays.binarySearch()` was broken until version 6.0 of the SDK. As a plug for [my own answer](http://stackoverflow.com/a/35206907/2547229), it includes tests. – Benjohn Feb 04 '16 at 17:10
  • 1
    In case you want to check a complete repository of algorithms or check the particular BS there, here's is the Swift Algorithms Club repo https://github.com/raywenderlich/swift-algorithm-club/blob/master/Binary%20Search/BinarySearch.swift – Barbara R May 29 '17 at 20:49

14 Answers14

51

Here's my favorite implementation of binary search. It's useful not only for finding the element but also for finding the insertion index. Details about assumed sorting order (ascending or descending) and behavior with respect to equal elements are controlled by providing a corresponding predicate (e.g. { $0 < x } vs { $0 > x } vs { $0 <= x } vs { $0 >= x }). The comment unambiguously says what exactly does it do.

extension RandomAccessCollection {
    /// Finds such index N that predicate is true for all elements up to
    /// but not including the index N, and is false for all elements
    /// starting with index N.
    /// Behavior is undefined if there is no such N.
    func binarySearch(predicate: (Element) -> Bool) -> Index {
        var low = startIndex
        var high = endIndex
        while low != high {
            let mid = index(low, offsetBy: distance(from: low, to: high)/2)
            if predicate(self[mid]) {
                low = index(after: mid)
            } else {
                high = mid
            }
        }
        return low
    }
}

Example usage:

(0 ..< 778).binarySearch { $0 < 145 } // 145
Vadim Yelagin
  • 3,112
  • 2
  • 17
  • 20
  • Btw when `Index` is `Int` and `startIndex` is zero the result is the number of elements for which the predicate is true. – Vadim Yelagin Dec 10 '15 at 10:59
  • 2
    A Swift 3 version was posted at http://stackoverflow.com/questions/40226479/converting-swift-2-code-to-swift-3-stridable-protocol. – Martin R Oct 24 '16 at 20:37
  • Thanks, Martin! I've updated the answer with your Swift 3 converted code. – Vadim Yelagin Oct 25 '16 at 11:34
  • 1
    Add `if low == high || predicate(self[index(high, offsetBy: -1)]) { return high }` before the while loop, to optimize for append operations. – jazzgil Mar 22 '17 at 07:57
  • I'm going to plug a [link to my answer](https://stackoverflow.com/a/35206907/2547229) because: 1 – it has some tests. 2 – it defines what happens if there is no element matching the block. This is _extremely useful_ because you can use it to get ranges of elements out, and you can also gracefully detect when the input does not contain the element that you are searching for. – Benjohn May 29 '17 at 14:51
  • 4
    this should be an extension on `RandomAccessCollection`, not `Collection`. This extension can only guarantee O(n log n) complexity. – taylor swift Jan 26 '19 at 03:17
  • @taylorswift why not O(logn) which seems to be the normal thing for binary search? Thanks – Dan Rosenstark Feb 14 '19 at 04:10
  • 1
    @DanRosenstark dereferencing a `Collection` index is only guaranteed to be O(n), not O(1) – taylor swift Feb 15 '19 at 02:34
  • 2
    Subscripting into Collection is always O(1), but offsetting index by K is O(K). Thus overall complexity for this extension on Collection is O(N) (because N/2 + N/4 + N/8 + ... + 1). Complexity for RandomAccessCollection is O(logN) because offsetting is O(1). – Vadim Yelagin Feb 15 '19 at 05:09
  • 1
    VadimYelagin are you and @taylorswift disagreeing on this or agreeing? Sorry to drag this out, thanks everybody – Dan Rosenstark Feb 17 '19 at 19:49
  • 3
    @DanRosenstark taylorswift is absolutely right that this algorithm guarantees O(logN) complexity only for RandomAccessCollection, not for an arbitrary Collection. However the specific reason for why that is the case was not exactly correct, so I felt I needed to clarify it to avoid further confusion in the comments. – Vadim Yelagin Feb 19 '19 at 03:53
  • Got it and your comment above makes sense. I guess it’s not just adding two numbers together! – Dan Rosenstark Feb 20 '19 at 01:49
  • @VadimYelagin - I'm not sure I understand the example usage correctly - does (0 ..< 778).binarySearch { $0 < 145 } ask binary search to return all the values between 0 and 778 that are less than 145? Another question I have - can we only check for ranges with this implementation of binary search or could we also do something such as $0 == 145 instead of $0 < 145? – japsoccer Apr 03 '20 at 14:18
  • @taylorswift binary search on linked list can be a perfectly good thing to do. C++ requires the equivalent of collection. – Justin Meiners Sep 10 '21 at 21:50
  • I'm late to this party, but anyway. It seems like a deficiency that "Behavior is undefined if there is no such N." That said, if all the predicate is false for all elements, or true for all elements, it seems like the correct result (according to the description) is startIndex or endIndex, respectively. – Robert Dodier Oct 18 '21 at 22:26
  • @RobertDodier Undefined (or at least useless) behavior on unsorted arrays is an inherent property of binary search algorithm. – Vadim Yelagin Oct 27 '21 at 19:14
29

Here's a generic way to use binary search:

func binarySearch<T:Comparable>(_ inputArr:Array<T>, _ searchItem: T) -> Int? {
    var lowerIndex = 0
    var upperIndex = inputArr.count - 1

    while (true) {
        let currentIndex = (lowerIndex + upperIndex)/2
        if(inputArr[currentIndex] == searchItem) {
            return currentIndex
        } else if (lowerIndex > upperIndex) {
            return nil
        } else {
            if (inputArr[currentIndex] > searchItem) {
                upperIndex = currentIndex - 1
            } else {
                lowerIndex = currentIndex + 1
            }
        }
    }
}

var myArray = [1,2,3,4,5,6,7,9,10]
if let searchIndex = binarySearch(myArray, 5) {
    print("Element found on index: \(searchIndex)")
}
Shaheen Ghiassy
  • 7,397
  • 3
  • 40
  • 40
Daniel Krom
  • 9,751
  • 3
  • 43
  • 44
  • 4
    This would be greatly improved by not returning `-1` when there is no match. A more Swift like approach would be to return an optional. An alternative good approach would be to return the `endIndex` when the element is not found. – Benjohn May 29 '17 at 14:56
  • @Benjohn Totally agreed about the optional, answer is 2 years old and it's time to edit it :) – Daniel Krom May 29 '17 at 19:51
  • it is also possible to be applied on string ? – Jeff Bootsholz Nov 06 '17 at 07:33
  • @RajuyourPepe for class that conform to `Comparable` protocol – Twitter khuong291 Apr 23 '19 at 08:01
  • you should add `guard !inputArr.isEmpty else { return nil }` at the beginning to account for empty array case. Also, I think the else if condition should be `lowerIndex >= upperIndex` – Morpheus Sep 22 '20 at 08:36
12

I use an extension on RandomAccessCollection implementing bisectToFirstIndex(where:) and taking a predicate.

  • It takes a test predicate, and returns the index of the first element to pass the test.
  • If there is no such index, it returns nil.
  • If the Collection is empty, it returns nil.

Example

let a = [1,2,3,4]

a.map{$0>=3}
// returns [false, false, true, true]

a.bisectToFirstIndex {$0>=3}
// returns 2

Important

You need to ensure test never returns a false for any index after an index it has said true for. This is equivalent to the usual precondition that binary search requires your data to be in order.

Specifically, you must not do a.bisectToFirstIndex {$0==3}. This will not work correctly.

Why?

bisectToFirstIndex is useful because it lets you find ranges of stuff in your data. By adjusting the test, you can find the lower and upper limits of "stuff".

Here's some data:

let a = [1,1,1, 2,2,2,2, 3, 4, 5]

We can find the Range of all the 2s like this…

let firstOf2s = a.bisectToFirstIndex { $ 0>= 2 }
let endOf2s = a.bisectToFirstIndex { $0 > 2 }
let rangeOf2s = firstOf2s ..< endOf2s

Example Application

I use this in an implementation of layoutAttributesForElementsInRect. My UICollectionViewCells are stored sorted vertically in an array. It's easy to write a pair of calls that will find all cells that are within a particular rectangle and exclude any others.

Code

extension RandomAccessCollection {
    
    public func bisectToFirstIndex(where predicate: (Element) throws -> Bool) rethrows -> Index? {
        var intervalStart = startIndex
        var intervalEnd = endIndex
        
        while intervalStart != intervalEnd {
            let intervalLength = distance(from: intervalStart, to: intervalEnd)
            
            guard intervalLength > 1 else {
                return try predicate(self[intervalStart]) ? intervalStart : nil
            }
            
            let testIndex = index(intervalStart, offsetBy: (intervalLength - 1) / 2)
            
            if try predicate(self[testIndex]) {
                intervalEnd = index(after: testIndex)
            }
            else {
                intervalStart = index(after: testIndex)
            }
        }
        
        return nil
    }
}

Updates…

The implementation here extends RandomAccessCollection and I've updated the code to build with the current Swift version (5 or something).

A Binary Search Caution

Binary searches are notoriously hard to correctly code. You really should read that link to find out just how common mistakes in their implementation are, but here is an extract:

When Jon Bentley assigned it as a problem in a course for professional programmers, he found that an astounding ninety percent failed to code a binary search correctly after several hours of working on it, and another study shows that accurate code for it is only found in five out of twenty textbooks. Furthermore, Bentley's own implementation of binary search, published in his 1986 book Programming Pearls, contains an error that remained undetected for over twenty years.

Given that last point, here is a test for this code. It passes! The testing isn't exhaustive – so there may certainly still be errors.

Tests

final class Collection_BisectTests: XCTestCase {

    func test_bisect() {
        for length in 0...100 {
            let collection = 0 ... length
            let targets = -4 ... length + 4
            
            for toFind in targets {
                let bisectIndex = collection.bisectToFirstIndex { $0 > toFind }
                let expectIndex = collection.firstIndex { $0 > toFind }
                XCTAssertEqual(bisectIndex, expectIndex, "Finding \(toFind+1) in 0...\(length)")
            }
        }
    }
}
Benjohn
  • 13,228
  • 9
  • 65
  • 127
  • Thanks very much for the detailed answer and explanation(s). I read in the swift 2.x docs for `Indexable`: "Important: In most cases, it's best to ignore this protocol and use `CollectionType` instead, as it has a more complete interface." So as per Vadim's answer I've used `extension CollectionType where Index: RandomAccessIndexType`. Also I'm wondering if it's worth & possible fool proofing this by ordering (self) before the `while` – AJP Aug 11 '16 at 19:36
  • Hi @ajp, thanks for the thoughts. 1. I don't know the swift standard libraries well (still!), but in general, I would use the minimum interface that supports the functionality required. 2. I understand your concern but I would advise against the sort before hand. Sorting is (in most cases) an `O(n.log(n))` operation. Binary search is a `O(log(n))` operation. If you're using a binary search, you probably need that (huge) asymptotic performance difference. If you don't need that difference, you are better off with a completely different algorithm that will handle unordered data. – Benjohn Aug 13 '16 at 17:18
  • Thanks @Benjohn, understood, nice logic. – AJP Aug 13 '16 at 20:25
  • How about string version ? – Jeff Bootsholz Nov 14 '17 at 06:40
  • 1
    Hey @AJP – I updated to contemporary Swift and got it conforming to `RandomAccessCollection` as you suggested 6 years ago :-) – Benjohn Nov 12 '22 at 21:24
9
extension ArraySlice where Element: Comparable {
    func binarySearch(_ value: Element) -> Int? {
        guard !isEmpty else { return nil }

        let midIndex = (startIndex + endIndex) / 2
        if value == self[midIndex] {
            return midIndex
        } else if value > self[midIndex] {
            return self[(midIndex + 1)...].binarySearch(value)
        } else {
            return self[..<midIndex].binarySearch(value)
        }
    }
}

extension Array where Element: Comparable {
    func binarySearch(_ value: Element) -> Int? {
        return self[0...].binarySearch(value)
    }
}

This is, in my opinion, very readable and leverages the fact that Swift's ArraySlice is a view on Array and retains the same indexes as the original Array with which it shares the storage so, in absence of mutations (like in this case), it is therefore very efficient.

3

here is binary search using while syntax

func binarySearch<T: Comparable>(_ a: [T], key: T) -> Int? {
    var lowerBound = 0
    var upperBound = a.count
    while lowerBound < upperBound {
        let midIndex = lowerBound + (upperBound - lowerBound) / 2
        if a[midIndex] == key {
            return midIndex
        } else if a[midIndex] < key {
            lowerBound = midIndex + 1
        } else {
            upperBound = midIndex
        }
    }
    return nil
}
Lucy Jeong
  • 320
  • 7
  • 21
2

Here is an implementation for a sorted array of strings.

var arr = ["a", "abc", "aabc", "aabbc", "aaabbbcc", "bacc", "bbcc", "bbbccc", "cb", "cbb", "cbbc", "d" , "defff", "deffz"]

func binarySearch(_ array: [String], value: String) -> String {

    var firstIndex = 0
    var lastIndex = array.count - 1
    var wordToFind = "Not founded"
    var count = 0

    while firstIndex <= lastIndex {

        count += 1
        let middleIndex = (firstIndex + lastIndex) / 2
        let middleValue = array[middleIndex]

        if middleValue == value {
            wordToFind = middleValue
            return wordToFind
        }
        if value.localizedCompare(middleValue) == ComparisonResult.orderedDescending {
            firstIndex = middleIndex + 1
        }
        if value.localizedCompare(middleValue) == ComparisonResult.orderedAscending {
            print(middleValue)
            lastIndex = middleIndex - 1
        }
    }
    return wordToFind
}
//print d
print(binarySearch(arr, value: "d")) 
James Rochabrun
  • 4,137
  • 2
  • 20
  • 17
1

Another implementation: if you want to have your structs or classes searchable without making them Comparable, make them BinarySearchable instead:

public protocol BinarySearchable {
    associatedtype C: Comparable
    var searchable: C { get }
}


public extension Array where Element: BinarySearchable {

    func binarySearch(_ prefix: Element.C) -> Index {
        var low = 0
        var high = count
        while low != high {
            let mid = (low + high) / 2
            if self[mid].searchable < prefix {
                low = mid + 1
            } else {
                high = mid
            }
        }
        return low
    }
}

Example usage for a struct that should be sorted and searched by name:

struct Country: BinraySearchable {
    var code: String
    var name: String

    var searchable: String { name }
}

// Suppose you have a list of countries sorted by `name`, you want to find
// the index of the first country whose name starts with "United", others
// will follow:

let index = listOfCountries.binarySearch("United")
mojuba
  • 11,842
  • 9
  • 51
  • 72
  • The `BinarySearchable` type is pointless, you could just directly use `Comparable` instead. –  Mar 31 '21 at 15:00
  • @ErikAigner not if the search key is not the object itself. Say you have an object with `id` and you want to sort and search by `id`, but the array stores entire objects. You define your "searchable" property separately, hence a separate protocol BinarySearchable. – mojuba Apr 01 '21 at 09:27
  • That way you could make one object only searchable by a single property, because you can only extend it once. If you use `Comparable` directly you can wrap your stored object in a `struct` that implements the comparison behavior you want. –  Apr 01 '21 at 14:42
  • @ErikAigner I know but making an object Comparable by just one property may be undesirable and even dangerous - think of accidental comparisons. Binray search is a very specific functionality and I prefer limiting this type of comparisons to it. – mojuba Apr 02 '21 at 16:26
1

And for completeness, here's a entirely pattern matching based implementation:

extension Collection where Element: Comparable {
    func binarySearch(for element: Element) -> Index? {
        switch index(startIndex, offsetBy: distance(from: startIndex, to: endIndex) / 2) {
        case let i where i >= endIndex: return nil
        case let i where self[i] == element: return i
        case let i where self[i] > element: return self[..<i].binarySearch(for: element)
        case let i: return self[index(after: i)..<endIndex].binarySearch(for: element)
        }
    }
}

The above code should work with any kind of collections, sliced or not sliced, zero offset-ed or non-zero offset-ed.

Cristik
  • 30,989
  • 25
  • 91
  • 127
1

Here is how you create a binary search function in swift 5, in this example I assume that the item you are looking for is guaranteed to be in the list, however if your item is not guaranteed to be in the list then you can run this code to check first:

yourList.contains(yourItem) //will return true or false

Here is the binary search function:

override func viewDidLoad() {
    super.viewDidLoad()
    
    print(binarySearch(list: [1, 2, 4, 5, 6], num: 6)) //returns 4
}

func binarySearch(list: [Int], num: Int) -> Int //returns index of num
{
    var firstIndex = 0
    var lastIndex = list.count - 1
    
    var middleIndex = (firstIndex + lastIndex) / 2
    var middleValue = list[middleIndex]
    
    while true //loop until we find the item we are looking for
    {
        middleIndex = (firstIndex + lastIndex) / 2 //getting the list's middle index
        middleValue = list[middleIndex]
        
        if middleValue > num
        {
            lastIndex = middleIndex - 1 //get the left side of the remaining list
        }
        else if middleValue < num
        {
            firstIndex = middleIndex + 1 //get the right side of the remaining list
        }
        else if middleValue == num
        { 
            break //found the correct value so we can break out of the loop
        }
    }
    return middleIndex
}

I have made a youtube video explaining this here

Aryaa Sk
  • 81
  • 1
  • 5
0

Here's a better implementation that returns more than one index, if there are more than 1 in the array.

extension Array where Element: Comparable {

/* Array Must be sorted */

func binarySearch(key: Element) -> [Index]? {
    return self.binarySearch(key, initialIndex: 0)
}

private func binarySearch(key: Element, initialIndex: Index) -> [Index]? {

    guard count > 0 else { return nil }

    let midIndex = count / 2
    let midElement = self[midIndex]

    if key == midElement {

        // Found!

        let foundIndex = initialIndex + midIndex

        var indexes = [foundIndex]

        // Check neighbors for same values

        // Check Left Side

        var leftIndex = midIndex - 1

        while leftIndex >= 0 {

            //While there is still more items on the left to check

            print(leftIndex)

            if self[leftIndex] == key {

                //If the items on the left is still matching key

                indexes.append(leftIndex + initialIndex)
                leftIndex--

            } else {

                // The item on the left is not identical to key

                break
            }
        }

        // Check Right side

        var rightIndex = midIndex + 1

        while rightIndex < count {

            //While there is still more items on the left to check

            if self[rightIndex] == key {

                //If the items on the left is still matching key

                indexes.append(rightIndex + initialIndex)
                rightIndex++

            } else {

                // The item on the left is not identical to key

                break
            }
        }

        return indexes.sort{ return $0 < $1 }
    }

    if count == 1 {

        guard let first = first else { return nil }

        if first == key {
            return [initialIndex]
        }
        return nil
    }


    if key < midElement {

        return Array(self[0..<midIndex]).binarySearch(key, initialIndex: initialIndex + 0)
    }

    if key > midElement {

        return Array(self[midIndex..<count]).binarySearch(key, initialIndex: initialIndex + midIndex)
    }

    return nil
}

}

Jacky Wang
  • 618
  • 7
  • 27
0

By recursive binary search,

func binarySearch(data : [Int],search: Int,high : Int,low:Int) -> Int? {
    if (low >  high)
    {
        return nil
    }
    let mid = low + (low + high)/2

    if (data[mid] == search) {
        return mid
    }
    else if (search < data[mid]){
        return binarySearch(data: data, search: search, high: high-1, low: low)
    }else {
        return binarySearch(data: data, search: search, high: high, low: low+1)
    }
}

Input : let arry = Array(0...5) // [0,1,2,3,4,5]

print(binarySearch(data: arry, search: 0, high: arry.count-1, low: 0))
Joyal Clifford
  • 1,212
  • 11
  • 20
-1

Here's a full example with several test cases for Swift 3.1. There is no chance that this is faster than the default implementation, but that's not the point. Array extension is at the bottom:

//  BinarySearchTests.swift
//  Created by Dan Rosenstark on 3/27/17
import XCTest
@testable import SwiftAlgos

class BinarySearchTests: XCTestCase {

    let sortedArray : [Int] = [-25, 1, 2, 4, 6, 8, 10, 14, 15, 1000]

    func test5() {
        let traditional = sortedArray.index(of: 5)
        let newImplementation = sortedArray.indexUsingBinarySearch(of: 5)
        XCTAssertEqual(traditional, newImplementation)
    }

    func testMembers() {
        for item in sortedArray {
            let traditional = sortedArray.index(of: item)
            let newImplementation = sortedArray.indexUsingBinarySearch(of: item)
            XCTAssertEqual(traditional, newImplementation)
        }
    }

    func testMembersAndNonMembers() {
        for item in (-100...100) {
            let traditional = sortedArray.index(of: item)
            let newImplementation = sortedArray.indexUsingBinarySearch(of: item)
            XCTAssertEqual(traditional, newImplementation)
        }
    }

    func testSingleMember() {
        let sortedArray = [50]
        for item in (0...100) {
            let traditional = sortedArray.index(of: item)
            let newImplementation = sortedArray.indexUsingBinarySearch(of: item)
            XCTAssertEqual(traditional, newImplementation)
        }
    }

    func testEmptyArray() {
        let sortedArray : [Int] = []
        for item in (0...100) {
            let traditional = sortedArray.index(of: item)
            let newImplementation = sortedArray.indexUsingBinarySearch(of: item)
            XCTAssertEqual(traditional, newImplementation)
        }
    }
}

extension Array where Element : Comparable {
    // self must be a sorted Array
    func indexUsingBinarySearch(of element: Element) -> Int? {
        guard self.count > 0 else { return nil }
        return binarySearch(for: element, minIndex: 0, maxIndex: self.count - 1)
    }

    private func binarySearch(for element: Element, minIndex: Int, maxIndex: Int) -> Int? {
        let count = maxIndex - minIndex + 1
        // if there are one or two elements, there is no futher recursion:
        // stop and check one or both values (and return nil if neither)
        if count == 1 {
            return element == self[minIndex] ? minIndex : nil
        } else if count == 2 {
            switch element {
                case self[minIndex]: return minIndex
                case self[maxIndex]: return maxIndex
                default: return nil
            }
        }

        let breakPointIndex = Int(round(Double(maxIndex - minIndex) / 2.0)) + minIndex
        let breakPoint = self[breakPointIndex]

        let splitUp = (breakPoint < element)
        let newMaxIndex : Int = splitUp ? maxIndex : breakPointIndex
        let newMinIndex : Int = splitUp ? breakPointIndex : minIndex

        return binarySearch(for: element, minIndex: newMinIndex, maxIndex: newMaxIndex)
    }
}

This is quite homemade, so... caveat emptor. It does work and does do binary search.

Dan Rosenstark
  • 68,471
  • 58
  • 283
  • 421
-1

Simple solution in Swift 5:

func binarySerach(list: [Int], item: Int) -> Int? {
    var low = 0
    var high = list.count - 1
    while low <= high {
        let mid = (low + high) / 2
        let guess = list[mid]
        if guess == item {
            return mid
        } else if guess > item {
            high = mid - 1
        } else {
            low = mid + 1
        }
    }
    return nil
}

let myList = [1,3,4,7,9]

print(binarySerach(list: myList, item: 9))
//Optional(4)
Janserik
  • 2,306
  • 1
  • 24
  • 43
-1

Details

  • Swift 5.2, Xcode 11.4 (11E146)

Solution

import Foundation

extension RandomAccessCollection where Element: Comparable {

    private func binarySearchIteration(forIndexOf value: Element, in range: Range<Index>? = nil,
                                       valueDetected: ((Index, _ in: Range<Index>) -> Index?)) -> Index? {
        let range = range ?? startIndex..<endIndex

        guard range.lowerBound < range.upperBound else { return nil }

        let size = distance(from: range.lowerBound, to: range.upperBound)
        let middle = index(range.lowerBound, offsetBy: size / 2)

        switch self[middle] {
        case value: return valueDetected(middle, range) ?? middle
        case ..<value: return binarySearch(forIndexOf: value, in: index(after: middle)..<range.upperBound)
        default: return binarySearch(forIndexOf: value, in: range.lowerBound..<middle)
        }
    }

    func binarySearch(forIndexOf value: Element, in range: Range<Index>? = nil) -> Index? {
        binarySearchIteration(forIndexOf: value, in: range) { currentIndex, _ in currentIndex }
    }

    func binarySearch(forFirstIndexOf value: Element, in range: Range<Index>? = nil) -> Index? {
        binarySearchIteration(forIndexOf: value, in: range) { currentIndex, range in
            binarySearch(forFirstIndexOf: value, in: range.lowerBound..<currentIndex)
        }
    }

    func binarySearch(forLastIndexOf value: Element, in range: Range<Index>? = nil) -> Index? {
        binarySearchIteration(forIndexOf: value, in: range) { currentIndex, range in
            binarySearch(forFirstIndexOf: value, in: index(after: currentIndex)..<range.upperBound)
        }
    }

    func binarySearch(forIndicesRangeOf value: Element, in range: Range<Index>? = nil) -> Range<Index>? {
        let range = range ?? startIndex..<endIndex
        guard range.lowerBound < range.upperBound else { return nil }

        guard let currentIndex = binarySearchIteration(forIndexOf: value, in: range, valueDetected: { index, _ in index
        }) else { return nil }

        let firstIndex = binarySearch(forFirstIndexOf: value, in: range.lowerBound ..< index(after: currentIndex)) ?? currentIndex
        let lastIndex = binarySearch(forFirstIndexOf: value, in: index(after: currentIndex) ..< range.upperBound) ?? currentIndex

        return firstIndex..<index(after: lastIndex)
    }
}

Usage

//let array = ["one", "two", "three", "three", "three", "three", "three", "four", "five", "five"]
//let value = "three"
let array = [1, 2, 3, 3, 3, 3, 3, 4, 5, 5]
let value = 3
print(array.binarySearch(forFirstIndexOf: value))
print(array.binarySearch(forLastIndexOf: value))
print(array.binarySearch(forIndicesRangeOf: value))

Tests

protocol _BinarySearchTestable: class where Collection: RandomAccessCollection, Collection.Element: Comparable {
    associatedtype Collection
    var array: Collection! { get set }
    var elementToSearch: Collection.Element! { get set }
    func testFindFirstIndexOfValueInCollection()
    func testFindLastIndexOfValueInCollection()
    func testFindIndicesRangeOfValueInCollection()
}

extension _BinarySearchTestable where Self: XCTest {

    typealias Element = Collection.Element
    typealias Index = Collection.Index

    func _testFindFirstIndexOfValueInCollection() {
        _testfindFirstIndex(comparableArray: array, testableArray: array)
    }

    func _testFindLastIndexOfValueInCollection() {
        let index1 = array.lastIndex(of: elementToSearch)
        let index2 = array.binarySearch(forLastIndexOf: elementToSearch)
        _testElementsAreEqual(indexInComparableArray: index1, comparableArray: array,
                              indexInTestableArray: index2, testableArray: array)
    }

    func _testFindIndicesRangeOfValueInCollection() {
        var range1: Range<Index>?
        if  let firstIndex = array.firstIndex(of: elementToSearch),
            let lastIndex = array.lastIndex(of: elementToSearch) {
                range1 = firstIndex ..< array.index(after: lastIndex)
        }
        let range2 = array.binarySearch(forIndicesRangeOf: elementToSearch)
        XCTAssertEqual(range1, range2)
    }

    private func _testElementsAreEqual(indexInComparableArray: Index?, comparableArray: Collection,
                                       indexInTestableArray: Index?, testableArray: Collection) {
        XCTAssertEqual(indexInComparableArray, indexInTestableArray)
        var valueInComparableArray: Element?
        if let index = indexInComparableArray { valueInComparableArray = comparableArray[index] }

        var valueInTestableArray: Element?
        if let index = indexInComparableArray { valueInTestableArray = testableArray[index] }
        XCTAssertEqual(valueInComparableArray, valueInTestableArray)
    }

    private func _testfindFirstIndex(comparableArray: Collection, testableArray: Collection) {
        let index1 = comparableArray.firstIndex(of: elementToSearch)
        let index2 = testableArray.binarySearch(forFirstIndexOf: elementToSearch)
        _testElementsAreEqual(indexInComparableArray: index1, comparableArray: comparableArray,
                              indexInTestableArray: index2, testableArray: testableArray)
    }
}

class TestsInEmptyArray: XCTestCase, _BinarySearchTestable {

    var array: [String]!
    var elementToSearch: String!

    override func setUp() {
        array = []
        elementToSearch = "value"
    }

    func testFindFirstIndexOfValueInCollection() { _testFindFirstIndexOfValueInCollection() }
    func testFindLastIndexOfValueInCollection() { _testFindLastIndexOfValueInCollection() }
    func testFindIndicesRangeOfValueInCollection() { _testFindIndicesRangeOfValueInCollection() }
}

class TestsInArray: XCTestCase, _BinarySearchTestable {

    var array: [Int]!
    var elementToSearch: Int!

    override func setUp() {
        array = [1, 2, 3, 3, 3, 3, 3, 4, 5, 5]
        elementToSearch = 3
    }

    func testFindFirstIndexOfValueInCollection() { _testFindFirstIndexOfValueInCollection() }
    func testFindLastIndexOfValueInCollection() { _testFindLastIndexOfValueInCollection() }
    func testFindIndicesRangeOfValueInCollection() { _testFindIndicesRangeOfValueInCollection() }
}

class TestsInArrayWithOneElement: XCTestCase, _BinarySearchTestable {

    var array: [Date]!
    var elementToSearch: Date!

    override func setUp() {
        let date = Date()
        array = [date]
        elementToSearch = date
    }

    func testFindFirstIndexOfValueInCollection() { _testFindFirstIndexOfValueInCollection() }
    func testFindLastIndexOfValueInCollection() { _testFindLastIndexOfValueInCollection() }
    func testFindIndicesRangeOfValueInCollection() { _testFindIndicesRangeOfValueInCollection() }
}
Vasily Bodnarchuk
  • 24,482
  • 9
  • 132
  • 127