Learn C++ STL

Lesson 1 of 9 · binary_search, lower_bound, upper_bound and equal_range

Module 13 · binary_search, lower_bound, upper_bound and equal_range

Binary Search: Halve a Sorted Row Until One Place Is Left

FreeReading

In this lesson

  • Say in one sentence what binary search does, and explain why it only works on a sorted range.
  • Call binary_search, lower_bound, upper_bound and equal_range on a sorted vector, and read each answer: a yes or no, a first position, or both bounds.
  • Turn a position into an index with it - v.begin(), check "not found" against v.end(), count the copies of x as upper minus lower, and state the cost, O(log n).

Amara's bookshop keeps a printed list of its 16 book prices, from the cheapest up. A customer asks whether any book costs exactly 300 taka. Another wants to know where a new 310-taka book would go on the list. Reading the list from the front can take 16 looks for each question. The children's game of "higher or lower" needs at most 5. This lesson is that game, played by the library on a sorted vector.

The guessing game: higher or lower

Zara thinks of a whole number from 1 to 100, and you guess. After each guess she says "higher", "lower" or "yes". A careless player starts at 1 and climbs, and may need 100 guesses. A careful player always guesses the middle of what is still possible.

Guess 50 first. If she says "lower", every number from 50 to 100 is gone in one answer. About 50 numbers are left, then 25, then 12, 6, 3 and 1. So the careful player never needs more than 7 guesses, because 7 halvings shrink 100 numbers to one.

That is the whole idea of binary search. Compare with the middle of the part still in question, and throw away the half that cannot hold the answer. "Binary" means two: each look splits what is left into two halves. So the number of looks grows with how many times you can halve n, not with n itself.

Sorted is the price you pay

The game works only because the numbers 1 to 100 are in order. When Zara says "lower" after 50, you know every number above 50 is wrong too. Amara's list works the same way only because it is printed from the cheapest up. Here is lower_bound's search for the first 300 on her 16 prices, look by look.

lower_bound(300) on Amara's 16 sorted prices, with the halves crossed out look by look lower_bound(300) on Amara's 16 prices: four looks 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 120 150 150 200 240 300 300 300 350 420 450 480 520 600 640 700 Look 1 350 Look 2 240 Look 3 300 Look 4 300 Result lands at index 5, the first 300 Each look compares the middle price with 300 and crosses out one half.

Look 1 found 350 in the middle. On a sorted list, everything to its right is 350 or more, so the first 300 cannot be there. On an unsorted list, a 300 could hide anywhere, and crossing out a half would throw it away. So sorting is the price you pay, once, for searches that take a handful of looks instead of a long walk.

Watch the library halve Amara's list

The C++ library does this search for you. Below are its two most useful searches on Amara's list, step by step. Each step shows the part still in question as [lo, hi): index lo is in, index hi is not. Each compared element is the middle one, at lo + (hi - lo) / 2. Every probe is the one GCC 12's library, libstdc++, really made, printed from a run on Compiler Explorer.

The same search as a table: each row is one step, and the part still in question is written as [lo, hi).

StepStill in questionComparedWhat happened
1[0, 16): all 16none yetAll 16 prices are still in question. lower_bound looks for the first price that is not below 300.
2[0, 16): 16index 8: 350Index 8 holds 350, not below 300. So the answer is index 8 or earlier: keep [0, 8).
3[0, 8): 8index 4: 240Index 4 holds 240, below 300. The answer is to its right: keep [5, 8).
4[5, 8): 3index 6: 300Index 6 holds 300, not below 300. The answer is index 6 or earlier: keep [5, 6).
5[5, 6): 1index 5: 300Index 5 holds 300, not below 300. The answer is index 5 or earlier: keep [5, 5).
6[5, 5): nonenoneNothing is left in question: lower_bound lands at index 5, the first 300. Four comparisons.

