12 Binary Search¶
Binary search is one of the most efficient searching algorithms with a time complexity of O(log n). You’ve already implemented a binary search once using a binary search tree. In this chapter you’ll reimplement binary search on a sorted list.
Two conditions need to be met for the type of binary search that this chapter describes:
- The collection must be sorted.
- The underlying collection must be able to perform random index lookup in constant time.
As long as the elements are sorted, a Dart List
meets both of these requirements.
Linear Search vs. Binary Search¶
The benefits of binary search are best illustrated by comparing it with linear search. Dart’s List
type uses a linear search to implement its indexOf
method. It traverses through the whole collection until it finds the first element:
11915015245221710531Linear search for the value 31
Binary search handles things differently by taking advantage of the fact that the collection is already sorted. Here’s an example of applying binary search to find the value 31:
11915015245221710531Binary search for the value 31
Instead of eight steps to find 31, it only takes three. Here’s how it works:
Step 1: Find the Middle Index¶
The first step is to find the middle index of the collection.
11915015245221710531
Step 2: Check the Element at the Middle Index¶
The next step is to check the element stored at the middle index. If it matches the value you’re looking for, return the index. Otherwise, continue to Step 3.
Step 3: Recursively Call Binary Search¶
The final step is to call the binary search recursively. However, this time, you’ll only consider the elements exclusively to the left or to the right of the middle index, depending on the value you’re searching for. If the value you’re searching for is less than the middle value, you search the left subsequence. If it is greater than the middle value, you search the right subsequence.
Each step effectively removes half of the comparisons you would otherwise need to perform.
In the example where you’re looking for the value 31 (which is greater than the middle element 22), you apply binary search on the right subsequence:
11915015522171052431
You continue these three steps until you can no longer split up the collection into left and right halves or until you find the value inside the collection.
Binary search achieves an O(log n) time complexity this way.
Implementation¶
Open the starter project for this chapter. Create a new lib folder in the root of your project. Then add a new file to it called binary_search.dart.
Adding an Extension on List¶
Add the following List
extension to binary_search.dart:
extension SortedList<E extends Comparable<dynamic>> on List<E> {
int? binarySearch(E value, [int? start, int? end]) {
// more to come
}
}
Things are relatively simple so far:
- You use
List
since it allows random access to any element by index. - Since you need to be able to compare elements, the value type must be
Comparable
. As mentioned in an earlier chapter, you can useComparable<E>
, but if you do your users will need to specifyList<num>
for integers rather thanList<int>
since onlynum
directly implementsComparable
. binarySearch
is recursive, so you need to pass in a range to search. The parametersstart
andend
are optional, so you can start the search without specifying a range, in which case the entire collection will be searched.- As is common for range indices,
start
is inclusive andend
is exclusive. That is, theend
index is one greater than the index it refers to. This makes it play well withlength
since the length of a zero-based list is always one greater than the last index.
Writing the Algorithm¶
Next, fill in the logic for binarySearch
by replacing // more to come
in the code above with the following:
// 1
final startIndex = start ?? 0;
final endIndex = end ?? length;
// 2
if (startIndex >= endIndex) {
return null;
}
// 3
final size = endIndex - startIndex;
final middle = startIndex + size ~/ 2;
// 4
if (this[middle] == value) {
return middle;
// 5
} else if (value.compareTo(this[middle]) < 0) {
return binarySearch(value, startIndex, middle);
} else {
return binarySearch(value, middle + 1, endIndex);
}
Here are the steps:
- First, you check if
start
andend
arenull
. If so, you create a range that covers the entire collection. - Then, you check if the range contains at least one element. If it doesn’t, the search has failed, and you return
null
. - Now that you’re sure you have elements in the range, you find the middle index of the range.
- You then compare the value at this index with the value that you’re searching for. If the values match, you return the middle index.
- If not, you recursively search either the left or right half of the collection.
Testing it Out¶
That wraps up the implementation of binary search! Open bin/starter.dart to test it out. Replace the contents of the file with the following:
import 'package:starter/binary_search.dart';
void main() {
final list = [1, 5, 15, 17, 19, 22, 24, 31, 105, 150];
final search31 = list.indexOf(31);
final binarySearch31 = list.binarySearch(31);
print('indexOf: $search31');
print('binarySearch: $binarySearch31');
}
Run that and you should see the following output in the console:
indexOf: 7
binarySearch: 7
7 is the index of the value 31 that you were looking for. Both search methods returned the same result.
Binary search is a powerful algorithm to learn and comes up often in programming interviews. Whenever you read something along the lines of “Given a sorted list…”, consider using the binary search algorithm. Also, if you’re given a problem that looks like it’s going to be O(n²) to search, consider doing some up-front sorting so you can use a binary search to reduce it down to the cost of the sort at O(n log n).
Note
You’ll learn more about sorting and the time complexity of sorting algorithms in future chapters.
Challenges¶
Try out the challenges below to further strengthen your understanding of binary searches. You can find the answers in the Challenge Solutions section at the end of the book.
Challenge 1: Binary Search as a Free Function¶
In this chapter, you implemented binary search as an extension of List
. Since binary search only works on sorted lists, exposing binarySearch
for every list (including unsorted ones) opens it up to being misused.
Your challenge is to implement binary search as a free function.
Challenge 2: Non-Recursive Search¶
Does recursion make your brain hurt? No worries, you can always perform the same task in a non-recursive way. Re-implement binarySearch
using a while
loop.
Challenge 3: Searching for a Range¶
Write a function that searches a sorted list and finds the range of indices for a particular element. You can start by creating a class named Range
that holds the start and end indices.
For example:
final list = [1, 2, 3, 3, 3, 4, 5, 5];
final range = findRange(list, value: 3);
findRange
should return Range(2, 5)
since those are the start and end indices for the value 3
.
Key Points¶
- Binary search is only a valid algorithm on sorted collections.
- Sometimes it may be beneficial to sort a collection to leverage the binary search capability for looking up elements.
- The
indexOf
method onList
uses a linear search with O(n) time complexity. Binary search has O(log n) time complexity, which scales much better for large data sets if you are doing repeated lookups.