Views

Report Content

×

Searching Algorithms: Complete Guide

Master search algorithms from linear to exponential, with complexity analysis, optimization techniques, and real-world applications. Includes implementations for both sorted and unsorted data.

Search Algorithm Usage (2023)

Binary Search (40%)
Hash Tables (25%)
Tree Searches (20%)
Specialized (15%)

1. Fundamental Search Algorithms

Linear Search

Definition

Linear Search is a sequential searching algorithm that checks every element in a list one by one until the target value is found or the list ends. It works on both sorted and unsorted data.

How It Works

1
Start at the beginning
  • Initialize index i = 0
  • Set target value to search for
2
Compare current element
  • Check if arr[i] equals target
  • If yes, return index i
3
Move to next element
  • Increment i by 1
  • Repeat step 2 until target found or end reached
def linear_search(arr, target):
    for i in range(len(arr)):
        if arr[i] == target:
            return i
    return -1

Complexity: O(n) time, O(1) space
Use When: Unsorted or small datasets

Binary Search

Definition

Binary Search is an efficient algorithm that finds a target value within a sorted array by repeatedly dividing the search interval in half. It compares the target to the middle element and eliminates half of the remaining elements each time.

How It Works

1
Set boundaries
  • Initialize left = 0, right = n-1
  • Array must be sorted
2
Find middle element
  • Calculate mid = (left + right) / 2
  • Compare arr[mid] with target
3
Narrow the search
  • If target equals arr[mid] → return mid
  • If target < arr[mid] → search left half (right = mid - 1)
  • If target > arr[mid] → search right half (left = mid + 1)
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

Complexity: O(log n) time, O(1) space
Use When: Sorted arrays

Time Complexities

Algorithm Best Average Worst Space
Linear O(1) O(n) O(n) O(1)
Binary O(1) O(log n) O(log n) O(1)
Jump O(1) O(√n) O(√n) O(1)
Exponential O(1) O(log i) O(log n) O(1)

2. Advanced Search Techniques

Jump Search

Definition

Jump Search (or Block Search) is a search algorithm for sorted arrays that works by jumping ahead by fixed steps (optimal block size = √n) and then performing a linear search within the identified block.

How It Works

1
Calculate block size
  • Set blockSize = √(array length)
  • Initialize prev = 0
2
Jump ahead in blocks
  • While arr[min(step, n)-1] < target
  • Set prev = step, step += blockSize
  • If prev >= n, target not found
3
Linear search in block
  • Search from prev to step-1
  • If found, return index
  • If not, return -1
import math

def jump_search(arr, target):
    n = len(arr)
    block_size = int(math.sqrt(n))
    prev = 0
    
    while arr[min(block_size, n) - 1] < target:
        prev = block_size
        block_size += int(math.sqrt(n))
        if prev >= n:
            return -1
    
    for i in range(prev, min(block_size, n)):
        if arr[i] == target:
            return i
    return -1

Optimal Block: √n
Use When: Sorted arrays, better than linear

Exponential Search

Definition

Exponential Search (also called galloping search) is an algorithm that finds the range where the target exists by exponentially increasing the search interval, then performs binary search within that range. It's ideal for unbounded or infinite arrays.

How It Works

1
Check first element
  • If arr[0] == target, return 0
  • Initialize i = 1
2
Exponentially increase range
  • While i < n and arr[i] <= target
  • Double i each time (i *= 2)
  • This finds a range where target could exist
3
Binary search the range
  • Perform binary search from i/2 to min(i, n-1)
  • Return index if found, else -1
def exponential_search(arr, target):
    if arr[0] == target:
        return 0
    
    n = len(arr)
    i = 1
    while i < n and arr[i] <= target:
        i *= 2
    
    left, right = i // 2, min(i, n - 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

Combines: Exponential + Binary
Use When: Infinite/unbounded arrays

Interpolation Search

Definition

Interpolation Search is an improved variant of binary search that works best on uniformly distributed sorted data. Instead of always checking the middle, it calculates a probable position using a formula based on the target value.

How It Works

1
Calculate probe position
  • pos = low + ((target - arr[low]) * (high - low)) / (arr[high] - arr[low])
  • This estimates where the target might be
2
Compare and narrow
  • If arr[pos] == target → return pos
  • If arr[pos] < target → search right half
  • If arr[pos] > target → search left half
3
Repeat until found
  • Continue recalculating pos
  • Stop when low > high or target out of range
def interpolation_search(arr, target):
    low, high = 0, len(arr) - 1
    
    while low <= high and target >= arr[low] and target <= arr[high]:
        pos = low + int(((target - arr[low]) * (high - low)) / (arr[high] - arr[low]))
        
        if arr[pos] == target:
            return pos
        if arr[pos] < target:
            low = pos + 1
        else:
            high = pos - 1
    return -1

Complexity: O(log log n) avg
Use When: Uniformly distributed data

Ternary Search

Definition

Ternary Search is a divide-and-conquer algorithm that splits the array into three parts instead of two. It's primarily used for finding the maximum or minimum of a unimodal function rather than searching for a specific value.

How It Works

1
Calculate mid points
  • mid1 = left + (right - left) / 3
  • mid2 = right - (right - left) / 3