lower_bound wants the first price that is not less than 300. When the middle price is below 300, the answer must be to its right. When it is not below, the answer is that index or to its left. After four looks, nothing is left in question, and the bound sits at index 5.

The same search as a table: each row is one step, and the part still in question is written as [lo, hi).

StepStill in questionComparedWhat happened
1[0, 16): all 16none yetAll 16 prices are still in question. upper_bound looks for the first price above 300.
2[0, 16): 16index 8: 350Index 8 holds 350, above 300. So the answer is index 8 or earlier: keep [0, 8).
3[0, 8): 8index 4: 240Index 4 holds 240, not above 300. The answer is to its right: keep [5, 8).
4[5, 8): 3index 6: 300Index 6 holds 300, not above 300. The answer is to its right: keep [7, 8).
5[7, 8): 1index 7: 300Index 7 holds 300, not above 300. The answer is to its right: keep [8, 8).
6[8, 8): nonenoneNothing is left in question: upper_bound lands at index 8, the 350 just after the last 300. Four comparisons.

upper_bound wants the first price that is greater than 300. It makes the same first two looks, then keeps going right past the 300s, and lands at index 8. So on the same list, the two searches stop at the two ends of the 300s: index 5 before them, index 8 after them.

The syntax you need on day one

The four searches on a sorted range

#include <algorithm>

binary_search(v.begin(), v.end(), x)   true if x is there, false if not
lower_bound(v.begin(), v.end(), x)     the first position whose element is not less than x
upper_bound(v.begin(), v.end(), x)     the first position whose element is greater than x
equal_range(v.begin(), v.end(), x)     both positions at once, as a pair
it - v.begin()                         a position turned into an index
it == v.end()                          the position past the last element: nothing there
  • All four live in the header <algorithm> and take a range: two iterators, the start and one past the end, as Module 2 showed.
  • The range must already be sorted from small to large, by the same < the search uses.
  • binary_search returns a bool. The other three return positions: iterators, the markers for one place that Module 2 introduced.
  • A position may be v.end(), which means "after the last element". Check for it before you read *it.

So one search answers "is it there?", and the other three answer "where?".

binary_search says yes or no, and nothing else

binary_search answers one question: is an element equal to x anywhere in the range? It prints as 1 for true and 0 for false.

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> price{120, 150, 150, 200, 240, 300, 300, 300,
                      350, 420, 450, 480, 520, 600, 640, 700};
    cout << "a book at 300? " << binary_search(price.begin(), price.end(), 300) << '\n';
    cout << "a book at 310? " << binary_search(price.begin(), price.end(), 310) << '\n';
    return 0;
}
a book at 300? 1
a book at 310? 0

There is a book at 300, and none at 310. But the answer stops there. It does not say where the 300 is, or how many there are, or where 310 would go. Beginners reach for binary_search to find a position, and it has none to give. So use it for a plain yes or no, and ask lower_bound for a place.

lower_bound and upper_bound: two fences around the 300s

Picture a fence you can stand between two neighbouring prices. lower_bound puts it just before the first element that is not less than x. upper_bound puts it just after the last element that is not greater than x. Between the two fences sit exactly the copies of x.

lower_bound and upper_bound as two fences around the three 300s Two fences around the 300s 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 120 150 150 200 240 300 300 300 350 420 450 480 520 600 640 700 end() lower_bound(300) = index 5 upper_bound(300) = index 8 8 - 5 = 3 copies of 300 A fence stands between two prices, so a bound is a position, not a price.

Here are the two fences as a program. Each position is turned into an index by subtracting price.begin(), and the program reads the price just after each fence.

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> price{120, 150, 150, 200, 240, 300, 300, 300,
                      350, 420, 450, 480, 520, 600, 640, 700};
    auto first = lower_bound(price.begin(), price.end(), 300);
    auto after = upper_bound(price.begin(), price.end(), 300);
    cout << "lower_bound(300): index " << first - price.begin() << ", price " << *first << '\n';
    cout << "upper_bound(300): index " << after - price.begin() << ", price " << *after << '\n';
    cout << "copies of 300: " << after - first << '\n';
    return 0;
}
lower_bound(300): index 5, price 300
upper_bound(300): index 8, price 350
copies of 300: 3

