Article outline:
  • Ordinary binary search

  • Binary search on the left edge

  • Dichotomous cable on the right boundary

  • Binary search implementation details summary

  • Shorthand CARDS







The idea of binary search is very easy, but the difficulty lies in the implementation details of the code. Most of the time, it is inefficient to think about the abstract code. The problems of these details can be better clarified through the GIF.


Of course, there are many ways to understand and memorize an algorithm, you can write a poem, you can draw a picture, you can draw an animation with powerpoint. But I prefer a debugable animation, which means it’s up to the code you write, and you can’t predict it.
One graph to figure out merge sort


Of course, algorithm visualization has two kinds of dynamic and static, static algorithm visualization is also a pretty awesome method. Personally, I think the highest level of algorithm visualization is that through a static picture, you can interpret the characteristics of the whole algorithm and various details.
Again, what kind of static image you get is entirely up to your code, and that’s what makes algorithmic visualization so appealing.
Chromatogram visualization of sorting algorithm
The graph shows heap sort




Then enter the main topic of this paper, binary search.


Here is an ordered array
let nums = [1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16]Copy the code


We will use binary search to find the index of element 7 in this array, which is the index of the element shown in the pointer below.


1 Ordinary binary search

The code looks like this:


let binatySearch = function(nums){
  let left = 0, right = nums.length - 1
  
  while (left <= right) {
    let mid = (left + right) >> 1if (nums[mid].value == target) {
      return mid
    }
    else if (nums[mid].value < target) {
      left = mid + 1
    }
    else { // nums[mid].value > target
      right = mid - 1}}return - 1 
}Copy the code
Binary search is definitely hard to understand looking at the code, but it’s pretty easy to understand when combined with the animation:


This public number before an article specifically introduced the cursor animation, how to understand the bubble algorithm of the boundary conditions?

Now the cursor animation has been updated to introduce a new animation element – line segment, let’s call this new animation line segment animation. Line segment animation has part of the characteristics of static visualization.


The interval represented by the left and right Pointers in the figure is [left, right].

From the line segment graph, we can understand the interval variation of left and right Pointers well.

We start with [left, right] representing the elements of the entire array, and then we keep reducing the range in half by the middle value. Because we have an ordered array, we shrink the right pointer if the middle value is greater than the element we’re looking for, and we shrink the left pointer if the middle value is less than the element we’re looking for.


If you understand the range variation, you will understand the code for mid + 1 and mid – 1.


Of course, the most important step in binary search is to take the left and right Pointers of the middle value is also a skill, details can be seen in my article: the clever use of bitwise operation – find the average of two integers

The examples in this article are written in the dumbest possible way to be easy to understand (lazy).

To make the animation easier to understand, elements selected as intermediate values in the diagram are highlighted in other colors.


Note that while (left <= right) why is <=?

While (left < right) = while (left < right)

As you can see, the animation ends when the left and right Pointers overlap, without returning the element we expected. The absence of the equal sign is equivalent to missing the interval represented when the left and right Pointers are equal.



The disadvantages of ordinary binary search

Consider the following ordered array with repeating elements:


let nums = [1.2.3.4.4.6.6.6.6.10.11.12.13.14.15.16.16.16.16]Copy the code


There are multiple 6 elements in the array, if we want to return the leftmost 6 or the rightmost 6.



A normal binary search returns only the index of the first 6 found, such as the second 6 in the following animation, which is obviously not sufficient.



2 left boundary binary search

How do I find the leftmost boundary element 6? The code looks like this:


// [left, mid) mid [mid+1, right)
let binatySearchLeftBound = function(nums){
  let left = 0, right = nums.length
  while (left < right) {
    let mid = (left + right) >> 1
    if (nums[mid].value == target) {
      right = mid
    }
    else if (nums[mid].value < target) {
      left = mid + 1
    }
    else { // data[mid].value > target
      right = mid
    }
  }
  if(nums[left].value ! = target)return - 1
  return left
}Copy the code
It’s better to look at the animation with the code:



Why does the code return left at the end?
As you can see from the animation, the left and right Pointers always overlap at the end, so it makes no difference whether you return left or right.


Why does the code loop condition while (left < right) not have an equal sign?
Let’s see what happens if we now add the equal sign while (left <= right) :

Here we can clearly see that the line segment is continuously extending down, and there is an infinite loop!!


If I put an equal sign, which is when the left and right Pointers overlap, then I’m going to say that the left is equal to the right is equal to the middle, and I’m going to keep saying right = mid, and I’m never going to get out of the loop. So details are the devil.


When does it return return-1?
In fact, it can be seen from the line segment that the interval represented by the left and right Pointers is constantly shrinking to the left edge, that is, from [6,6,6,6] all the way to [6]. Therefore, if the element pointed to by the pointer on the left is not 6, it means that there is no 6 in the array.



