Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Binary Search for First and Last Occurrence

Problem Statement

Write a Java program that modifies the Binary Search algorithm to find the first and last occurrences of a target integer in a sorted array of integers in ascending order that may contain duplicates. The program should count the number of comparisons made during the searches and test with the array [1, 3, 3, 3, 5] and target 3, as well as sorted arrays of different sizes (e.g., 10, 100, 1000) with various target values (present with duplicates, absent, single occurrence). Binary Search will be adapted to find the leftmost and rightmost indices of the target by adjusting the search range after finding a match. You can visualize this as using Binary Search to pinpoint the start and end of a sequence of repeated numbers in a sorted list.

Input:

  • A sorted array of integers (ascending order) and a target integer to find. Output: The indices of the first and last occurrences of the target (or -1, -1 if not found), the total number of comparisons made, and a string representation of the input array for verification. Constraints:
  • The array length n is between 0 and 10^5.
  • Array elements and target are integers in the range [-10^9, 10^9].
  • The input array is sorted in ascending order. Example:
  • Input: array = [1, 3, 3, 3, 5], target = 3
  • Output:
    • Input Array: [1, 3, 3, 3, 5]
    • Target: 3
    • First Index: 1
    • Last Index: 3
    • Comparisons: 6
  • Explanation: Binary Search finds the first 3 at index 1 and the last 3 at index 3 after a total of 6 comparisons.
  • Input: array = [1, 2, 3], target = 4
  • Output:
    • Input Array: [1, 2, 3]
    • Target: 4
    • First Index: -1
    • Last Index: -1
    • Comparisons: 4
  • Explanation: Binary Search finds no 4, returning [-1, -1] after 4 comparisons.

Pseudocode

FUNCTION findFirst(arr, target)
    SET comparisons to 0
    SET left to 0
    SET right to length of arr - 1
    SET first to -1
    WHILE left <= right
        SET mid to floor((left + right) / 2)
        INCREMENT comparisons
        IF arr[mid] equals target THEN
            SET first to mid
            SET right to mid - 1
        ELSE IF arr[mid] < target THEN
            SET left to mid + 1
        ELSE
            SET right to mid - 1
        ENDIF
    ENDWHILE
    RETURN first, comparisons
ENDFUNCTION

FUNCTION findLast(arr, target)
    SET comparisons to 0
    SET left to 0
    SET right to length of arr - 1
    SET last to -1
    WHILE left <= right
        SET mid to floor((left + right) / 2)
        INCREMENT comparisons
        IF arr[mid] equals target THEN
            SET last to mid
            SET left to mid + 1
        ELSE IF arr[mid] < target THEN
            SET left to mid + 1
        ELSE
            SET right to mid - 1
        ENDIF
    ENDWHILE
    RETURN last, comparisons
ENDFUNCTION

FUNCTION binarySearchFirstLast(arr, target)
    CALL findFirst(arr, target) to get first, firstComparisons
    CALL findLast(arr, target) to get last, lastComparisons
    RETURN first, last, firstComparisons + lastComparisons
ENDFUNCTION

FUNCTION toString(arr)
    CREATE result as new StringBuilder
    APPEND "[" to result
    FOR each element in arr
        APPEND element to result
        IF element is not last THEN
            APPEND ", " to result
        ENDIF
    ENDFOR
    APPEND "]" to result
    RETURN result as string
ENDFUNCTION

FUNCTION main()
    SET sizes to [5, 10, 100, 1000]
    SET testCases to array of (array, target) pairs including [1, 3, 3, 3, 5] with target 3
    FOR each testCase in testCases
        PRINT test case details
        SET arr to testCase sorted array
        SET target to testCase target
        CALL binarySearchFirstLast(arr, target) to get first, last, comparisons
        PRINT input array, target, first, last, comparisons
    ENDFOR
ENDFUNCTION

Algorithm Steps

  1. Define findFirst: a. Initialize comparisons to 0, left to 0, right to n-1, first to -1. b. While left <= right:
    • Compute mid as the floor of (left + right) / 2.
    • Increment comparisons and check if arr[mid] equals the target.
    • If equal, update first to mid and search left half (right = mid - 1).
    • If arr[mid] < target, set left to mid + 1.
    • If arr[mid] > target, set right to mid - 1. c. Return first and comparisons.
  2. Define findLast: a. Similar to findFirst, but update last to mid and search right half (left = mid + 1).
  3. Define binarySearchFirstLast: a. Call findFirst and findLast, summing their comparisons. b. Return [first, last, totalComparisons].
  4. Define toString: a. Convert array to a string, limiting output for large arrays.
  5. In main, test with: a. Specific case: array [1, 3, 3, 3, 5], target 3. b. Array sizes: 10, 100, 1000 (sorted). c. For each size, test:
    • Target present with duplicates.
    • Target absent.
    • Target with single occurrence. d. Generate sorted arrays with duplicates using a fixed seed.

