CS 411 Fall 2025  >  Outline & Supplemental Notes for September 24, 2025


CS 411 Fall 2025
Outline & Supplemental Notes
for September 24, 2025

Outline

Decrease by a Constant Factor [L 4.4]

Supplemental Notes

Binary Search: Implementation Issues

Extra Comparison?

A common question when implementing Binary Search is whether we should do an extra comparison to see if the middle item in the sequence is exactly the value we are searching for, and if so, end there.

Certainly we could do this. If we did, then the algorithm would still be logarithmic-time, and it would be faster—constant time!—for some instances. However, exceptional speed-ups would be rare, and in the worst case, the number of comparisons would double. Therefore, I generally avoid doing the extra comparison.

Tail Recursion Elimination

The last thing Binary Search does is its recursive call, whose return value it returns. This is called tail recursion. Tail-recursive calls can be transformed easily into efficient loops.

This illustrates a common phenomenon: we often describe algorithms recursively, but we implement them iteratively when we can. Sometimes we do this transformation only in part; when we discuss Quicksort, we will see that a natural implementation has two recursive calls, but we usually eliminate one of these, leaving one.

Generality

If we are careful when implementing Binary Search, we can make it work for a larger class of datasets than we might think at first glance.

Replacing the base-case equality check with one using the “<” operator allows our function to be used with value types that do not have the “==” operator defined.

We replace

[C++]

return *first == findme;

with

[C++]

return !(*first < findme) && !(findme < *first);

The first condition above is equality. The one we replace it with is equivalence.

We can also allow our function to be used with iterators that are not random-access.

We replace

[C++]

size_t size = last - first;

with

[C++]

size_t size = std::distance(first, last);

We similarly replace

[C++]

FDIter middle = first + size/2;

with

[C++]

FDIter middle = std::next(first, size/2);

For both of these changes, the initial code works only with random-access iterators, while the final code works with the more general category of forward iterators, while still remaining fast for random-access iterators.