lower_bound stopped at index 5 and found a 300 there. upper_bound stopped at index 8, at the 350, one past the last 300. The distance between them is 3: there are three books at 300 taka. So the count of x is always upper minus lower. It is 0 when x is missing, because then both fences stand in the same gap.

equal_range: both fences in one call

equal_range returns the two fences together as a pair: .first is the lower bound and .second the upper one. Module 1's pair works here unchanged.

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> price{120, 150, 150, 200, 240, 300, 300, 300,
                      350, 420, 450, 480, 520, 600, 640, 700};
    for (int x : {300, 310}) {
        auto range = equal_range(price.begin(), price.end(), x);
        cout << x << " at " << range.first - price.begin() << " to "
             << range.second - price.begin() << ", count "
             << range.second - range.first << '\n';
    }
    return 0;
}
300 at 5 to 8, count 3
310 at 8 to 8, count 0

For 300, the pair spans indexes 5 to 8, three books. For 310, both halves of the pair are index 8: an empty stretch, count 0, in the gap where a 310-taka book would go. So equal_range is lower_bound and upper_bound in one call, and lesson 02 counts its comparisons.

From a position to an index, and the end() check

A position is an iterator, not a number. On a vector, it - v.begin() turns it into an index: how many elements stand before it. When every element is less than x, lower_bound returns v.end(), the place after the last element. There is no element there, so compare with v.end() before you read *it.

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> price{120, 150, 150, 200, 240, 300, 300, 300,
                      350, 420, 450, 480, 520, 600, 640, 700};
    for (int x : {310, 100, 800}) {
        auto it = lower_bound(price.begin(), price.end(), x);
        int index = it - price.begin();
        if (it == price.end()) {
            cout << x << ": nothing costs that much; it goes last, at index " << index << '\n';
        } else {
            cout << x << ": goes at index " << index << ", before the " << *it << "-taka book\n";
        }
    }
    return 0;
}
310: goes at index 8, before the 350-taka book
100: goes at index 0, before the 120-taka book
800: nothing costs that much; it goes last, at index 16

The 310-taka book goes at index 8, between the 300s and the 350. A 100-taka book would go first. Nothing on the list costs 800 or more, so lower_bound returned end(), and index 16 is the list's size. So end() is C++'s "not found" for a position, where other languages often use -1.

What it costs: O(log n)

Each look halves the part in question. So the number of looks is about how many times n can be halved before one place is left. That number is the base-2 logarithm of n, written log n. The cost is O(log n), read "order log n". Reading from the front is O(n): in the worst case, every element once.

The table counts the comparisons GCC 12's lower_bound really made. A counting comparator searched for every x from -1 to 2n, on vectors holding 0, 2, 4 and so on. The counts come from one run on Compiler Explorer, GCC 12.2, g++ -O2 -std=c++17, and the same input always gives the same count.

Elements nReading from the front, at worstlower_bound, fewest measuredlower_bound, most measured
161645
10001000910
100000010000001920

A million prices need at most 20 comparisons, because 20 twos multiplied together make 1048576. Making the list a thousand times longer added only about 10 comparisons. So binary search turns a long walk into a short climb, as long as the range stays sorted.

Example 1: which bus routes stop at the school

The smallest program: a fixed list of route numbers, already sorted, and four yes-or-no questions.

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> route{3, 8, 11, 17, 24, 31, 40};   // already sorted
    for (int r : {17, 18, 3, 41}) {
        bool stops = binary_search(route.begin(), route.end(), r);
        cout << "route " << r << ": " << (stops ? "stops here" : "does not stop") << '\n';
    }
    return 0;
}
route 17: stops here
route 18: does not stop
route 3: stops here
route 41: does not stop

