title: Binary Search
slug: binary-search
difficulty: easy
leetcode_id: 704
leetcode_url: https://leetcode.com/problems/binary-search/
categories:
  - arrays
  - binary-search
patterns:
  - binary-search

function_signature: "def search(nums: list[int], target: int) -> int:"

test_cases:
  visible:
    - input: { nums: [-1, 0, 3, 5, 9, 12], target: 9 }
      expected: 4
    - input: { nums: [-1, 0, 3, 5, 9, 12], target: 2 }
      expected: -1
  hidden:
    - input: { nums: [5], target: 5 }
      expected: 0
    - input: { nums: [5], target: 1 }
      expected: -1
    - input: { nums: [1, 2, 3, 4, 5], target: 1 }
      expected: 0
    - input: { nums: [1, 2, 3, 4, 5], target: 5 }
      expected: 4
    - input: { nums: [1, 2, 3, 4, 5], target: 3 }
      expected: 2
    - input: { nums: [-5, -3, -1, 0, 2, 4], target: -3 }
      expected: 1
    - input: { nums: [1, 3], target: 2 }
      expected: -1

description: |
  Given an array of integers `nums` which is sorted in ascending order, and an integer `target`, write a function to search `target` in `nums`.

  If `target` exists, return its index. Otherwise, return `-1`.

  You must write an algorithm with **O(log n)** runtime complexity.

constraints: |
  - `1 <= nums.length <= 10^4`
  - `-10^4 < nums[i], target < 10^4`
  - All the integers in `nums` are **unique**
  - `nums` is sorted in **ascending order**

examples:
  - input: "nums = [-1,0,3,5,9,12], target = 9"
    output: "4"
    explanation: "9 exists in nums at index 4."
  - input: "nums = [-1,0,3,5,9,12], target = 2"
    output: "-1"
    explanation: "2 does not exist in nums, so return -1."

explanation:
  intuition: |
    Imagine you're looking up a word in a physical dictionary. You don't start at page 1 and read every page — you open to the middle, see if your word comes before or after, then flip to the middle of the remaining half. You repeat this until you find the word.

    Think of it like a **guessing game**: "I'm thinking of a number between 1 and 100." The optimal strategy is always to guess the middle — each guess eliminates half the possibilities.

    This is the essence of **binary search**: by comparing the target with the middle element, we eliminate half the search space with each comparison. Instead of O(n) comparisons with linear search, we need only O(log n) comparisons.

    The key requirement is that the array must be **sorted** — without sorted order, we can't know which half to eliminate.

  approach: |
    We solve this using **Iterative Binary Search**:

    **Step 1: Initialise the search boundaries**

    - Set `left = 0` (start of array)
    - Set `right = len(nums) - 1` (end of array)
    - Our target, if it exists, must be within `[left, right]`

    &nbsp;

    **Step 2: Loop while the search space is valid**

    - Continue while `left <= right` (search space has at least one element)
    - Calculate the middle index: `mid = left + (right - left) // 2`
    - Note: We use `left + (right - left) // 2` instead of `(left + right) // 2` to prevent integer overflow in some languages

    &nbsp;

    **Step 3: Compare and narrow the search space**

    - If `nums[mid] == target`: Found it! Return `mid`
    - If `nums[mid] < target`: Target is in the right half, set `left = mid + 1`
    - If `nums[mid] > target`: Target is in the left half, set `right = mid - 1`

    &nbsp;

    **Step 4: Handle not found**

    - If the loop exits (left > right), the target doesn't exist
    - Return `-1` to indicate not found

    &nbsp;

    Each iteration eliminates half the remaining elements, giving us O(log n) time complexity.

  common_pitfalls:
    - title: Integer Overflow in Mid Calculation
      description: |
        Using `(left + right) / 2` can cause integer overflow in languages with fixed-size integers (like Java or C++) when `left` and `right` are both large.

        For example, if `left = 2,000,000,000` and `right = 2,000,000,000`, their sum overflows a 32-bit integer.

        The safe formula `left + (right - left) // 2` avoids this by never computing a sum larger than `right`.
      wrong_approach: "mid = (left + right) / 2"
      correct_approach: "mid = left + (right - left) // 2"

    - title: Off-by-One in Loop Condition
      description: |
        Using `while left < right` instead of `while left <= right` can miss the target when it's the only element left in the search space.

        For example, if `left == right == 3` and `nums[3]` is the target, `while left < right` exits immediately without checking!

        The `<=` ensures we check when the search space has exactly one element.
      wrong_approach: "while left < right"
      correct_approach: "while left <= right"

    - title: Incorrect Boundary Updates
      description: |
        After checking `mid`, it must be excluded from the next search. Using `left = mid` or `right = mid` can cause infinite loops.

        If `nums[mid] < target`, we know `mid` isn't the answer, so `left = mid + 1` excludes it.
        If `nums[mid] > target`, we know `mid` isn't the answer, so `right = mid - 1` excludes it.
      wrong_approach: "left = mid or right = mid"
      correct_approach: "left = mid + 1 or right = mid - 1"

  key_takeaways:
    - "**Sorted array prerequisite**: Binary search only works on sorted data — always verify this condition"
    - "**O(log n) power**: Halving the search space each iteration is incredibly efficient — 1 billion elements needs only ~30 comparisons"
    - "**Template for variations**: This basic pattern extends to finding insertion points, rotated arrays, peak elements, and more"
    - "**Boundary precision matters**: Off-by-one errors are the most common bugs — always think carefully about `<` vs `<=` and `mid ± 1`"

  time_complexity: "O(log n). Each iteration halves the search space, so we need at most log₂(n) iterations."
  space_complexity: "O(1). Only three variables (`left`, `right`, `mid`) are used, regardless of input size."

solutions:
  - approach_name: Iterative Binary Search
    is_optimal: true
    code: |
      def search(nums: list[int], target: int) -> int:
          # Initialise search boundaries
          left, right = 0, len(nums) - 1

          # Continue while search space is valid
          while left <= right:
              # Calculate middle index (overflow-safe)
              mid = left + (right - left) // 2

              if nums[mid] == target:
                  # Found the target
                  return mid
              elif nums[mid] < target:
                  # Target is in right half, exclude mid
                  left = mid + 1
              else:
                  # Target is in left half, exclude mid
                  right = mid - 1

          # Target not found
          return -1
    explanation: |
      **Time Complexity:** O(log n) — Search space halves each iteration.

      **Space Complexity:** O(1) — Only constant extra space used.

      Classic binary search: compare the middle element with the target and eliminate half the search space each iteration. The loop terminates when the target is found or the search space is exhausted.

  - approach_name: Recursive Binary Search
    is_optimal: false
    code: |
      def search(nums: list[int], target: int) -> int:
          def binary_search(left: int, right: int) -> int:
              # Base case: search space exhausted
              if left > right:
                  return -1

              # Calculate middle index
              mid = left + (right - left) // 2

              if nums[mid] == target:
                  return mid
              elif nums[mid] < target:
                  # Search right half
                  return binary_search(mid + 1, right)
              else:
                  # Search left half
                  return binary_search(left, mid - 1)

          return binary_search(0, len(nums) - 1)
    explanation: |
      **Time Complexity:** O(log n) — Same number of comparisons as iterative.

      **Space Complexity:** O(log n) — Recursion stack depth.

      Recursive implementation with the same logic. While elegant, it uses O(log n) stack space. The iterative version is preferred for its O(1) space complexity.