[left, right] [left, right] [left, right] [left, right] [left, right] [left, right]


Now we implement it again with left close right closed interval [left, right], as shown in the line segment below



Left closed right closed interval implementation



So instead of implementing the interval [left, right], the code would look like this:
// [left, mid-1] mid [mid+1, right]
let binatySearchLeftBoundClosed = function(nums){
  let left = 0, right = nums.length - 1
  while (left <= right) {
    let mid = (left + right) >> 1    
       
    if (nums[mid].value == target) {
      right = mid - 1
    }
    else if (nums[mid].value < target) {
      left = mid + 1
    }
    else { // data[mid].value > target
      right = mid - 1}}if(nums[left].value ! = target)return - 1
  return left
}Copy the code

The animation effect is as follows:


As you can see in this code, while (left <= right) has an equal sign.

Can we sum up a rule one here:

If [left, right), then the condition of the while loop is while (left < right);

If [left, right], then the condition of the while loop is while (left <= right);


Another difference in the above animation is that the animation ends with the right pointer to the left of the left pointer.

This is determined by right = mid-1, which means that at the end of the loop, the index of the target element should be equal to the left pointer, which is equal to the right pointer plus 1.


So by combining [left, right] and [left, right], we seem to be able to summarize one more
Rule 2:
If (nums[mid]. Value == target) right = mid-1, return right + 1.


If (nums[mid]. Value == target) is implemented with right = mid, the subscript of the target element can be written as return right.

If (nums[mid]. Value == target) right = mid + 1, return right – 1.


3 right boundary binary search

If we were looking for the rightmost element 6:




The code looks like this:
// [left, mid) mid [mid+1, right)
binatySearchRightBound = function(nums){
  let left = 0, right = nums.length
  
  while (left < right) {
    let mid = (left + right) >> 1
    if (nums[mid].value == target) {
      left = mid + 1
    }
    else if (nums[mid].value < target) {
      left = mid + 1
    }
    else { // data[mid].value > target
      right = mid
    }
  }
​
  if (right <= 0 || nums[right- 1].value ! = target)return - 1
  return right- 1
}Copy the code

The animation effect is as follows:

Notice that at the end of the method, the return value becomes return right-1. Why is that?
If (nums[mid]. Value == target) left = mid (nums[mid]. Value == target) Return right-1 is also equivalent.


Also, return 1 also becomes, the condition of an if (right < = 0 | | nums [right – 1]. The value! = target), why?




Nums [right-1]. Value! If right-1 is not the target, the element does not exist. If right-1 is not the target, the element does not exist.


And let’s look at the condition right <= 0. If we are looking for an element that does not exist in the array, such as looking for 0, the animation looks like this.

Without the condition that right <= 0, the right pointer would undoubtedly exceed the bounds of the array as the interval’s right bounds shrink.


Left closed right closed interval implementation

As usual we change the code to an implementation of the interval [left, right], as follows:

// [left, mid) mid [mid+1, right)
binatySearchRightBoundClosed = function(nums, node, timeline){
  let left = 0, right = nums.length - 1
  while (left <= right) {
    let mid = (left + right) >> 1if (nums[mid].value == target) {
      left = mid + 1
    }
    else if (nums[mid].value < target) {
      left = mid + 1
    }
    else { // data[mid].value > target
      right = mid - 1}}if (right < 0|| nums[right].value ! = target)return - 1
  return right
}Copy the code
The final animation will look something like this:

[left, right] [left, right] can be equal to 0.


3 summary

Through the above implementation of binary search, we have summarized a few noteworthy details.




Right = nums.length determines whether while (left < right) in the while loop has an equal sign (rule 1).

The implementation of nums[mid]. Value == target determines the return value of the method.


Third, the boundary condition of reversion-1 is also worth noting. It should take full account of the case when the element is not found or the pointer overflows.



X References

This article refers to a recent popular project labuladong/Fucking algorithm on Github: binary search details.


Review past

Kafka time wheel Kafka time wheel Kafka time wheel


The graph shows heap sort


One graph to figure out merge sort


How to understand the bubble algorithm boundary conditions? Cursor animation to help you


Visualization of chromatograms for sorting algorithms


Clever bit operation – find the average value of two integers






Small card

Binary search implementation details:

Rule one:

If [left, right), then the condition of the while loop is while (left < right);

If [left, right], then the condition of the while loop is while (left <= right);

Rule 2:
If (nums[mid]. Value == target) right = mid-1, return right + 1.


If (nums[mid]. Value == target) is implemented with right = mid, the subscript of the target element can be written as return right.

If (nums[mid]. Value == target) right = mid + 1, return right – 1.




Byte armed: Explains all kinds of interesting programming with D3 animations.