2
Compare with target
  • If target equals arr[mid1] or arr[mid2] → return index
  • If target < arr[mid1] → search left third
  • If target > arr[mid2] → search right third
  • Otherwise → search middle third
3
Repeat recursively
  • Update boundaries based on comparison
  • Continue until found or boundaries cross
def ternary_search(arr, target):
    left, right = 0, len(arr) - 1
    
    while left <= right:
        mid1 = left + (right - left) // 3
        mid2 = right - (right - left) // 3
        
        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

Comparisons: More than binary
Use When: Finding min/max in unimodal functions

3. Search in Data Structures

Hash Tables

Definition

Hash Tables are data structures that map keys to values using a hash function. They provide O(1) average-time search by computing a hash code that directly points to the location of the data.

How It Works

1
Apply hash function
  • Convert key to hash code using hash function
  • Map hash code to array index using modulo
2
Handle collisions
  • Use chaining (linked lists at each index)
  • Or open addressing (linear probing)
3
Search for key
  • Go to computed index
  • Search through chain/probe sequence
  • Return value if key matches
class HashTable:
    def __init__(self, size=53):
        self.key_map = [[] for _ in range(size)]
    
    def _hash(self, key):
        total = 0
        PRIME = 31
        for i, char in enumerate(str(key)[:100]):
            total = (total * PRIME + ord(char)) % len(self.key_map)
        return total
    
    def search(self, key):
        index = self._hash(key)
        for k, v in self.key_map[index]:
            if k == key:
                return v
        return None

Complexity: O(1) average
Use When: Fast lookups needed

Binary Search Trees

Definition

Binary Search Trees (BSTs) are hierarchical data structures where each node has at most two children, and for every node, all left descendants are smaller and all right descendants are larger.

How It Works

1
Start at root
  • Set current node = root
  • Compare target with current node value
2
Traverse left or right
  • If target equals current value → found
  • If target < current value → go left
  • If target > current value → go right
3
Continue until found or null
  • Repeat step 2 until target found
  • If current becomes null, target not in tree
class Node:
    def __init__(self, value):
        self.value = value
        self.left = None
        self.right = None

class BST:
    def __init__(self):
        self.root = None
    
    def search(self, value, node=None):
        if node is None:
            node = self.root
        if not node:
            return False
        if value == node.value:
            return True
        if value < node.value:
            return self.search(value, node.left)
        return self.search(value, node.right)

Complexity: O(h) time, O(1) space
Balanced: O(log n) height

Search Algorithm Cheat Sheet

Scenario Best Algorithm Time Complexity
Unsorted small array Linear Search O(n)
Sorted array Binary Search O(log n)
Unbounded array Exponential Search O(log i)
Uniform distribution Interpolation Search O(log log n)
Key-value pairs Hash Table O(1)

4. Real-World Applications

Database Indexing

B-Trees: Balanced search trees for disk-based storage

class BTreeNode:
    def __init__(self, leaf=False):
        self.leaf = leaf
        self.keys = []
        self.values = []
        self.children = []

def btree_search(node, key):
    i = 0
    while i < len(node.keys) and key > node.keys[i]:
        i += 1
    
    if i < len(node.keys) and key == node.keys[i]:
        return node.values[i]
    
    if node.leaf:
        return None
    
    return btree_search(node.children[i], key)

Autocomplete Systems

Trie Search: Prefix-based string searching

class TrieNode:
    def __init__(self):
        self.children = {}
        self.is_end = False

class Trie:
    def __init__(self):
        self.root = TrieNode()
    
    def search(self, word):
        node = self.root
        for char in word:
            if char not in node.children:
                return False
            node = node.children[char]
        return node.is_end
    
    def starts_with(self, prefix):
        node = self.root
        for char in prefix:
            if char not in node.children:
                return []
            node = node.children[char]
        return self._get_words(node, prefix)

Geospatial Data

Quadtree/K-d Tree: Nearest neighbor searches

import math

class KDNode:
    def __init__(self, point, depth=0):
        self.point = point
        self.left = None
        self.right = None
        self.depth = depth

def distance(p1, p2):
    return math.sqrt(sum((a - b) ** 2 for a, b in zip(p1, p2)))

def nearest_neighbor(node, point, best=None):
    if not node:
        return best
    
    dist = distance(node.point, point)
    if not best or dist < distance(best, point):
        best = node.point
    
    axis = node.depth % 2
    next_branch = node.left if point[axis] < node.point[axis] else node.right
    other_branch = node.right if next_branch == node.left else node.left
    
    best = nearest_neighbor(next_branch, point, best)
    
    if abs(point[axis] - node.point[axis]) < distance(best, point):
        best = nearest_neighbor(other_branch, point, best)
    
    return best

Search Algorithm Selection Guide

Unsorted data → Linear or Hash Table
Sorted arrays → Binary Search
Unknown size → Exponential Search
Uniform data → Interpolation Search
Frequent lookups → Hash Table

Pro Tip: Modern systems combine multiple search techniques. For example, databases use B-trees (logarithmic search) with hash indexes for exact matches. Always consider your data characteristics before choosing an algorithm.

Share and Join the Discussion

You need to be logged in to participate in this discussion.

×
×