Searching Algorithms

Searching is a fundamental operation in software engineering, enabling programs to locate data efficiently within various data structures. This tutorial provides an in‑depth look at the most widely used searching algorithms, their theoretical underpinnings, practical implementations, and performance considerations.

Why Searching Algorithms Matter

Choosing the right searching algorithm can dramatically affect the runtime of an application, especially when dealing with large data sets or real‑time constraints. A well‑chosen algorithm reduces computational overhead, saves memory, and improves user experience.

📝 Note: All algorithms discussed assume that the input data is stored in an array or list unless otherwise specified.

Classification of Searching Algorithms

  • Sequential (Linear) Search
  • Binary Search (and its variants)
  • Interpolation Search
  • Jump Search
  • Exponential Search
  • Fibonacci Search
  • Ternary Search

1. Linear Search

Linear search examines each element sequentially until the target value is found or the list ends. It works on unsorted data and has a time complexity of O(n).

def linear_search(arr, target):
    for i, value in enumerate(arr):
        if value == target:
            return i
    return -1
public static int linearSearch(int[] arr, int target) {
    for (int i = 0; i < arr.length; i++) {
        if (arr[i] == target) {
            return i;
        }
    }
    return -1;
}
int linearSearch(const std::vector<int>& arr, int target) {
    for (size_t i = 0; i < arr.size(); ++i) {
        if (arr[i] == target) return static_cast<int>(i);
    }
    return -1;
}
💡 Tip: Use linear search only for small or unsorted collections; otherwise consider more efficient alternatives.

2. Binary Search

Binary search works on sorted arrays by repeatedly dividing the search interval in half. Its average and worst‑case time complexity is O(log n).

⚠ Warning: The input array must be sorted in non‑decreasing order before applying binary search.
def binary_search(arr, target):
    left, right = 0, len(arr) - 1
    while left <= right:
        mid = (left + right) // 2
        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    return -1
public static int binarySearch(int[] arr, int target) {
    int left = 0, right = arr.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (arr[mid] == target) return mid;
        if (arr[mid] < target) left = mid + 1;
        else right = mid - 1;
    }
    return -1;
}
int binarySearch(const std::vector<int>& arr, int target) {
    int left = 0, right = static_cast<int>(arr.size()) - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (arr[mid] == target) return mid;
        if (arr[mid] < target) left = mid + 1;
        else right = mid - 1;
    }
    return -1;
}
📝 Note: Iterative implementation avoids the stack overhead of recursion and is preferred in production code.

3. Interpolation Search

Interpolation search improves upon binary search for uniformly distributed sorted data by estimating the position of the target value. Its average time complexity is O(log log n), but it can degrade to O(n) for non‑uniform distributions.

def interpolation_search(arr, target):
    low = 0
    high = len(arr) - 1
    while low <= high and target >= arr[low] and target <= arr[high]:
        if low == high:
            return low if arr[low] == target else -1
        # Estimate the position
        pos = low + int(((float(high - low) / (arr[high] - arr[low])) * (target - arr[low])))
        if arr[pos] == target:
            return pos
        if arr[pos] < target:
            low = pos + 1
        else:
            high = pos - 1
    return -1
💡 Tip: Use interpolation search only when you can guarantee a roughly uniform distribution of values.

4. Jump Search

Jump search also requires a sorted array. It jumps ahead by a fixed block size (typically √n) and performs a linear search within the identified block. The time complexity is O(√n).

import math

def jump_search(arr, target):
    n = len(arr)
    step = int(math.sqrt(n))
    prev = 0
    while arr[min(step, n) - 1] < target:
        prev = step
        step += int(math.sqrt(n))
        if prev >= n:
            return -1
    for i in range(prev, min(step, n)):
        if arr[i] == target:
            return i
    return -1
Jump search strikes a balance between linear and binary search, making it useful when random access is costly.

5. Exponential Search

Exponential search first finds a range where the target could reside by repeatedly doubling the index, then applies binary search within that range. It runs in O(log n) time.

def exponential_search(arr, target):
    if arr[0] == target:
        return 0
    i = 1
    while i < len(arr) and arr[i] <= target:
        i *= 2
    # Call binary search on the found range
    left = i // 2
    right = min(i, len(arr) - 1)
    return binary_search(arr[left:right+1], target)

# Re‑use binary_search from earlier

