🏆 Phase 6 · Competitive Programming 🟠 Advanced MODULE 25

Common CP Algorithms — Binary Search & Two Pointers

⏱️ 25 min read
📖 Theory + Code
🧩 5 Quiz Questions
🏗️ 1 Challenge
Your progress in Phase 6 — Competitive Programming67%
🎯 What you'll learn: Two algorithmic patterns that appear in a huge fraction of competitive programming problems — binary search for locating values (and boundaries) in sorted data in O(log n), and the two-pointer technique for scanning arrays with a pair of moving indices in a single O(n) pass. You'll write a manual binary search, use the STL's lower_bound/upper_bound/binary_search, and apply two pointers to a pair-sum search and an in-place duplicate-removal problem.

Binary Search — The Idea

Given a sorted array, a linear scan checks elements one at a time — O(n) in the worst case. Binary search does better by throwing away half of the remaining search space on every comparison. It keeps two boundaries, lo and hi, checks the middle element, and then narrows the range based on whether the target is smaller or larger than that middle value.

Because the search space halves every step, binary search only needs about log₂(n) comparisons to find a target (or conclude it's absent) — for a million elements, that's around 20 comparisons instead of up to a million.

binary_search_manual.cpp
C++
#include <iostream>
#include <vector>
using namespace std;

int binarySearch(const vector<int>& arr, int target) {
    int lo = 0, hi = (int)arr.size() - 1;
    while (lo <= hi) {
        int mid = lo + (hi - lo) / 2;
        if (arr[mid] == target) return mid;
        else if (arr[mid] < target) lo = mid + 1;
        else hi = mid - 1;
    }
    return -1;
}

int main() {
    vector<int> arr = {2, 5, 8, 12, 16, 23, 38, 45, 56, 72, 91};
    int target = 23;

    int idx = binarySearch(arr, target);
    if (idx != -1) cout << "Found " << target << " at index " << idx << "\n";
    else cout << target << " not found\n";

    return 0;
}

Each loop iteration eliminates roughly half of the remaining candidates, so the loop runs about log₂(n) times — that's the source of binary search's O(log n) time complexity. This only works because the array is sorted: the comparison arr[mid] < target is only meaningful if you can trust that everything to the left of mid is smaller and everything to the right is larger.

⚠️
Watch the mid calculation
Writing mid = (lo + hi) / 2 can overflow if lo and hi are both large int values, since their sum might exceed INT_MAX before the division happens. mid = lo + (hi - lo) / 2 computes the same midpoint without ever adding two large numbers together — it's the safer form to reach for by habit.

STL Binary Search: lower_bound, upper_bound, binary_search

Writing binary search by hand is a rite of passage, but <algorithm> already provides tested, general versions that work on any sorted range. std::binary_search only tells you whether a value exists; std::lower_bound and std::upper_bound tell you where — which is what you usually actually need.

stl_binary_search.cpp
C++
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
    vector<int> arr = {2, 5, 8, 8, 8, 12, 16, 23, 38};

    bool found = binary_search(arr.begin(), arr.end(), 8);
    cout << "Contains 8: " << (found ? "yes" : "no") << "\n";

    auto lo = lower_bound(arr.begin(), arr.end(), 8);
    auto hi = upper_bound(arr.begin(), arr.end(), 8);
    cout << "First 8 at index " << (lo - arr.begin()) << "\n";
    cout << "Count of 8s: " << (hi - lo) << "\n";

    auto pos = lower_bound(arr.begin(), arr.end(), 20);
    cout << "First element >= 20 is " << *pos << " at index " << (pos - arr.begin()) << "\n";

    return 0;
}

lower_bound(first, last, x) returns an iterator to the first element that is not less than x (i.e. the first element >= x). upper_bound(first, last, x) returns an iterator to the first element strictly greater than x. Subtracting the two — upper_bound - lower_bound — gives you the number of elements equal to x, which is a common way to count occurrences in a sorted array without scanning it. Both run in O(log n), just like manual binary search.

💡
If lower_bound reaches the end
If every element in the range is smaller than x, lower_bound (and upper_bound) return the end() iterator. Always compare the result against arr.end() before dereferencing it, the same way you'd check a manual binary search's result against -1.

The Two-Pointer Technique — Pair Sum

The two-pointer technique uses two indices that move through a structure — often from opposite ends toward each other — to solve problems in a single pass instead of the O(n²) a naive nested loop would take. A classic example: given a sorted array, find a pair of elements that sum to a target value.

two_pointer_pair_sum.cpp
C++
#include <iostream>
#include <vector>
using namespace std;

bool findPairWithSum(const vector<int>& arr, int target) {
    int left = 0, right = (int)arr.size() - 1;
    while (left < right) {
        int sum = arr[left] + arr[right];
        if (sum == target) {
            cout << "Pair found: " << arr[left] << " + " << arr[right] << " = " << target << "\n";
            return true;
        } else if (sum < target) {
            left++;
        } else {
            right--;
        }
    }
    return false;
}

