Binary Search and Applications

In this tutorial we’ll look at one of the fundamental algorithms of computer science, binary search. We’ll also look at a practical application of binary search: implementing fast autocompletion.

final screenshot

Introduction


Consider the issue of discovering products in an range. The apparent remedy is to go through all the products in the range and examine if any of them is similar to our product. This strategy is known as straight line look for and an execution can be seen below.

func linearSearch(array: [Int], key: Int) -> Bool {
    for item in array {
        if item == key {
            return true
        }
    }
    return false
}

 Straight line look for is often fast enough for most realistic reasons. However linear look for decelerates considerably when the variety of components in an range is large or evaluating components is expensive.

Another much quicker strategy is binary look for. Binary look for only performs on categorized arrays so we have to type our range or sustain it in categorized order for binary look for to work.

Binary look for performs by breaking the range into 2 sections around the center factor. The look for only carries on in one of the sections based on the discovered factor.
If the discovered factor is less than our focus on we proceed looking in the higher 50 percent of the range.
If the factor is higher than our focus on we proceed looking in the lower 50 percent of the range.
If the discovered factor is just like our focus on we are done.
We proceed this procedure until we have either discovered the factor or we’re breaking an range of duration 1.

To get some instinct on how binary look for performs, consider a yellow pages that contains an alphabetic list of subscribers. Let’s say you want to discover the contact variety of Nash David, a wise decision would be to open the guide in the center and look for for the variety from there. Now you only have to look for 50 percent of the guide because Nash David will either be after the name you discover in the center or before it. Binary look for performs on a identical concept but repeat the procedure of halving the look for space until the factor is discovered.

The below blueprints demonstrate the actions binary look for takes when finding the factor 36 in the categorized range below.

let array = [1, 2, 3, 5, 5, 7, 10, 36, 50]

 step 1
step 2
step 3
step 4

Binary look for is much more efficient than straight line look for because it sections the look for area at each phase. This is not important for our range of duration 9, here straight line look for requires at most 9 actions and binary look for requires at most 4 actions. But consider an range with 1000 components, here straight line look for requires at most 1000 actions while binary look for requires at most 10 actions (subsequently considering arrays of dimension 1000, 500, 250, 75, 37, 18, 9, 4, 2, 1).

Even if we had 1 billion dollars components binary look for will still be able to discover our key in at most 30 actions.


Mind == Blown
Consider an array with a Googol(10^100) entries. That’s way more items than the number of particles in the observable universe. Binary search can find an item in that array in at most 333steps, while linear search would take longer than the life of the universe.

To go a little further if our array has K elements, linear search will take K steps while binary search will take [log₂(K)]steps.
log₂(K) is the base 2 logarithm of K, that is the power we have to raise 2 too so that we get K. In other words how many times do we have to repeatedly multiply 1 by 2 so that we get K.
If log₂(K) = n then 2ⁿ = K .

Swift implementation

Until now we only talked about binary search from a theoretical perspective, let’s see how we can implement the algorithm in swift. The algorithm will be implemented as a function that takes an array of ints and a target and returnstrue/false depending on whether the target is in our array.
We’ll maintain two indices (left and right) that indicate what slice of the array we’re considering for search. Initially this correspond to the first and last index of the array.
Next we’ll repeat the process of getting the middle element from our slice and comparing it to our target. We want to repeat these steps in a while loop while the length of our slice is at least 1. That is left <= right.
At each step of the loop we compute the middle index as the average of left and right and retrieve the value from the middle.
Now we can encounter 3 cases: 1. We find our target. We’re done! We return true. 2. The found value is less than our target. We set left to mid + 1 and continue our search in the upper slice of our array. 3. The found value is greater than our target. We set right to mid - 1 and continue our search in the lower slice of our array.
If we exit the loop then our target is definitely not in the array and we return false.


