Searching Algorithms

Introduction

Searching algorithms are fundamental techniques used to find specific elements or data within a collection. They are among the most frequently used algorithms in computer science, appearing in databases, operating systems, web applications, and virtually every software system.

The efficiency of a searching algorithm can dramatically impact application performance, especially when dealing with large datasets. Understanding when and how to use different searching algorithms is crucial for writing efficient code.

Overview

Linear search (also called sequential search) is the simplest searching algorithm. It checks every element in the collection sequentially until the target is found or the end is reached.

How It Works

  1. Start from the first element
  2. Compare each element with the target
  3. If found, return the index
  4. If end is reached without finding, return -1

Visual Example

Searching for 7 in array: [3, 1, 7, 9, 2]

Step 1: Compare 3 with 7 → Not match
Step 2: Compare 1 with 7 → Not match
Step 3: Compare 7 with 7 → Match! Return index 2

When to Use

  • Unsorted data: Works on any collection
  • Small datasets: Overhead of other algorithms not worth it
  • Single search: When searching only once
  • Linked lists: When random access is expensive

Advantages

  1. Simple to implement
  2. Works on unsorted data
  3. Works on any data structure
  4. No preprocessing required
  5. Best for small datasets

Disadvantages

  1. Slow for large datasets: O(n) time complexity
  2. Inefficient for multiple searches
  3. No optimization possible for sorted data

Overview

Binary search is an efficient algorithm for searching sorted arrays. It repeatedly divides the search interval in half, eliminating half of the remaining elements with each comparison.

Prerequisites

  • Data must be sorted
  • Random access to elements (works best with arrays)

How It Works

  1. Compare target with middle element
  2. If target equals middle, return the index
  3. If target is less than middle, search left half
  4. If target is greater than middle, search right half
  5. Repeat until found or search space is empty

Visual Example

Searching for 7 in sorted array: [1, 3, 5, 7, 9, 11, 13]

Step 1: [1, 3, 5, 7, 9, 11, 13]
        Middle = 7 → Match! Return index 3

Alternative: Searching for 11
Step 1: [1, 3, 5, 7, 9, 11, 13]
        Middle = 7, target > 7, search right

Step 2: [9, 11, 13]
        Middle = 11 → Match! Return index 5

Key Concepts

Search Space: The portion of the array currently being considered.

Invariant: After each iteration, if the target exists, it must be within the current search space.

Termination: When left > right, the search space is empty.

Binary Search Variations

1. Find Exact Match

Standard binary search to find if element exists.

def binary_search(arr, target):
    left, right = 0, len(arr) - 1

    while left <= right:
        mid = left + (right - left) // 2

        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            left = mid + 1
        else:
            right = mid - 1

    return -1

2. Find First Occurrence (Lower Bound)

Find the leftmost position where target can be inserted to maintain sorted order.

def lower_bound(arr, target):
    left, right = 0, len(arr)

    while left < right:
        mid = left + (right - left) // 2

        if arr[mid] < target:
            left = mid + 1
        else:
            right = mid

    return left

3. Find Last Occurrence (Upper Bound)

Find the rightmost position where target can be inserted.

def upper_bound(arr, target):
    left, right = 0, len(arr)

    while left < right:
        mid = left + (right - left) // 2

        if arr[mid] <= target:
            left = mid + 1
        else:
            right = mid

    return left

4. Find Peak Element

Find any element that is greater than its neighbors.

def find_peak(arr):
    left, right = 0, len(arr) - 1

    while left < right:
        mid = left + (right - left) // 2

        if arr[mid] < arr[mid + 1]:
            left = mid + 1
        else:
            right = mid

    return left

5. Search in Rotated Sorted Array

Binary search on array that was sorted then rotated.

def search_rotated(arr, target):
    left, right = 0, len(arr) - 1

    while left <= right:
        mid = left + (right - left) // 2

        if arr[mid] == target:
            return mid

        # Left half is sorted
        if arr[left] <= arr[mid]:
            if arr[left] <= target < arr[mid]:
                right = mid - 1
            else:
                left = mid + 1
        # Right half is sorted
        else:
            if arr[mid] < target <= arr[right]:
                left = mid + 1
            else:
                right = mid - 1

    return -1

Advanced Searching Techniques

1. Binary Search on Answer