6. Fibonacci Search

Fibonacci search uses Fibonacci numbers to divide the array, offering a similar performance to binary search but with only addition and subtraction operations, which can be advantageous on systems where division is expensive.

def fibonacci_search(arr, target):
    n = len(arr)
    # Initialize fibonacci numbers
    fibMMm2 = 0  # (m-2)'th Fibonacci No.
    fibMMm1 = 1  # (m-1)'th Fibonacci No.
    fibM = fibMMm2 + fibMMm1  # m'th Fibonacci
    while fibM < n:
        fibMMm2 = fibMMm1
        fibMMm1 = fibM
        fibM = fibMMm2 + fibMMm1
    offset = -1
    while fibM > 1:
        i = min(offset + fibMMm2, n - 1)
        if arr[i] < target:
            fibM = fibMMm1
            fibMMm1 = fibMMm2
            fibMMm2 = fibM - fibMMm1
            offset = i
        elif arr[i] > target:
            fibM = fibMMm2
            fibMMm1 = fibMMm1 - fibMMm2
            fibMMm2 = fibM - fibMMm1
        else:
            return i
    if fibMMm1 and offset + 1 < n and arr[offset + 1] == target:
        return offset + 1
    return -1

7. Ternary Search

Ternary search divides the sorted array into three parts and discards two‑thirds of the range each iteration. It is most useful for unimodal functions, but for discrete arrays its time complexity is still O(log₃ n).

def ternary_search(arr, target, left=0, right=None):
    if right is None:
        right = len(arr) - 1
    while left <= right:
        third = (right - left) // 3
        mid1 = left + third
        mid2 = right - third
        if arr[mid1] == target:
            return mid1
        if arr[mid2] == target:
            return mid2
        if target < arr[mid1]:
            right = mid1 - 1
        elif target > arr[mid2]:
            left = mid2 + 1
        else:
            left = mid1 + 1
            right = mid2 - 1
    return -1

Performance Comparison

AlgorithmBest CaseAverage CaseWorst CaseSpace ComplexityRequires Sorted Data
Linear SearchO(1)O(n)O(n)O(1)No
Binary SearchO(1)O(log n)O(log n)O(1)Yes
Interpolation SearchO(log log n)O(log log n)O(n)O(1)Yes (uniform)
Jump SearchO(1)O(√n)O(√n)O(1)Yes
Exponential SearchO(1)O(log n)O(log n)O(1)Yes
Fibonacci SearchO(1)O(log n)O(log n)O(1)Yes
Ternary SearchO(1)O(log₃ n)O(log₃ n)O(1)Yes

Choosing the Right Algorithm

The decision matrix for selecting a searching algorithm can be summarised as follows:

  1. Is the data sorted? → Choose any logarithmic algorithm (binary, exponential, Fibonacci, etc.).
  2. Are the values uniformly distributed? → Consider interpolation search.
  3. Is random access cheap? → Binary or exponential search are optimal.
  4. Do you need to minimise division operations? → Fibonacci search may be advantageous.
  5. Is the dataset extremely large and you prefer block‑wise scanning? → Jump search.

Further reading on algorithm analysis

📘 Summary: Searching algorithms are essential tools in a software engineer’s toolbox. Understanding their time/space complexities, data requirements, and implementation nuances enables developers to write faster, more scalable code.

Q: Can binary search be applied to linked lists?
A: Yes, but it requires O(log n) random access, which linked lists do not provide efficiently. The overall complexity degrades to O(n).


Q: When is interpolation search faster than binary search?
A: When the dataset is sorted and the values are uniformly distributed, interpolation search can achieve O(log log n) time, outperforming binary search.


Q: Is linear search ever preferable?
A: For very small datasets or when the data is unsorted and cannot be pre‑processed, linear search is the simplest and most practical solution.


Q. Which searching algorithm has the best average‑case time complexity on a uniformly distributed sorted array?
  • Linear Search
  • Binary Search
  • Interpolation Search
  • Jump Search

Answer: Interpolation Search
Interpolation search exploits the uniform distribution to estimate the target's position, achieving O(log log n) average time.

Q. What is the space complexity of Fibonacci search?
  • O(1)
  • O(log n)
  • O(n)
  • O(n log n)

Answer: O(1)
Fibonacci search only uses a few integer variables for index calculations, resulting in constant extra space.

References