func binarySearch(array: [Int], target: Int) -> Bool {

    var left = 0
    var right = array.count - 1

    while (left <= right) {
        let mid = (left + right) / 2
        let value = array[mid]

        if (value == target) {
            return true
        }

        if (value < target) {
            left = mid + 1
        }

        if (value > target) {
            right = mid - 1
        }
    }

    return false
}
Our implementation currently only works on integers but we can easily expand it to work on generic types that areComparable.
func binarySearch<T : Comparable>(array: [T], target: T) -> Bool {
    var left = 0
    var right = array.count - 1

    while (left <= right) {
        let mid = (left + right) / 2
        let value = array[mid]

        if (value == target) {
            return true
        }

        if (value < target) {
            left = mid + 1
        }

        if (value > target) {
            right = mid - 1
        }
    }

    return false
}

Searching for prefixes

 Our general edition of binary look for can be used to look for for a sequence in an range of post. We can further increase binary look for on post such that instead of discovering an actual coordinate it looks if a sequence in our selection begins with our focus on post. If a sequence begins with our focus on sequence we say that the focus on sequence is a prefix of that sequence.

To apply this we only have to substitute our equal rights examine with a contact to the hasPrefix technique.


func binarySearchPrefix(array: [String], target: String) -> Bool {
    var left = 0
    var right = array.count - 1

    while (left <= right) {
        let mid = (left + right) / 2
        let value = array[mid]

        if (value.hasPrefix(target)) {
            return true
        }

        if (value < target) {
            left = mid + 1
        }

        if (value > target) {
            right = mid - 1
        }
    }

    return false
}

 There can possibly be many products in our range that begin with the same prefix. We can apply a operate that instead of true/false profits the catalog of the first sequence in our range that begins with a given prefix. In the same way we can apply a operate that profits the catalog of the last sequence in our range that begins with a given prefix. We’ll use these techniques to apply autocomplete.

The technique is that we only come back an catalog in one of 2 cases:
1. Our remaining and right spiders are equivalent and the focus on is a prefix of the present value
2. The focus on is a prefix of the present value and the past factor in our range is not a prefix of the present value. This performs for the binarySearchFirst technique. We also have to be cautious not to accessibility a feature outside our range.
3. For binarySearchLast we use a identical strategy but we examine if the next factor is not a prefix of the present value.

If no sequence suits the given focus on the value -1 is came back.

binarySearchFirst

func binarySearchFirst(array: [String], target: String) -> Int {
    var left = 0
    var right = array.count - 1

    while (left <= right) {
        let mid = (left + right) / 2
        let value = array[mid]

        if (left == right && value.hasPrefix(target)) {
            return left
        }

        if value.hasPrefix(target) {
            if mid > 0 {
                if !array[mid - 1].hasPrefix(target) {
                    return mid
                }
            }
            right = mid - 1
        } else if (value < target) {
            left = mid + 1
        } else if (value > target) {
            right = mid - 1
        }
    }

    return -1
}
 
binarySearchLast

func binarySearchLast(array: [String], target: String) -> Int {
    var left = 0
    var right = array.count - 1

    while (left <= right) {
        let mid = (left + right) / 2
        let value = array[mid]

        if (left == right && value.hasPrefix(target)) {
            return left
        }

        if value.hasPrefix(target) {
            if mid < array.count - 1 {
                if !array[mid + 1].hasPrefix(target) {
                    return mid
                }
            }

            left = mid + 1
        } else if (value < target) {
            left = mid + 1
        } else if (value > target) {
            right = mid - 1
        }
    }

    return -1
}

Autocompletion

Consider the following issue, you’re creating a web browser and want to offer autocomplete when a customer kinds in the url to a web page. A customer can possibly have a large number of URLs in their look for record, doing a straight line look for through them for autocomplete just won’t cut it after a while.

Below you can see how autocomplete acts with ~90K records. Observe that straight line look for is incredibly laggy at this factor making for a bad consumer encounter.
 
Linear Search



Binary Search





The UI code for this project is pretty straightforward: a textField where the user types the URL and a tableViewbelow it that provides autocomplete suggestions. The data for the tableview is provided by the class SearchManager. This class contains the following methods:
func updateFilter(filter: String) – updates the string used for filtering the URLs
func filteredURLAtIndex(index: Int) -> String – returns the URL at an index for the list of filtered URLs
func filteredURLCount() -> Int – returns the number of URLs that match the current filter.