Routes 17 and 3 are on the list; 18 and 41 are not. The answer is a bool, kept in stops and printed as words. The list was typed in order, so no sorting was needed.

Run in Compiler
Example 2: the two bounds of each temperature

David wrote down a week's temperatures and sorted them. For three readings, the program prints both bounds as indexes and the copies between them.

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> temp{18, 21, 21, 21, 24, 26, 26, 30};   // a week of readings, sorted
    for (int x : {21, 26, 25}) {
        int lo = lower_bound(temp.begin(), temp.end(), x) - temp.begin();
        int hi = upper_bound(temp.begin(), temp.end(), x) - temp.begin();
        cout << x << ": lower " << lo << ", upper " << hi << ", copies " << hi - lo << '\n';
    }
    return 0;
}
21: lower 1, upper 4, copies 3
26: lower 5, upper 7, copies 2
25: lower 5, upper 5, copies 0

21 appears three times, at indexes 1 to 3, so its fences are 1 and 4. 25 never appears, so both fences stand at index 5, between 24 and 26, and the count is 0. The index idiom - temp.begin() is written once per call.

Run in Compiler
Example 3: Zara's spelling list

Zara keeps her spelling-bee words in alphabetical order. The same searches work on strings, because < compares strings in dictionary order. For a missing word, the program says where it would go.

#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
using namespace std;

int main() {
    vector<string> words{"anchor", "bridge", "candle", "garden", "island",
                         "lantern", "meadow", "puzzle", "rocket", "violin"};
    for (string w : {"garden", "gardens", "harbor", "zebra"}) {
        if (binary_search(words.begin(), words.end(), w)) {
            cout << w << ": on the list\n";
        } else {
            auto it = lower_bound(words.begin(), words.end(), w);
            cout << w << ": not there; it would go at index " << it - words.begin();
            if (it != words.end()) {
                cout << ", before " << *it;
            }
            cout << '\n';
        }
    }
    return 0;
}
garden: on the list
gardens: not there; it would go at index 4, before island
harbor: not there; it would go at index 4, before island
zebra: not there; it would go at index 10

"gardens" is not on the list, though "garden" is: a search wants equal, not close. Both "gardens" and "harbor" would go at index 4, before "island". "zebra" comes after every word, so lower_bound returned end(), and the program checked before printing a neighbour. This is the version you would actually write: ask yes or no first, then ask where.

Run in Compiler

Where this is used

  • git bisect. You tell Git one commit that has a bug and one older commit that does not. Git picks a commit in the middle of that range, and you test it and say good or bad. The range halves until the commit that brought the bug is left.
  • Python's bisect module. bisect_left returns the place to insert x before any equal entries: C++'s lower_bound. bisect_right returns the place after them: upper_bound.
  • Java's Arrays.binarySearch. It returns the index of the key when it is there. When it is missing, it returns -(insertion point) - 1, a negative number that hides the place to insert. C++ splits that job into two calls, binary_search for yes or no and lower_bound for the place.

Common mistakes

1. Searching a vector that is not sorted.

vector<int> cleaned{204, 211, 207, 202, 215, 209};   // in the order Alice cleaned them
for (int room : {207, 202, 215, 211}) {
    cout << room << ": " << (binary_search(cleaned.begin(), cleaned.end(), room) ? "cleaned" : "not yet") << '\n';
}

No message at the runner's flags. It printed not yet for all four rooms, and Alice had cleaned every one of them. The search halved a list with no order, so it crossed out the halves that held the rooms. Sort first with Module 12's sort, or keep the list sorted as it grows. You will make this mistake because a vector of numbers looks searchable, and nothing complains.

2. Comparing the answer with -1.

vector<int> price{120, 150, 200, 300, 350};
if (lower_bound(price.begin(), price.end(), 310) == -1) {
    cout << "no such book\n";
}

