giannif / BinarySearch

Binary Search Swift Extension

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Binary Search Swift Extension

Build Status Carthage compatible

Perform a binary search on Collections (e.g. Array, Dictionary, Set), with a few powerful options such as finding the first or last index in the collection, matching an optional predicate, and the ability to extract values from a collection to avoid having to prepare that collection for a binary search.

Methods

  • binarySearch - Find the index of the first element that passes the truth test (predicate). The default predicate is ==.
  • binarySearchFirst - Find the index of the first element in the collection that matches the predicate.
  • binarySearchLast - Find the index of the last element in the collection that matches the predicate.
  • binarySearchRange - Find the startIndex and endIndex of elements that match the predicate.
  • binarySearchInsertionIndexFor - Find the index that an element would be inserted at.

Arguments

  • find required - The value you're looking for
  • predicate optional - A truth test, e.g. $0 <= $1, $0.hasPrefix($1), where $0 is the value of find and $1 is the current element value. See more.
  • extract optional - The value to compare, e.g. $0.someVal, where someVal is the same type as the argument for find. See more.

Important

Running binary search requires sorted data.

Important

Duplicate values are unsupported.

Performance Benefit

Binary Search runs in O(log n), as opposed to linear search which runs in O(n). Meaning, if you use linear search, in the worst case scenario, the number of steps to find your value will equal the number of elements. For binary search, this number is dramatically lower.

All binary search methods in this extension return an Index, or a Range<Index> for binarySearchRange.

binarySearch

In all examples below, assume the tests pass. The documation code has tests.

Find an item that is equal to the first argument.

let sortedInts = [0,1,2,3,4,5,6,7,8,9,10]
guard let intIndex = sortedInts.binarySearch(6) else {
  XCTFail("Failed to find Int 6")
  return
}
XCTAssertEqual(intIndex, 6)
// In this contrived example, our values are the same as the indices.
XCTAssertEqual(sortedInts[intIndex], 6) 
let fruitsArray = ["apples", "bananas", "oranges"]
guard let fruitIndex = fruitsArray.binarySearch("bananas") else {
  XCTFail("Failed to find 'bananas'")
  return
}
XCTAssertEqual(fruitIndex, 1)
XCTAssertEqual(fruitsArray[fruitIndex], "bananas")

let kiwiIndex = fruitsArray.binarySearch("kiwi")
XCTAssertNil(kiwiIndex) // If the search fails the result is nil

Predicate

An optional argument to evaluate truthfulness. The default is $0 == $1, where $0 is the value of find and $1 is the current element value.

let fruitsArray = ["apples", "bananas", "oranges"]
guard let index = fruitsArray.binarySearch("bana", predicate:{$0.hasPrefix($1)}) else {
  XCTFail("Failed to find element that begins with 'bana'")
  return
}
XCTAssertEqual(index, 1)
XCTAssertEqual(fruitsArray[index], "bananas")

Extract

An optional argument to extract a value. The value must match the type you're searching for, and the array must be sorted on the property that you're extracting.

The default is Generator.Element -> T = {(el) -> T in return el as! T}

struct Fruit {
  let name:String
}
// An Array of Fruits
let fruits = [
  Fruit(name:"apple"),
  Fruit(name:"banana"),
  Fruit(name:"orange")
]
guard let index = fruits.binarySearch("banana", extract:{$0.name}) else {
  XCTFail("Failed to find a dictionary with a name 'bananas'")
  return
}
XCTAssertEqual(index, 1)
XCTAssertEqual(fruits[index].name, "banana")

binarySearchFirst

Find the value with the lowest index that meets the predicate.

let sortedArray = [0,5,15,75,100]
guard let indexGreaterThanOrEqual = sortedArray.binarySearchFirst(10, predicate: {$0 >= $1}) else {
  XCTFail("Failed to find an index greater than or equal to 10")
  return
}
XCTAssertEqual(indexGreaterThanOrEqual, 2)
XCTAssertEqual(sortedArray[indexGreaterThanOrEqual], 15)