UI Code

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var textField: UITextField!

    var searchManager = SearchManager()

    override func viewDidLoad() {
        super.viewDidLoad()


    }

    @IBAction func textFieldDidChange(sender: AnyObject) {
        searchManager.updateFilter(textField.text)
        tableView.reloadData()
    }


    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

        let cell = UITableViewCell()

        cell.textLabel!.text = searchManager.filteredURLAtIndex(indexPath.row)

        return cell
    }

    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return searchManager.filteredURLCount()
    }

    func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        self.textField.text = searchManager.filteredURLAtIndex(indexPath.row)
    }

}
 
Search manager

 Look for administrator performs by keeping a record of all web addresses that it flows from a computer file at initialization. Two spiders are maintained: filterStart – the catalog of the first URL that suits the given narrow and filterEnd – the catalog of the last URL that suits the given narrow.

Below you can see an execution of SearchManager that uses straight line search.

class SearchManager {

    private var urls: [String]
    private var filterStart: Int
    private var filterEnd: Int

    init() {
        let path = NSBundle.mainBundle().pathForResource("urls", ofType: "txt")!

        let content = String(contentsOfFile: path, encoding: NSUTF8StringEncoding, error: nil)!

        urls = content.componentsSeparatedByString("\n")

        filterStart = 0
        filterEnd = urls.count - 1
    }

    func filteredURLCount() -> Int {
        if filterStart == -1 {
            return 0
        }

        return filterEnd - filterStart + 1
    }

    func filteredURLAtIndex(index: Int) -> String {
        return urls[filterStart + index]
    }



    func updateFilter(filter: String) {

        if filter == "" {
            filterStart = 0
            filterEnd = urls.count - 1
            return
        }

        var foundFirst = false

        var index = 0
        for url in urls {
            if url.hasPrefix(filter) {
                if !foundFirst {
                    filterStart = index
                    foundFirst = true
                }

                filterEnd = index

            }
            index++
        }

        if !foundFirst {
            filterStart = -1
        }
    }
}
 
A version of SearchManager that uses binary search is straightforward to write. Note that we don’t have to change our UI code in any way when changing the internals of SearchManager.
 
class SearchManager {

    private var urls: [String]
    private var filterStart: Int
    private var filterEnd: Int

    init() {
        let path = NSBundle.mainBundle().pathForResource("urls", ofType: "txt")!

        let content = String(contentsOfFile: path, encoding: NSUTF8StringEncoding, error: nil)!

        urls = content.componentsSeparatedByString("\n")

        filterStart = 0
        filterEnd = urls.count - 1
    }

    func filteredURLCount() -> Int {
        if filterStart == -1 {
            return 0
        }

        return filterEnd - filterStart + 1
    }

    func filteredURLAtIndex(index: Int) -> String {
        return urls[filterStart + index]
    }

    func binarySearchLast(array: [String], target: String) -> Int {
        var left = 0
        var right = array.count - 1

        while (left <= right) {
            let mid = (left + right) / 2
            let value = array[mid]

            if (left == right && value.hasPrefix(target)) {
                return left
            }

            if value.hasPrefix(target) {
                if mid < array.count - 1 {
                    if !array[mid + 1].hasPrefix(target) {
                        return mid
                    }
                }

                left = mid + 1
            } else if (value < target) {
                left = mid + 1
            } else if (value > target) {
                right = mid - 1
            }
        }

        return -1
    }

    func binarySearchFirst(array: [String], target: String) -> Int {
        var left = 0
        var right = array.count - 1

        while (left <= right) {
            let mid = (left + right) / 2
            let value = array[mid]

            if (left == right && value.hasPrefix(target)) {
                return left
            }

            if value.hasPrefix(target) {
                if mid > 0 {
                    if !array[mid - 1].hasPrefix(target) {
                        return mid
                    }
                }
                right = mid - 1
            } else if (value < target) {
                left = mid + 1
            } else if (value > target) {
                right = mid - 1
            }
        }

        return -1
    }


    func updateFilter(filter: String) {

        if filter == "" {
            filterStart = 0
            filterEnd = urls.count - 1
            return
        }

        filterStart = binarySearchFirst(urls, target: filter)
        filterEnd = binarySearchLast(urls, target: filter)
    }

}

 Download Project