FirstHack Learn
Log in Sign up free
Lessons in this course 0/6 All courses Algorithms and Complexity

CSE

Progress0 / 6 lessons
  1. 1. Big-O without hand-waving
  2. 2. Searching: linear and binary
  3. 3. Bubble, selection and insertion sort
  4. 4. Merge sort and quick sort
  5. 5. Recursion and how to reason about it
  6. 6. Greedy versus dynamic programming

Courses › Algorithms and Complexity

Searching: linear and binary

Binary search in full, including the two bugs that break almost every first attempt.

11 min read · Lesson 2 of 6 · Free

C++
int linear_search(const int a[], int n, int key) {
    for (int i = 0; i < n; i++)
        if (a[i] == key) return i;
    return -1;
}

O(n), works on any data in any order, and for small arrays it is genuinely the right answer. Do not dismiss it - it needs no sorting, no preprocessing, and it is impossible to get wrong.

Requires the array to be sorted. Each step throws away half the range.

C++
#include <iostream>
using namespace std;

int binary_search_index(const int a[], int n, int key) {
    int lo = 0, hi = n - 1;
    while (lo <= hi) {
        int mid = lo + (hi - lo) / 2;
        if (a[mid] == key)      return mid;
        else if (a[mid] < key)  lo = mid + 1;
        else                    hi = mid - 1;
    }
    return -1;
}

int main() {
    int a[] = {11, 22, 33, 44, 55, 66, 77};
    int n = 7;
    cout << binary_search_index(a, n, 55) << "\n";   // 4
    cout << binary_search_index(a, n, 11) << "\n";   // 0
    cout << binary_search_index(a, n, 50) << "\n";   // -1
    return 0;
}

Nine lines. Every one of them is a place people get it wrong. Take them one at a time.

Bug 1: the loop condition

It must be lo <= hi, not lo < hi.

With lo < hi, the loop stops when lo == hi - a range with exactly one element left, which never gets checked. Search for 11 in the array above and you get -1 even though it is at index 0.

To convince yourself: search a 1-element array. lo = 0, hi = 0. With lo < hi the loop body never runs at all.

⚠️

lo <= hi pairs with lo = mid + 1 and hi = mid - 1. If you write hi = mid instead of hi = mid - 1 while keeping lo <= hi, and mid lands on hi, hi never changes and the loop spins forever. The pair must match: either (<=, mid+1, mid-1) or (<, mid+1, mid). Never mix.

Bug 2: computing mid

The obvious version is int mid = (lo + hi) / 2;. It is wrong, and the bug survived in the Java standard library for nine years before anyone noticed.

If lo and hi are both large, lo + hi overflows a 32-bit int. The maximum is 2,147,483,647; two indices near 1.5 billion sum past it and the result goes negative. a[negative] is undefined behaviour.

C++
int mid = lo + (hi - lo) / 2;   // correct: hi - lo cannot overflow

hi - lo is at most the array size, so it is always safe. Write it this way every time, out of habit.

ℹ️

For an array of a few hundred elements this never triggers, so your code "works" in the lab. It fails on a large dataset in a placement test. Habits formed on small inputs are what break on real ones.

Tracing it

Search for 66 in {11, 22, 33, 44, 55, 66, 77}.

Step lo hi mid a[mid] Action
1 0 6 3 44 44 < 66, go right: lo = 4
2 4 6 5 66 found, return 5

Search for 50:

Step lo hi mid a[mid] Action
1 0 6 3 44 44 < 50, lo = 4
2 4 6 5 66 66 > 50, hi = 4
3 4 4 4 55 55 > 50, hi = 3
4 - - - - lo(4) > hi(3), exit, return -1

Step 3 is the one lo < hi would have skipped.

Why it is O(log n)

Start with n elements. After one step: n/2. After two: n/4. After k steps: n / 2^k. The search ends when that reaches 1, so 2^k = n, so k = log2(n).

For n = 1,000,000, that is 20 steps. A linear search would average 500,000.

Finding the first occurrence

Plain binary search returns some index holding the key. With duplicates, you often need the first one. This variant is the standard follow-up question.

C++
int lower_bound_index(const int a[], int n, int key) {
    int lo = 0, hi = n;            // note: hi = n, not n - 1
    while (lo < hi) {              // note: strict <
        int mid = lo + (hi - lo) / 2;
        if (a[mid] < key) lo = mid + 1;
        else              hi = mid;
    }
    return lo;   // first index with a[i] >= key; n if there is none
}

int main() {
    int a[] = {1, 2, 2, 2, 5, 9};
    cout << lower_bound_index(a, 6, 2) << "\n";   // 1
    cout << lower_bound_index(a, 6, 3) << "\n";   // 4
    cout << lower_bound_index(a, 6, 9) << "\n";   // 5
    return 0;
}

This is the other consistent pair: hi = n, lo < hi, hi = mid. The range is half-open [lo, hi), which is why hi starts one past the end. Never has an infinite loop, never misses an element. It is what C++'s std::lower_bound does.

When binary search does not apply

  • The array is not sorted. Sorting costs O(n log n), so for a single lookup

linear search is cheaper.

  • The data is a linked list. Jumping to the middle is O(n), which destroys

the benefit.

  • You will search only once or twice. Sort plus search is O(n log n); one

linear scan is O(n).

💡

Binary search is not only for arrays. Any question with a yes/no answer that flips exactly once as a parameter increases can be binary searched. That idea - binary search on the answer - is one of the highest-value patterns in interviews, and it gets a full lesson in the DSA Interview Patterns course.