Binary Search with Typescript

"What is a Binary Search?", I was asked this week. “I have no idea!”, I replied. I was interested to see if I could understand what a Binary Search was and whether I could write a Binary Search with Typescript.

TLDR;

Why Typescript?

Recently I have been taking the course Understanding Typescript on Udemy withMaximilian Schwarzmüller. I am finding this course to be a wonderful introduction to Typescript. The course is extremely clear and well thought out. Putting the Binary Search together in Typescript was be a good way to practice what I was learning.

What is a Binary Search?

The first Step was figuring out what a Binary Search was. A little Wikipedia reading and Youtube watching did the trick. The Binary Search entry on Wikipedia helped me to understand the basic concept.

const ary: number[] = [1, 2, ... 99, 100]

The concept is this: if you take an array of numbers, say from 1–100, and you want to search for the number 27, you do not need to pass through all 100 indexes of the array. That would not be the most efficient method.

Wouldn’t it be great if there was a way to chop out parts of the array that you knew you did not need to search?

If I chop my array of 100 numbers in half at index 50, and the value at the half-way point is greater than my target-number 27, then I know that I don’t have to search the 2nd-half of the array.

Wonderful!

We can keep iterating through the array this way, chopping left, or chopping right, until we either find the value we want, or we run out of array to chop.

So I suppose "Binary Search" might get it’s name because it either searches this-half of the array or that-half of the array. But perhaps there is another good reason to call it a “Binary Search”, namely… it’s logarithm.

Worst Case Performance

Before we get to the naming... lets look at another interesting feature of Binary Search. It follows something called "O(Log N)", which describes the Worst Case Performance of the search algorithm.

In computer science, best, worst, and average cases of a given algorithm express what the resource usage is at least, at most and on average, respectively. Usually the resource being considered is running time, i.e. time complexity, but it could also be memory or other resource. Wikipedia

But what on earth does O(Log N) actually mean?

Reading the Wikipedia page for Big O Notation was not a huge help to me (too many scary math symbols); but I found the following Youtube video by Matt Garland helped me understand just-enough, that I calculate the Worst Case Performance in JavaScript.

What we are trying to get at when we talk about "performance" here? We are asking the question:

"How many times do we need to iterate the search function before we can know whether the target value exists in the array?"

That’s a super-cool question!

If you watched the video above, or you are a computer science/math grad, you probably already have a good idea of the answer.

In short: the number of times you need to search the array, will be a logarithm of the length of the array. In JavaScript you can use Math.log2(ary.length) to know how many iterations you will need before you can safely stop searching the array.

Looking at this code example, you can see that the value of each binary digit doubles, from right-to-left. So the binary number 0b100000 has a Base-10 value of 32 and the binary number 0b111111 has a Base-10 value of 63.

This means that the index position of each digit, from right-to-left, is the O(Log N) of it’s Base-10 value. :)

Now we have a counter, a max iteration variable and an exit strategy, we can create the recursive compare function that will do the more interesting work of stepping through the array. Here is the basic handling of the variables:

const compare = (target: number, ary: number[], start: number, end: number):
number[] | number | boolean => {
// Find the range between the start and the end
// (we want to chop this in half to reduce the search)
const range: number = end - start
// Our midPoint is the value of the half-way point...
// Note: this could be a decimal!
const midPoint: number = start + (range / 2)
// Our midIndex must be an Integer to access the array index
const midIndex: number = Math.floor(midPoint)
// Finally get the value of the number half way through the array
// This is what we will compare to our target value
const midValue: number = ary[midIndex]
}

Once we have the search positions and the compare values, we can add the logic to we need to evaluate the next action. The rest of the function here handles the logic deciding whether we should exit or continue searching.

const compare = (target: number, ary: number[], start: number, end: number):
number[] | number | boolean => {
...
// Fail: Exit Strategy 1 - If we iterated past O(Log N)
if (iterCount > maxIterations && exit === FailExits.ologn) {
return false
}
iterCount += 1
// Sucess: We found the value we were looking for!
if (midValue === target) {
return midIndex
}
// Fail: Exit Strategy 2 - No more array to look in
// If the start and the middle of the range have the same
// array-index, then we know it's time to exit
if (start === midIndex && exit === FailExits.nochop) {
return false
}
// Continue: Chop Left
// We know the index of the target value has to be in
// the lower half of the array (if it exists)
if (target < midValue) {
start = start
end = midIndex
}
// Continue: Chop Right
// We know the index of the target value has to be in
// the upper half of the array (if it exists)
if (target > midValue) {
start = midIndex
}
// This compare function calls itself again with the
// new start and end values until it exits
return compare(target, ary, start, end)
}

And that's it!

Check out The Live Demo here and try tweaking the values to see what happens.

Conclusion

Using Typescript

Creating the Binary Search was a fun puzzle. Adding Typescript in the mix gave me the opportunity to learn some interesting things about how Typescript interacts with browser elements.

For example: there is such a thing as an HTMLElement type.

const elem: HTMLElement document.getElementById('foo')

And I also did not know that some types have un-documented functions, that prohibit the use of dot-notation.

I think passing the array + values to the next compare() call will incur extra memory overhead. Not enough to worry about in a simple web page, but this could be a real concern if you were iterating large quanities of data server-side.