int main() {
    vector<int> arr = {1, 3, 5, 7, 9, 11, 14, 20};
    int target = 20;

    if (!findPairWithSum(arr, target)) {
        cout << "No pair sums to " << target << "\n";
    }

    return 0;
}

Why this works: left starts at the smallest value and right at the largest. If their sum is too small, the only way to increase it is to move left forward (since the array is sorted, every element after it is at least as large). If the sum is too big, move right backward. Either way, one pointer advances every iteration, so the pointers meet after at most n steps — O(n) total, with no extra memory beyond the two indices.

Compare to the brute-force approach
A nested loop checking every pair is O(n²) and doesn't need sorted input. Two pointers trade that flexibility for speed: it requires the array to be sorted first (which itself costs O(n log n) if it isn't already), but the pair-finding scan afterward is O(n) instead of O(n²).

Two Pointers — Removing Duplicates In-Place

Two pointers don't have to start at opposite ends — a common variant uses a slow pointer and a fast pointer moving in the same direction. This pattern shows up whenever you need to compact an array in-place, such as removing duplicates from a sorted array without allocating a second array.

remove_duplicates_two_pointer.cpp
C++
#include <iostream>
#include <vector>
using namespace std;

int removeDuplicates(vector<int>& arr) {
    if (arr.empty()) return 0;
    int slow = 0;
    for (int fast = 1; fast < (int)arr.size(); fast++) {
        if (arr[fast] != arr[slow]) {
            slow++;
            arr[slow] = arr[fast];
        }
    }
    return slow + 1;
}

int main() {
    vector<int> arr = {1, 1, 2, 2, 2, 3, 4, 4, 5};

    int newLength = removeDuplicates(arr);
    cout << "New length: " << newLength << "\n";
    cout << "Unique elements: ";
    for (int i = 0; i < newLength; i++) cout << arr[i] << " ";
    cout << "\n";

    return 0;
}

slow marks the boundary of the unique region built so far; fast scans ahead looking for the next value different from what slow points to. Whenever it finds one, slow advances and copies that new value into place. fast visits every element exactly once, so this is also O(n) time and O(1) extra space — no new container is allocated.

🧠
This is exactly how std::unique works
The standard library's std::unique (from <algorithm>) uses this same slow/fast pattern internally on a sorted range, and returns an iterator marking the new logical end. Understanding the hand-written version makes reading STL algorithm source — or reimplementing similar logic under a judge that restricts library use — much more approachable.

Choosing the Right Tool

Both patterns need sorted (or otherwise structured) input to work correctly. Here's a quick reference for when each one is the right call:

🔍
Binary search — O(log n)
Locate one specific value in sorted data, or find where a value would go. Needs random access (works on vector/arrays, not list).
📍
lower_bound / upper_bound — O(log n)
Find the first position >= or > a value — the building blocks for counting occurrences or inserting into a sorted structure.
↔️
Two pointers (opposite ends) — O(n)
Search for a pair (or triple) satisfying a sum/condition in sorted data — one linear pass instead of nested loops.
🐢🐇
Two pointers (slow/fast) — O(n)
Compact or filter an array in-place — removing duplicates, moving zeros, partitioning — without extra memory.
🧩 Knowledge Check — Lesson 25
Answer all 5 questions to test your understanding. Instant feedback on every answer.
1. What is the time complexity of binary search on a sorted array of n elements?
2. Binary search only works correctly if the input array is:
3. Why is mid = lo + (hi - lo) / 2 preferred over mid = (lo + hi) / 2?
4. What does std::lower_bound(arr.begin(), arr.end(), x) return?
5. What is the time complexity of the two-pointer technique scanning a sorted array once with two indices moving toward each other?
💪
Coding Challenge — Lesson 25
Apply what you learned · Advanced Level

Now combine both patterns from this lesson. Complete the challenge below in any C++ compiler.

Challenge: Fast Lookup & Pair Finder 🔎

Write a program that:

1. Defines a sorted vector<int> of at least 10 values
2. Implements your own binarySearch(arr, value) function that returns the index of value, or -1 if it isn't present
3. Implements a findPairWithSum(arr, target) function using the two-pointer technique that reports whether any pair of elements sums to target
4. In main(), test both functions — one call that should succeed and one that should fail for each — and print the results

Rules: No std::binary_search, lower_bound, or nested loops — write the searches by hand, as practiced in this lesson.
💡 Show hints if you're stuck
  • Reuse the binarySearch and findPairWithSum functions from Sections 1 and 3 as your starting point
  • Remember to keep lo <= hi as your binary search loop condition, and left < right for the two-pointer loop
  • Test with a value that doesn't exist in the array to confirm your binary search correctly returns -1
Finished this lesson?
Mark it complete to track your progress.
🎉

Lesson 25 Complete!

Binary search and two pointers are now in your toolkit. Last stop: a practice quiz covering everything from Phase 6 before you finish the whole course.

Module 25 of 26 Phase 6 — Competitive Programming