An error at the runner's flags: error: no match for 'operator==' (operand types are '__gnu_cxx::__normal_iterator<int*, std::vector<int> >' and 'int'). lower_bound returns a position, an iterator, and an iterator is never a number. The "not found" check is it == price.end(); to ask whether x itself is there, also check *it == x. Writing lower_bound(...) - price.begin() == -1 compiles, and is simply never true, because an index from lower_bound runs from 0 to the size. You will write -1 because C functions, and Java, use a negative number for "not found".

3. Reading *it when it is end().

vector<int> price{120, 150, 200, 300, 350};
auto it = lower_bound(price.begin(), price.end(), 800);
cout << "index " << it - price.begin() << '\n';
cout << "price " << *it << '\n';

No message at the runner's flags. One run on Compiler Explorer printed index 5 and then price 0, a price that is not on the list. Index 5 is end(), and reading there is undefined behaviour: any value, or a crash, may come out. Check it != price.end() before *it. You will skip the check because your test values are always on the list.

4. Forgetting <algorithm>.

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> price{120, 150, 200, 300, 350};
    cout << lower_bound(price.begin(), price.end(), 300) - price.begin() << '\n';
    cout << upper_bound(price.begin(), price.end(), 300) - price.begin() << '\n';
    return 0;
}

An error at the runner's flags: error: 'upper_bound' was not declared in this scope. The strange part: with the upper_bound line removed, the program compiles and prints 3. GCC 12's <vector> happens to bring lower_bound along, and not the other three. That is one library's accident, not a rule, so always include <algorithm>. You will forget it because the first search you wrote worked without it.

Brain teaser

On Amara's 16 prices, Bob runs lower_bound(price.begin(), price.end(), 100) and then lower_bound(price.begin(), price.end(), 800). How many comparisons does each search make? The two answers differ by one. Say why, before you count with the widget.

Replay the widget's rule from both ends. When the middle element is not below x, how many elements stay in question? When it is below x, does the middle element itself stay?

Exercise 1Easy

Zara runs the lost-and-found desk at the bus station. Every item found gets a numbered tag, and two items may carry the same number. Passengers bring claim tickets: a claim is good when an item with the ticket's number is at the desk.

Input. A line with n, then n tags in the order the items were found. Then a line with q, then q ticket numbers.

Output. One line per ticket, in order: yes if an item with that tag is at the desk, no if not.

Constraints. 1 <= n <= 200000, 1 <= tag <= 999999, and a tag may repeat. 1 <= q <= 200000.

Sample. Input 6, then 512 77 3020 77 918 45, then 4, then 918 100 77 3021 gives yes, no, yes and no.

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n = 0;
    cin >> n;
    vector<int> tags(n);   // the tags in the order the items were found
    for (int i = 0; i < n; i++) {
        cin >> tags[i];
    }
    // Get tags ready for a binary search, once.

    int q = 0;
    cin >> q;
    for (int i = 0; i < q; i++) {
        int ticket = 0;
        cin >> ticket;
        // Print yes if an item with this tag is at the desk, no if not.
    }
    return 0;
}

Graded as lost-and-found, a free problem in this module's problem set. The hidden tests include one tag, 60000 tags with many repeats, and tickets below the smallest tag and above the largest. One test has 200000 tags and 200000 tickets, none of them at the desk.

Run in Compiler
Exercise 2Medium

Alice takes the bus from the stop outside her house. The timetable lists when each bus leaves, in minutes after midnight, and it is the same every day. Alice arrives at a stop at minute t. How long does she wait? A bus that leaves at minute t itself still takes her.

Input. A line with n, then n departure times in increasing order. Then a line with q, then q arrival times.

Output. One line per arrival: the minutes Alice waits. When no bus leaves at t or later that day, she waits for the first bus of the next day.

Constraints. 1 <= n <= 1440, and the times are different whole numbers from 0 to 1439. 1 <= q <= 100000, and every arrival time is from 0 to 1439.