Search for optimal value in a range where you can verify if a value works.

Template:

def binary_search_answer(lo, hi):
    result = -1

    while lo <= hi:
        mid = lo + (hi - lo) // 2

        if is_feasible(mid):
            result = mid
            # Try for better answer
            hi = mid - 1  # or lo = mid + 1
        else:
            lo = mid + 1  # or hi = mid - 1

    return result

Use Cases:

  • Minimizing maximum value
  • Maximizing minimum value
  • Finding threshold values

Find maximum/minimum in unimodal function (single peak/valley).

def ternary_search(left, right, func):
    while right - left > 2:
        mid1 = left + (right - left) // 3
        mid2 = right - (right - left) // 3

        if func(mid1) < func(mid2):
            left = mid1
        else:
            right = mid2

    # Check remaining elements
    result = left
    for i in range(left + 1, right + 1):
        if func(i) > func(result):
            result = i

    return result

Time Complexity: O(log₃ n)

Useful when target is closer to beginning of unbounded/infinite array.

def exponential_search(arr, target):
    if arr[0] == target:
        return 0

    # Find range for binary search
    i = 1
    while i < len(arr) and arr[i] <= target:
        i *= 2

    # Binary search in found range
    return binary_search(arr, target, i // 2, min(i, len(arr) - 1))

Time Complexity: O(log n)
Use Cases: Unbounded arrays, searching near beginning

Improved variant for uniformly distributed sorted data.

def interpolation_search(arr, target):
    left, right = 0, len(arr) - 1

    while left <= right and arr[left] <= target <= arr[right]:
        if left == right:
            return left if arr[left] == target else -1

        # Estimate position
        pos = left + ((target - arr[left]) * (right - left)) // (arr[right] - arr[left])

        if arr[pos] == target:
            return pos
        elif arr[pos] < target:
            left = pos + 1
        else:
            right = pos - 1

    return -1

Time Complexity:

  • Best/Average: O(log log n)
  • Worst: O(n)

Use Cases: Uniformly distributed data, phone books, dictionaries

Block-based search for sorted arrays.

import math

def jump_search(arr, target):
    n = len(arr)
    step = int(math.sqrt(n))
    prev = 0

    # Find block where element may be present
    while arr[min(step, n) - 1] < target:
        prev = step
        step += int(math.sqrt(n))
        if prev >= n:
            return -1

    # Linear search in block
    while arr[prev] < target:
        prev += 1
        if prev == min(step, n):
            return -1

    if arr[prev] == target:
        return prev

    return -1

Time Complexity: O(√n)
Use Cases: When backward movement is costly

Implementation

Complete Searching Implementation

class SearchingAlgorithms:
    """Collection of searching algorithms implementations."""
    
    def linear_search(self, arr, target):
        """
        Linear search - works on unsorted arrays.
        Time: O(n), Space: O(1)
        """
        for i in range(len(arr)):
            if arr[i] == target:
                return i
        return -1
    
    def binary_search(self, arr, target):
        """
        Binary search - requires sorted array.
        Time: O(log n), Space: O(1)
        """
        left, right = 0, len(arr) - 1
        
        while left <= right:
            mid = left + (right - left) // 2
            
            if arr[mid] == target:
                return mid
            elif arr[mid] < target:
                left = mid + 1
            else:
                right = mid - 1
        
        return -1
    
    def binary_search_recursive(self, arr, target, left=0, right=None):
        """
        Recursive binary search.
        Time: O(log n), Space: O(log n) due to recursion
        """
        if right is None:
            right = len(arr) - 1
        
        if left > right:
            return -1
        
        mid = left + (right - left) // 2
        
        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            return self.binary_search_recursive(arr, target, mid + 1, right)
        else:
            return self.binary_search_recursive(arr, target, left, mid - 1)
    
    def find_first_occurrence(self, arr, target):
        """
        Find first (leftmost) occurrence of target.
        Time: O(log n), Space: O(1)
        """
        left, right = 0, len(arr) - 1
        result = -1
        
        while left <= right:
            mid = left + (right - left) // 2
            
            if arr[mid] == target:
                result = mid
                right = mid - 1  # Continue searching left
            elif arr[mid] < target:
                left = mid + 1
            else:
                right = mid - 1
        
        return result
    
    def find_last_occurrence(self, arr, target):
        """
        Find last (rightmost) occurrence of target.
        Time: O(log n), Space: O(1)
        """
        left, right = 0, len(arr) - 1
        result = -1
        
        while left <= right:
            mid = left + (right - left) // 2
            
            if arr[mid] == target:
                result = mid
                left = mid + 1  # Continue searching right
            elif arr[mid] < target:
                left = mid + 1
            else:
                right = mid - 1
        
        return result
    
    def count_occurrences(self, arr, target):
        """
        Count total occurrences of target.
        Time: O(log n), Space: O(1)
        """
        first = self.find_first_occurrence(arr, target)
        if first == -1:
            return 0
        
        last = self.find_last_occurrence(arr, target)
        return last - first + 1
    
    def search_insert_position(self, arr, target):
        """
        Find position where target should be inserted to maintain sorted order.
        Time: O(log n), Space: O(1)
        """
        left, right = 0, len(arr)
        
        while left < right:
            mid = left + (right - left) // 2
            
            if arr[mid] < target:
                left = mid + 1
            else:
                right = mid
        
        return left
    
    def find_peak_element(self, arr):
        """
        Find any peak element (greater than neighbors).
        Time: O(log n), Space: O(1)
        """
        left, right = 0, len(arr) - 1
        
        while left < right:
            mid = left + (right - left) // 2
            
            if arr[mid] < arr[mid + 1]:
                left = mid + 1
            else:
                right = mid
        
        return left
    
    def search_rotated_array(self, arr, target):
        """
        Search in rotated sorted array.
        Time: O(log n), Space: O(1)
        """
        left, right = 0, len(arr) - 1
        
        while left <= right:
            mid = left + (right - left) // 2
            
            if arr[mid] == target:
                return mid
            
            # Determine which half is sorted
            if arr[left] <= arr[mid]:  # Left half is sorted
                if arr[left] <= target < arr[mid]:
                    right = mid - 1
                else:
                    left = mid + 1
            else:  # Right half is sorted
                if arr[mid] < target <= arr[right]:
                    left = mid + 1
                else:
                    right = mid - 1
        
        return -1
    
    def find_minimum_rotated(self, arr):
        """
        Find minimum element in rotated sorted array.
        Time: O(log n), Space: O(1)
        """
        left, right = 0, len(arr) - 1
        
        while left < right:
            mid = left + (right - left) // 2
            
            if arr[mid] > arr[right]:
                left = mid + 1
            else:
                right = mid
        
        return left
    
    def search_2d_matrix(self, matrix, target):
        """
        Search in 2D matrix (sorted rows and columns).
        Time: O(log(m*n)), Space: O(1)
        """
        if not matrix or not matrix[0]:
            return False
        
        m, n = len(matrix), len(matrix[0])
        left, right = 0, m * n - 1
        
        while left <= right:
            mid = left + (right - left) // 2
            mid_value = matrix[mid // n][mid % n]
            
            if mid_value == target:
                return True
            elif mid_value < target:
                left = mid + 1
            else:
                right = mid - 1
        
        return False
    
    def sqrt_binary_search(self, x):
        """
        Find square root using binary search.
        Time: O(log x), Space: O(1)
        """
        if x < 2:
            return x
        
        left, right = 2, x // 2
        
        while left <= right:
            mid = left + (right - left) // 2
            num = mid * mid
            
            if num == x:
                return mid
            elif num < x:
                left = mid + 1
            else:
                right = mid - 1
        
        return right

# Usage Examples

searcher = SearchingAlgorithms()

# Linear search

arr1 = [64, 34, 25, 12, 22, 11, 90]
print("Linear Search:", searcher.linear_search(arr1, 22)) # Output: 4

# Binary search

arr2 = [1, 3, 5, 7, 9, 11, 13, 15]
print("Binary Search:", searcher.binary_search(arr2, 7)) # Output: 3

# Find first and last occurrence

arr3 = [1, 2, 2, 2, 3, 4, 5]
print("First occurrence:", searcher.find_first_occurrence(arr3, 2)) # Output: 1
print("Last occurrence:", searcher.find_last_occurrence(arr3, 2)) # Output: 3
print("Count:", searcher.count_occurrences(arr3, 2)) # Output: 3

# Search in rotated array

arr4 = [4, 5, 6, 7, 0, 1, 2]
print("Rotated search:", searcher.search_rotated_array(arr4, 0)) # Output: 4

# Find peak element

arr5 = [1, 2, 3, 1]
print("Peak element:", searcher.find_peak_element(arr5)) # Output: 2


Time and Space Complexity

Algorithm Comparison

AlgorithmTime Complexity (Best)Time Complexity (Average)Time Complexity (Worst)Space ComplexityData Requirement
Linear SearchO(1)O(n)O(n)O(1)None
Binary SearchO(1)O(log n)O(log n)O(1) iterative, O(log n) recursiveSorted
Jump SearchO(1)O(√n)O(√n)O(1)Sorted
Interpolation SearchO(log log n)O(log log n)O(n)O(1)Sorted, uniformly distributed
Exponential SearchO(1)O(log n)O(log n)O(1)Sorted
Ternary SearchO(1)O(log₃ n)O(log₃ n)O(1)Unimodal function

Binary Search Variations Complexity

VariationTime ComplexitySpace Complexity
Find Exact MatchO(log n)O(1)
First/Last OccurrenceO(log n)O(1)
Count OccurrencesO(log n)O(1)
Search Insert PositionO(log n)O(1)
Peak ElementO(log n)O(1)
Rotated Array SearchO(log n)O(1)
2D Matrix SearchO(log(m×n))O(1)
Binary Search on AnswerO(log(range) × f(x))O(1)

Applications

00

Database Indexing

Databases use binary search on indexed columns to quickly locate records. B-trees and B+ trees, which are variants of binary search trees, enable efficient searching, insertion, and deletion in database systems.

01

Dictionary and Spell Checkers

Word processors and search engines use binary search to quickly look up words in dictionaries. This enables real-time spell checking and autocomplete suggestions as you type.

02

Version Control Systems

Git uses binary search (git bisect) to find the commit that introduced a bug. It efficiently narrows down the problematic commit by testing middle commits and eliminating half the search space each time.

03

Computer Graphics

Ray tracing and collision detection use binary search on spatial data structures like KD-trees and octrees to quickly determine which objects a ray intersects, enabling real-time rendering.

04

Network Routing

IP routing tables use binary search (or tries) to find the longest matching prefix for packet forwarding. This enables routers to quickly determine the next hop for millions of packets per second.

05

Machine Learning

Decision trees use binary search-like splitting to classify data. Hyperparameter tuning often uses binary search to find optimal values efficiently across large parameter spaces.

06

Financial Systems

Stock trading platforms use binary search to match buy and sell orders in order books. Options pricing models use binary search to find implied volatility from market prices.

07

Operating Systems

Memory management systems use binary search to allocate and deallocate memory blocks efficiently. File systems use binary search on directory structures to locate files quickly.


Must-Do LeetCode Problems

Beginner Level - Binary Search Basics

#ProblemDifficultyImportanceKey ConceptsLink
1Binary SearchEasy⭐⭐⭐⭐⭐Basic binary search implementation#704
2Search Insert PositionEasy⭐⭐⭐⭐⭐Lower bound, insert position#35
3First Bad VersionEasy⭐⭐⭐⭐API usage, binary search#278
4Sqrt(x)Easy⭐⭐⭐⭐Binary search for square root#69
5Peak Index in a Mountain ArrayEasy⭐⭐⭐⭐Finding peak element#852
6Find Minimum in Rotated Sorted ArrayMedium⭐⭐⭐⭐⭐Rotated array search#153
7Search in Rotated Sorted ArrayMedium⭐⭐⭐⭐⭐Rotated#33

Intermediate Level - Variations and Applications

#ProblemDifficultyImportanceKey ConceptsLink
8Find First and Last Position of Element in Sorted ArrayMedium⭐⭐⭐⭐⭐First/last occurrence, counting#34
9Count of Smaller Numbers After SelfHard⭐⭐⭐⭐Binary indexed#315

Advanced Level - Complex Searching Techniques

#ProblemDifficultyImportanceKey ConceptsLink
10Median of Two Sorted ArraysHard⭐⭐⭐⭐⭐Binary search on answer#4
11Kth Smallest Element in a Sorted MatrixMedium⭐⭐⭐⭐Binary search on answer, 2D matrix#378
12Find K Closest ElementsMedium⭐⭐⭐⭐Binary search on answer, two pointers#658