Start learning free

LESSON 244 OF 338 · BINARY SEARCH

Binary Search in Go: Finding an Index in a Sorted Slice

Lesson O(log n) Read the full explanation

In this video lesson we will learn about the concept of searching, linear searching and more important - Binary Searching:


Problem:

Given a sorted array of integers nums, use Binary Search to find and return the index of a given value.

If the value doesn't exist in nums, return -1.


Example 1:

Input: nums = [1, 2, 4, 5], value = 4
Output: 2
Explanation: nums[2] is 4

Note:

Your algorithm should run in O(log n) time and use O(1) extra space.


STEP 1 OF 7
    Ctrl + Enter to check

    GO · BINARY SEARCH

    Binary Search in Go, explained

    Searching: linear against binary

    To find a value in a slice you can look at the elements one by one from the start. That is a linear search, and in the worst case it checks every element: O(n) time. It works on any slice, sorted or not.

    If the slice is sorted, you can do much better. Look at the middle element. If it is the value you want, you are done. If it is smaller than the value, the value can only be in the right half, because everything to the left of the middle is smaller still. If it is larger, the value can only be in the left half. Either way, half of the remaining elements are discarded with one comparison. Repeating this is binary search, and it takes O(log n) time: a sorted slice of a million elements needs at most about 20 comparisons.

    The price is the precondition. Binary search on an unsorted slice gives wrong answers, so it only makes sense when the data is already sorted or is worth sorting first.

    The exercise

    The function find receives a sorted slice of integers, nums, and a value. It returns the index of value in nums, or -1 when it isn't there. With nums = [1, 2, 4, 5] and value = 4 the answer is 2, because nums[2] is 4. The budget is O(log n) time and O(1) extra space, so the search must be a loop that keeps only a few integers, not a recursion that copies slices.

    Step by step

    1. Keep two markers. left is the first index still possible and right is the last one. Both are inclusive: right starts at len(nums)-1, the last valid index. A single := declares both.
    2. Loop while the range has elements. Go has no while; a for with only a condition does the job. The condition is left <= right, with <= because when the two are equal there is still one element to check.
    3. Find the middle. left + (right-left)/2 is the index halfway between them. Dividing two ints in Go truncates, so no rounding function is needed.
    4. Found it: if nums[middle] equals value, return middle.
    5. Middle too small: the value can only be to the right, so set left = middle + 1. The + 1 matters because middle itself has been ruled out.
    6. Middle too large: set right = middle - 1, for the same reason.
    7. Not found: if the loop ends, the range is empty and every candidate was ruled out, so return -1 after the loop.

    The solution in Go

    func find(nums []int, value int) int {
        left, right := 0, len(nums)-1
        for left <= right {
            middle := left + (right-left)/2
            if nums[middle] == value {
                return middle
            } else if nums[middle] < value {
                left = middle + 1
            } else {
                right = middle - 1
            }
        }
        return -1
    }
    

    Here is a trace for nums = [1, 2, 4, 5] and value = 4. At the start left = 0 and right = 3, so middle = 1 and nums[1] is 2. That is smaller than 4, so left becomes 2. Now middle = 2 + (3-2)/2 = 2 and nums[2] is 4, so the function returns 2.

    For value = 3 in the same slice, the first step is identical (left becomes 2). Then nums[2] = 4 is larger than 3, so right becomes 1. Now left > right, the loop ends, and the function returns -1.

    Go details

    • Multiple assignment. left, right := 0, len(nums)-1 declares two variables in one statement.
    • for is the only loop. for left <= right { ... } is Go's while loop.
    • Integer division truncates toward zero. Here the numerator is never negative, so it is the same as rounding down.
    • Why left + (right-left)/2. It gives the same index as (left + right) / 2 but never adds the two indices. In languages with 32-bit integers that sum can overflow for huge arrays; Go's int is 64 bits on 64-bit machines, so it isn't a risk in practice here. It is still the form to remember.
    • Slices are not copied. The function passes the slice header (pointer, length, capacity) and works on the same elements, so it uses constant extra space.
    • The empty slice. With len(nums) == 0, right is -1, the condition is false at once and the function returns -1. No element is read.

    Common mistakes

    • Using left < right. The loop then skips a range of one element. With nums = [7] and value = 7 the function would return -1.
    • Setting left = middle or right = middle. When the range is two elements, middle equals left, so left = middle changes nothing and the loop never ends.
    • Using the search on an unsorted slice. The halving logic assumes order, so the result is meaningless otherwise.
    • Returning 0 for a miss. Index 0 is a valid position, so the not-found signal must be -1.
    • Returning the value instead of the index. The function returns middle, not nums[middle].

    Key takeaways

    • Binary search needs a sorted slice and runs in O(log n) time with O(1) extra space.
    • Keep an inclusive range [left, right] and shrink it past middle, never onto it.
    • Loop while left <= right; when the loop ends the value is absent.
    • This same loop is the base for variations such as lower bound, which searches for a position instead of an exact match.
    Keep going with GoAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free