Sample. Departures 360 420 600 1080 1320 and arrivals 400 600 1400 0 give 20, 0, 400 and 360. At minute 1400 the last bus has gone, and the next one leaves 40 + 360 = 400 minutes later.

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    int n;
    cin >> n;
    vector<int> leaves(n);   // departure times in minutes after midnight, sorted
    for (int i = 0; i < n; i++) {
        cin >> leaves[i];
    }
    int q;
    cin >> q;
    for (int i = 0; i < q; i++) {
        int t;
        cin >> t;
        // Print how many minutes Alice waits for the next bus. If no bus
        // leaves at t or later today, she waits for the first bus tomorrow.
    }
    return 0;
}

Not graded on its own. lower_bound finds the first bus at or after t. When it returns end(), the next bus is tomorrow's first, and a day has 1440 minutes.

Run in Compiler
Exercise 3Hard

Amara and Bob both sell notebooks, each from a sorted price list. Maria wants to buy one notebook from each of them, at prices as close as possible. That way neither friend feels the other got the better deal. What is the smallest difference she can find?

Input. A line with n, then Amara's n prices in increasing order. Then a line with m, then Bob's m prices in increasing order.

Output. One line: the smallest difference between one of Amara's prices and one of Bob's.

Constraints. 1 <= n, m <= 100000, and every price is from 1 to 1000000000. A list may repeat a price.

Sample. Amara's 120 300 450 700 and Bob's 210 380 690 give 10: Amara's 700 against Bob's 690.

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    int n;
    cin >> n;
    vector<long long> amara(n);   // Amara's prices, sorted
    for (int i = 0; i < n; i++) {
        cin >> amara[i];
    }
    int m;
    cin >> m;
    vector<long long> bob(m);   // Bob's prices, sorted
    for (int i = 0; i < m; i++) {
        cin >> bob[i];
    }

    // Find the smallest difference between one of Amara's prices and one
    // of Bob's, and print it.

    return 0;
}

Not graded on its own. The idea the lesson only pointed at: lower_bound shows where one of Amara's prices would go in Bob's list, between its two nearest neighbours there. Check both neighbours, and mind the two ends of the list.

Run in Compiler

Common doubts

  • Why must the range be sorted? Can't the library check?

    Checking would mean reading every element, which is the O(n) walk binary search exists to avoid. So the library trusts you. On an unsorted range it still halves, and its answers are simply wrong, as mistake 1 showed.

  • Does lower_bound find the value?

    It finds a place: the first position whose element is not less than x. If x is there, that place holds the first copy of x. If not, it holds the next bigger element, or it is end(). So check it != v.end() && *it == x when you need the value itself.

  • When x appears several times, which one does binary_search find?

    None in particular: it only says yes. lower_bound gives the first copy, and upper_bound - 1 the last one, when x is there.

  • Is binary search always faster than reading from the front?

    Not for a handful of elements. A short walk costs little, and each binary-search look jumps around memory. Lesson 04 measures where the crossover sits on our compiler.

Key takeaways

  • Binary search compares with the middle of the part still in question and throws away the half that cannot hold the answer.
  • It needs a sorted range: on an unsorted one it gives wrong answers, with no message.
  • binary_search says yes or no and nothing else; lower_bound is the first position not less than x, upper_bound the first greater, and equal_range both.
  • it - v.begin() is the index, it == v.end() is "nothing there", and upper minus lower counts the copies of x.
  • The cost is O(log n): 4 to 5 comparisons for 16 elements and 19 to 20 for a million, measured.
  • Go deeper: Under the Hood, GCC's halving loop, branches and the cache (Pro).

Next, lesson 02 goes through every form of the four calls, one by one, each with its cost and its trap.

End of lesson 1

Mark it done, and your progress moves with you.

Next: Every Binary Search Call, One by One, With Its Cost