Start learning free

LESSON 279 OF 410 · DIVIDE AND CONQUER

Binary Search II in JavaScript (O(log n) Time Complexity)

Practice Recursive Read the full explanation

Implement a recursive Binary Search algorithm that given a sorted array of integers nums, finds and returns the index of a given value.

If the value doesn't exist in nums, return -1.


Example 1:

Input: nums = [1, 2, 4, 5], value = 4
Output: 2
Explanation: the element at index 2 is 4

Note:

Your algorithm should run in O(log n) time and use O(log n) extra space.


Hints:

Since our subproblems reduce to searching the value in a subarray, we will write a recursive helper function which takes nums, the bounds left and right and value, looks for value in the part of nums between indexes left and right (both included) and returns its index or -1 if not found.

Inside the function, we should compute the middle index mid and compare the element at mid to value. What should we do if:
  • the element at mid is equal to value?
  • the element at mid is less than value?
  • the element at mid is greater than value?

  • If the element at mid is equal to value, then we found our solution and we should return mid
  • If the element at mid is less than value, we should continue the search between indexes mid + 1 and right, i.e. return the result of the recursive call on that range
  • If the element at mid is greater than value, we should continue the search between indexes left and mid - 1, i.e. return the result of the recursive call on that range
When should the function return -1 tho?

At the start of the function, we should treat the base case: left > right. This means we reduced the searching area to an empty subarray, so there is no solution and we should return -1
STEP 1 OF 7
    Ctrl + Enter to check

    JAVASCRIPT · DIVIDE AND CONQUER

    Binary Search in JavaScript, explained

    Understanding the Problem

    The core challenge of this problem is to efficiently find the index of a given value in a sorted array using a recursive approach. Binary search is a classic algorithm that divides the search interval in half repeatedly, making it highly efficient for sorted arrays.

    Common applications of binary search include searching in databases, finding elements in sorted data structures, and solving algorithmic problems that require efficient search operations.

    Potential pitfalls include not handling the base case correctly, which can lead to infinite recursion or incorrect results.

    Approach

    To solve this problem, we can use the binary search algorithm, which works as follows:

    • Calculate the middle index of the current search interval.
    • Compare the middle element with the target value.
    • If the middle element is equal to the target value, return the middle index.
    • If the middle element is less than the target value, recursively search the right half of the interval.
    • If the middle element is greater than the target value, recursively search the left half of the interval.
    • If the search interval becomes empty (left index exceeds right index), return -1.

    Let's start with a naive approach and then optimize it.

    Naive Approach

    The naive approach would be to iterate through the array and check each element, which would take O(n) time. This is not optimal for large arrays.

    Optimized Approach

    The optimized approach is to use binary search, which reduces the time complexity to O(log n). The recursive implementation of binary search is elegant and leverages the divide-and-conquer strategy.

    Algorithm

    Here is a step-by-step breakdown of the recursive binary search algorithm:

    1. Define a recursive function binarySearch(nums, left, right) that takes the array and the current search interval as parameters.
    2. Calculate the middle index: mid = Math.floor((left + right) / 2).
    3. Compare the middle element with the target value:
      • If nums[mid] === value, return mid.
      • If nums[mid] < value, recursively search the right half: return binarySearch(nums, mid + 1, right).
      • If nums[mid] > value, recursively search the left half: return binarySearch(nums, left, mid - 1).
    4. If the search interval becomes empty (i.e., left > right), return -1.

    Code Implementation

    /**
     * Recursive binary search function.
     * @param {number[]} nums - Sorted array of integers.
     * @param {number} value - Target value to search for.
     * @param {number} left - Left index of the current search interval.
     * @param {number} right - Right index of the current search interval.
     * @returns {number} - Index of the target value or -1 if not found.
     */
    function binarySearch(nums, value, left, right) {
      // Base case: if the search interval is empty
      if (left > right) {
        return -1;
      }
    
      // Calculate the middle index
      const mid = Math.floor((left + right) / 2);
    
      // Compare the middle element with the target value
      if (nums[mid] === value) {
        return mid; // Target value found
      } else if (nums[mid] < value) {
        // Search the right half
        return binarySearch(nums, value, mid + 1, right);
      } else {
        // Search the left half
        return binarySearch(nums, value, left, mid - 1);
      }
    }
    
    // Wrapper function to initiate the binary search
    function search(nums, value) {
      return binarySearch(nums, value, 0, nums.length - 1);
    }
    
    // Example usage
    const nums = [1, 2, 4, 5];
    const value = 4;
    console.log(search(nums, value)); // Output: 2
    

    Complexity Analysis

    The time complexity of the recursive binary search algorithm is O(log n) because the search interval is halved in each recursive call.

    The space complexity is also O(log n) due to the recursive call stack, which grows logarithmically with the size of the input array.

    Edge Cases

    Potential edge cases include:

    • Empty array: The function should return -1.
    • Array with one element: The function should correctly identify if the element is the target value or not.
    • Target value not present in the array: The function should return -1.

    Examples:

    console.log(search([], 4)); // Output: -1
    console.log(search([1], 1)); // Output: 0
    console.log(search([1, 2, 4, 5], 3)); // Output: -1
    

    Testing

    To test the solution comprehensively, consider using a variety of test cases, including simple, complex, and edge cases. You can use testing frameworks like Jest or Mocha for automated testing.

    const assert = require('assert');
    
    // Test cases
    assert.strictEqual(search([1, 2, 4, 5], 4), 2);
    assert.strictEqual(search([1, 2, 4, 5], 3), -1);
    assert.strictEqual(search([], 4), -1);
    assert.strictEqual(search([1], 1), 0);
    
    console.log('All test cases pass');
    

    Thinking and Problem-Solving Tips

    When approaching such problems, consider the following tips:

    • Understand the problem requirements and constraints thoroughly.
    • Break down the problem into smaller subproblems and solve them step by step.
    • Think about edge cases and how your solution handles them.
    • Practice solving similar problems to improve your problem-solving skills.

    Conclusion

    In this blog post, we discussed the recursive binary search algorithm, its implementation in JavaScript, and its complexity analysis. Understanding and solving such problems is crucial for developing efficient algorithms and improving problem-solving skills.

    We encourage you to practice and explore further to deepen your understanding of binary search and other algorithmic techniques.

    Additional Resources

    For further reading and practice problems, consider the following resources:

    Keep going with JavaScriptAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free