Binary Search Implementations
In the previous videos we introduced two versions of binary search, an iterative and a recursive version. Code snippets for the iterative versions are provided below and match the explanation provided in the videos.
You will find the recursive version to be slightly different however. If you remember, when starting the recursive version of binary search I made one modification to the algorithm - instead of returning the index value, I returned True
if the value was present in the list and False
otherwise. There was a reason for that.
When we use list slices with recursive calls the index value returned, even with a successful search will always be 0. Each sublist created will have a new set of indexes and it becomes very difficult to track the relationship betwee the index of an item in the sublist and where that item existed in the original list.
In the worst case scenario, where it takes a log n + 1
number of splits to find the element we end up with a single element array. The target, at this point, will always be at index position 0 regardless of its index position in the original array.
While we were able to demonstrate how recursion worked and provide a recursive implementation of binary search, this is not a good implementation. For all the code snippets below I have provided the correct implementation for the recursive version of binary search.
It's not that much different and it might even seem familiar to you - it combines the techniques we used in both the iterative version and recursive version of binary search.
For an explanation of this version and some of its implications, read the Python section below. You should then be able to look for code snippets in the language of your choice understand what the code is doing.
Python
Iterative Binary Search
def binary_search(list, target):
first = 0
last = len(list) - 1
while first <= last:
midpoint = (first + last)//2
if list[midpoint] == target:
return midpoint
elif list[midpoint] < target:
first = midpoint+1
else:
last = midpoint-1
return -1
Recursive Binary Search
The way we've tackled the recursive version of binary serach is by using a combination of a recursive call and the iterative approach with start
and end
variables to keep track of the portion of the list we're searching.
def recursive_binary_search(list, target, start=0, end=None):
if end is None:
end = len(list) - 1
if start > end:
return -1
mid = (start + end) // 2
if target == list[mid]:
return mid
else:
if target < list[mid]:
return recursive_binary_search(list, target, start, mid-1)
else:
return recursive_binary_search(list, target, mid+1, end)
The function has been modified to accept a start and end position with default values of 0
and None
respectively. The default values allow us to pass a list and target without needing to specify the arguments when invoking the function. Inside the body we set the appropriate values like we did when setting first
and last
in the iterative approach.
We then use start
and end
like we did in the iterative approach to keep track of the slice of the list we're searching through. The big difference here is that with each recursive call, by passing in different values for start
and end
we can define the slice of the list we're searching through. We're not actually executing a slice operation and we use only one list in memory, but each recursive call now focuses on a smaller slice of the original list.
This definitely has implications for the runtime of our code. One point that was not covered in the course was the cost of the slice operation used in the recursive version of the function. A slicing operation is not a constant time operation and has a runtime of O(k)
where k represents the size of the slice.
With the modified (and correct) version of recursive binary search, since we're not executing slicing operations we've eliminated that cost and our code now reflects the accurate runtime of the binary search algorithm at O(log n)
.
Slicing is also what lends to a space complexity of O(log n)
for the original recursive version since each slice required additional storage. Since we're not slicing the lsit anymore, the space complexity is now constant - no additional storage is used.
It's important to keep in mind that this doesn't change the fact that Python has a maximum recursion depth and an iterative approach is still preferred.
JavaScript
function binarySearch(array, key) {
let left = 0;
let right = array.length - 1;
while (left <= right) {
const mid = left + Math.floor((right - left) / 2);
if (array[mid] === key) {
return mid;
}
if (array[mid] < key) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
Swift
In Swift we can opt to use either the iterative or recursive implementation since Swift implements tail call optimization and maximum recursion depth is not an issue.
Iterative Binary Search
func binarySearch(_ a: [Int], key: Int) -> Int? {
var upperBound = 0
var lowerBound = a.index(before: a.endIndex)
while upperBound <= lowerBound {
let mid = (upperBound + lowerBound)/2
if a[mid] == key {
return mid
} else if a[mid] < key {
upperBound = mid + 1
} else {
lowerBound = mid - 1
}
}
return nil
}
Recursive Binary Search
func recursiveBinarySearch<T: Comparable>(_ a: [T], key: T, lower: Int, upper: Int) -> Int? {
if lower > upper {
return nil
} else {
let mid = (lower + upper)/2
if a[mid] == key {
return mid
} else {
if a[mid] < key {
return recursiveBinarySearch(a, key: key, lower: mid + 1, upper: upper)
} else {
return recursiveBinarySearch(a, key: key, lower: lower, upper: mid - 1)
}
}
}
}
Java
Iterative Binary Search
public class IterativeBinarySearch {
public static Integer binarySearch(int[] input, int target) {
int first = 0;
int last = input.length - 1;
while (first <= last) {
int mid = (first + last) / 2;
if (input[mid] == target) {
return mid;
} else if (input[mid] < target) {
first = mid + 1;
} else {
last = mid - 1;
}
}
return null;
}
}
Recursive Binary Search
public class RecursiveBinarySearch {
public static int binarySearch(int[] input, int target, int start, int end) {
if (start >= end) {
return -1;
} else {
int mid = start + (end - start) / 2;
if (target < input[mid]) {
return binarySearch(input, target, start, mid-1);
} else if (target > input[mid]) {
return binarySearch(input, target, mid+1, end);
} else {
return mid;
}
}
}
}
PHP
Iterative Binary Search
<?php
function iterative_binary_search($list, $target)
{
$first = 0;
$last = count($list)-1;
while ($first <= $last) {
$mid = ($first + $last) >> 1;
if ($list[$mid] == $target) {
return $mid;
} elseif ($list[$mid] > $target) {
$last = $mid - 1;
} elseif ($list[$mid] < $target) {
$first = $mid + 1;
}
}
return -1;
}
?>
Recursive Binary Search
<?php
function binary_search($list, $target, $start, $end)
{
if ($start > $end)
return -1;
$mid = ($start + $end) >> 1;
if ($list[$mid] == $target) {
return $mid;
} elseif ($list[$mid] > $target) {
return binary_search($list, $target, $start, $mid-1);
} elseif ($list[$mid] < $target) {
return binary_search($list, $target, $mid+1, $end);
}
}
?>