guard let indexLessThanOrEqual = sortedArray.binarySearchFirst(10, predicate: {$0 <= $1}) else {
  XCTFail("Failed to find an index less than or equal to 10")
  return
}
XCTAssertEqual(indexLessThanOrEqual, 0)
XCTAssertEqual(sortedArray[indexLessThanOrEqual], 0)

binarySearchLast

Find the value with the highest index that meets the predicate.

let sortedArray = [0,5,15,75,100]
guard let indexGreaterThanOrEqual = sortedArray.binarySearchLast(10, predicate: {$0 >= $1}) else {
  XCTFail("Failed to find an index greater than or equal to 10")
  return
}
XCTAssertEqual(indexGreaterThanOrEqual, 4)
XCTAssertEqual(sortedArray[indexGreaterThanOrEqual], 100)

guard let indexLessThanOrEqual = sortedArray.binarySearchLast(10, predicate: {$0 <= $1}) else {
  XCTFail("Failed to find an index less than or equal to 10")
  return
}
XCTAssertEqual(indexLessThanOrEqual, 1)
XCTAssertEqual(sortedArray[indexLessThanOrEqual], 5)

binarySearchRange

A convenience method that wraps binarySearchFirst and binarySearchLast

let sortedArray = [0,5,15,75,100]
guard let range = sortedArray.binarySearchRange(10, predicate:{$0 >= $1}) else {
  XCTFail("Failed to find a range of Ints greater than or equal to 10")
  return
}
XCTAssertEqual(range.startIndex, 2)
XCTAssertEqual(sortedArray[range.startIndex], 15)
XCTAssertEqual(range.endIndex, 4)
XCTAssertEqual(sortedArray[range.endIndex], 100)

If you need to have a different predicate for the start and end, just use the binarySearchFirst and binarySearchLast methods:

let sortedArray = [0,5,15,75,100,150]
guard let startIndex = sortedArray.binarySearchFirst(10, predicate:{$0 >= $1}),
  endIndex = sortedArray.binarySearchLast(100, predicate:{$0 <= $1})
  else {
    XCTFail("Failed to find a range of Ints greater than or equal to 10 and less than or equal to 100")
    return
}
// Find the range of values that's greater than or equal to 10
XCTAssertEqual(startIndex, 2)
XCTAssertEqual(sortedArray[startIndex], 15)
XCTAssertEqual(endIndex, 4)
XCTAssertEqual(sortedArray[endIndex], 100)

binarySearchInsertionIndexFor

Returns the Index that an element could be inserted at while maintaing the sort. If the return value is nil, the element already exists in the collection.

guard let foundIndex = [0,5,15,75,100].binarySearchInsertionIndexFor(10) else {
  XCTFail("The find should have succeeded")
  return
}
// The value 10 would go at the 2nd index
XCTAssertEqual(foundIndex, 2)
guard let foundIndex = [0,5,15,75,100].binarySearchInsertionIndexFor(-5) else {
  XCTFail("The find should have succeeded")
  return
}
// The value -5 would go at the start index
XCTAssertEqual(foundIndex, 0)
guard let foundIndex = [0,5,15,75,100].binarySearchInsertionIndexFor(500) else {
  XCTFail("The find should have succeeded")
  return
}
// The value 500 would go at the end index
XCTAssertEqual(foundIndex, 5)
guard let _ = [0,5,15,75,100].binarySearchInsertionIndexFor(75) else {
  XCTAssertTrue(true, "The element already exists")
  return
}
XCTFail("The foundIndex should be nil")

Installation

Copy the BinarySearch.swift file into your project, or use Carthage.

To Do

  • What can the predicate contain?
  • Support for duplicate values, or throw an error when they're encountered
  • Clean up tests. Write more tests.
  • Carthage support
  • Installation instructions
  • Run tests on Travis CI

About

Binary Search Swift Extension


Languages

Language:Swift 98.2%Language:Objective-C 1.8%