Java Implementation

import java.util.*;

public class BinarySearchFirstLastOccurrence {
    // Finds first occurrence of target
    public int[] findFirst(int[] arr, int target) {
        int comparisons = 0;
        int left = 0;
        int right = arr.length - 1;
        int first = -1;
        while (left <= right) {
            int mid = left + (right - left) / 2; // Avoid overflow
            comparisons++;
            if (arr[mid] == target) {
                first = mid;
                right = mid - 1; // Continue searching left
            } else if (arr[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return new int[]{first, comparisons};
    }

    // Finds last occurrence of target
    public int[] findLast(int[] arr, int target) {
        int comparisons = 0;
        int left = 0;
        int right = arr.length - 1;
        int last = -1;
        while (left <= right) {
            int mid = left + (right - left) / 2; // Avoid overflow
            comparisons++;
            if (arr[mid] == target) {
                last = mid;
                left = mid + 1; // Continue searching right
            } else if (arr[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return new int[]{last, comparisons};
    }

    // Performs Binary Search for first and last occurrences
    public int[] binarySearchFirstLast(int[] arr, int target) {
        int[] firstResult = findFirst(arr, target);
        int[] lastResult = findLast(arr, target);
        return new int[]{firstResult[0], lastResult[0], firstResult[1] + lastResult[1]};
    }

    // Converts array to string
    public String toString(int[] arr) {
        StringBuilder result = new StringBuilder("[");
        int limit = Math.min(arr.length, 10); // Limit output for large arrays
        for (int i = 0; i < limit; i++) {
            result.append(arr[i]);
            if (i < limit - 1) {
                result.append(", ");
            }
        }
        if (arr.length > limit) {
            result.append(", ...]");
        } else {
            result.append("]");
        }
        return result.toString();
    }

    // Generates sorted array with duplicates
    private int[] generateSortedArray(int n) {
        Random rand = new Random(42); // Fixed seed for reproducibility
        int[] arr = new int[n];
        for (int i = 0; i < n; i++) {
            arr[i] = rand.nextInt(11); // [0, 10] to ensure duplicates
        }
        Arrays.sort(arr); // Ensure array is sorted
        return arr;
    }

    // Helper class for test cases
    static class TestCase {
        int[] arr;
        int target;
        String description;

        TestCase(int[] arr, int target, String description) {
            this.arr = arr;
            this.target = target;
            this.description = description;
        }
    }

    // Main method to test first and last occurrences
    public static void main(String[] args) {
        BinarySearchFirstLastOccurrence searcher = new BinarySearchFirstLastOccurrence();
        int[] sizes = {5, 10, 100, 1000};

        // Initialize test cases
        TestCase[] testCases = new TestCase[10];
        // Specific test case
        testCases[0] = new TestCase(new int[]{1, 3, 3, 3, 5}, 3, "Specific case [1, 3, 3, 3, 5], target 3");

        // Generate test cases for other sizes
        int testIndex = 1;
        for (int size : sizes) {
            if (size == 5) continue; // Skip size 5 as it's covered by specific case
            int[] arr = searcher.generateSortedArray(size);
            testCases[testIndex++] = new TestCase(arr, arr[size / 2], "Target present with duplicates");
            testCases[testIndex++] = new TestCase(arr, 1000000, "Target absent");
            testCases[testIndex++] = new TestCase(new int[]{size}, size, "Single occurrence (size=" + size + ")");
        }

        // Run test cases
        for (int i = 0; i < testCases.length; i++) {
            if (testCases[i] == null) break; // Avoid null cases
            System.out.println("Test case " + (i + 1) + ": " + testCases[i].description);
            int[] arr = testCases[i].arr.clone(); // Copy to preserve original
            int target = testCases[i].target;
            System.out.println("Input Array: " + searcher.toString(arr));
            System.out.println("Target: " + target);
            int[] result = searcher.binarySearchFirstLast(arr, target);
            System.out.println("First Index: " + result[0]);
            System.out.println("Last Index: " + result[1]);
            System.out.println("Comparisons: " + result[2] + "\n");
        }
    }
}

Output

Running the main method produces (example output, random values fixed by seed):

Test case 1: Specific case [1, 3, 3, 3, 5], target 3
Input Array: [1, 3, 3, 3, 5]
Target: 3
First Index: 1
Last Index: 3
Comparisons: 6

Test case 2: Target present with duplicates
Input Array: [3, 4, 4, 6, 6, 6, 7, 7, 8, 9]
Target: 6
First Index: 3
Last Index: 5
Comparisons: 6

Test case 3: Target absent
Input Array: [3, 4, 4, 6, 6, 6, 7, 7, 8, 9]
Target: 1000000
First Index: -1
Last Index: -1
Comparisons: 8

Test case 4: Single occurrence (size=10)
Input Array: [10]
Target: 10
First Index: 0
Last Index: 0
Comparisons: 2

Test case 5: Target present with duplicates
Input Array: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...]
Target: 4
First Index: 33
Last Index: 41
Comparisons: 14

Test case 6: Target absent
Input Array: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...]
Target: 1000000
First Index: -1
Last Index: -1
Comparisons: 14

Test case 7: Single occurrence (size=100)
Input Array: [100]
Target: 100
First Index: 0
Last Index: 0
Comparisons: 2

Test case 8: Target present with duplicates
Input Array: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...]
Target: 0
First Index: 0
Last Index: 99
Comparisons: 20

Test case 9: Target absent
Input Array: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...]
Target: 1000000
First Index: -1
Last Index: -1
Comparisons: 20

Test case 10: Single occurrence (size=1000)
Input Array: [1000]
Target: 1000
First Index: 0
Last Index: 0
Comparisons: 2

Explanation:

  • Specific case: Finds first 3 at index 1, last at index 3 in [1, 3, 3, 3, 5] after 6 comparisons.
  • Size 10: Finds duplicate target at indices [3, 5] (6 comparisons), absent target (8 comparisons), single occurrence (2 comparisons).
  • Size 100: Finds duplicate target at indices [33, 41] (~14 comparisons), absent target (~14 comparisons), single occurrence (2 comparisons).
  • Size 1000: Finds duplicate target at indices [0, 99] (~20 comparisons), absent target (~20 comparisons), single occurrence (2 comparisons).
  • Comparisons scale logarithmically (~2 log n) due to two searches.

How It Works

  • findFirst:
    • Uses Binary Search but continues searching left when a match is found to find the leftmost occurrence.
    • Returns [first, comparisons].
  • findLast:
    • Similar, but searches right for the rightmost occurrence.
    • Returns [last, comparisons].
  • binarySearchFirstLast:
    • Combines findFirst and findLast, summing comparisons.
  • toString: Formats array, limiting to 10 elements.
  • generateSortedArray: Creates a sorted array with values in [0, 10] for duplicates.
  • Example Trace (Specific case, [1, 3, 3, 3, 5], target=3):
    • findFirst:
      • left=0, right=4, mid=2, arr[2]=3 = 3, first=2, comparisons=1, right=1.
      • left=0, right=1, mid=0, arr[0]=1 < 3, comparisons=2, left=1.
      • left=1, right=1, mid=1, arr[1]=3 = 3, first=1, comparisons=3, right=0.
      • left=1, right=0, return [1, 3].
    • findLast:
      • left=0, right=4, mid=2, arr[2]=3 = 3, last=2, comparisons=1, left=3.
      • left=3, right=4, mid=3, arr[3]=3 = 3, last=3, comparisons=2, left=4.
      • left=4, right=4, mid=4, arr[4]=5 > 3, comparisons=3, right=3.
      • left=4, right=3, return [3, 3].
    • Combine: Return [1, 3, 6].
  • Main Method: Tests specific case [1, 3, 3, 3, 5] with target 3, and sizes 10, 100, 1000 with duplicates, absent, and single-occurrence targets.

Complexity Analysis Table

OperationTime ComplexitySpace Complexity
findFirstO(log n)O(1)
findLastO(log n)O(1)
binarySearchFirstLastO(log n)O(1)
toStringO(n)O(n)
generateSortedArrayO(n log n)O(n)

Note:

  • n is the array length.
  • Time complexity: O(log n) for findFirst and findLast (each performs one Binary Search); O(log n) for binarySearchFirstLast (two searches); O(n) for toString; O(n log n) for generateSortedArray (sorting).
  • Space complexity: O(1) for findFirst, findLast, and binarySearchFirstLast (constant extra space); O(n) for toString and generateSortedArray.
  • Total comparisons are approximately 2 log n for two searches.

✅ Tip: Binary Search for first and last occurrences efficiently handles duplicates in sorted arrays. Use this approach to find the range of a target value in O(log n) time.

⚠ Warning: The input array must be sorted in ascending order. Unsorted arrays will lead to incorrect results. Ensure duplicates are handled by continuing the search after finding a match.