Algorithms

Searching Algorithms

Linear and binary search with implementation and analysis.

Linear Search

Sequentially checks each element until the target is found.

linear_search.c
#include <stdio.h>

int linearSearch(int arr[], int n, int key) {
    for (int i = 0; i < n; i++) {
        if (arr[i] == key)
            return i;
    }
    return -1;
}

int main() {
    int arr[] = {10, 20, 30, 40, 50};
    int n = sizeof(arr) / sizeof(arr[0]);
    int key = 30;
    
    int result = linearSearch(arr, n, key);
    if (result != -1)
        printf("Found at index %d\n", result);
    else
        printf("Not found\n");
    
    return 0;
}
CaseTime Complexity
BestO(1)
AverageO(n)
WorstO(n)
SpaceO(1)

Binary Search

Works on sorted arrays by repeatedly dividing the search interval in half.

binary_search.c
#include <stdio.h>

// Iterative
int binarySearch(int arr[], int n, int key) {
    int left = 0, right = n - 1;
    
    while (left <= right) {
        int mid = left + (right - left) / 2;
        
        if (arr[mid] == key)
            return mid;
        else if (arr[mid] < key)
            left = mid + 1;
        else
            right = mid - 1;
    }
    return -1;
}

// Recursive
int binarySearchRecursive(int arr[], int left, int right, int key) {
    if (left > right) return -1;
    
    int mid = left + (right - left) / 2;
    
    if (arr[mid] == key)
        return mid;
    else if (arr[mid] < key)
        return binarySearchRecursive(arr, mid + 1, right, key);
    else
        return binarySearchRecursive(arr, left, mid - 1, key);
}
CaseTime Complexity
BestO(1)
AverageO(log n)
WorstO(log n)
SpaceO(1) / O(log n)

Hashing / Hash Search

Real-world analogy: Imagine a library with numbered shelves where each book has a unique code that tells you exactly which shelf it's on. Instead of searching through every shelf, you calculate the shelf number from the code and go directly there!

Hashing maps keys to array indices using a hash function. This enables O(1) average case lookup, insertion, and deletion.

How Hashing Works

Keys:    ["Alice", "Bob", "Charlie", "David", "Eve"]
Values:  [   25   ,   30  ,    22    ,   28   ,  35  ]

Hash Function: h(key) = (sum of ASCII values) % table_size
h("Alice")   = (65+108+105+99+101) % 10 = 478 % 10 = 8
h("Bob")     = (66+111+98) % 10 = 275 % 10 = 5
h("Charlie") = (67+104+97+114+108+105+101) % 10 = 797 % 10 = 7

Hash Table (size = 10):
Index:  [0]    [1]    [2]    [3]    [4]    [5]    [6]    [7]      [8]      [9]
        []     []     []     []     []   [Bob]    []  [Charlie] [Alice]   []
                    
Search "Alice": Calculate h("Alice") = 8 → Check index 8 → Found! ✓

Collision Resolution

When two keys hash to the same index, we have a collision. Two main techniques resolve this:

1. Separate Chaining (Linked List)

Hash Table with Chaining:

Index:  [0]      [1]      [2]      [3]      [4]
        []       []       []       []       []
        ↓        ↓        ↓        ↓        ↓
        NULL     NULL     NULL     NULL     NULL

After inserting: h("Apple")=2, h("Banana")=2, h("Cherry")=4

Index:  [0]      [1]      [2]                [3]      [4]
        []       []    ["Banana"]             []    ["Cherry"]
        ↓        ↓        ↓                   ↓        ↓
        NULL     NULL  ["Apple"]             NULL    NULL
                         ↓
                        NULL

Both "Apple" and "Banana" at index 2 → Linked list

2. Open Addressing (Linear Probing)

Linear Probing: If slot is full, try next slot

Initial:    [ ]    [ ]    [ ]    [ ]    [ ]    [ ]
            0      1      2      3      4      5

Insert 5: h(5)=5%6=5 → [ ] [ ] [ ] [ ] [ ] [5]
                       0   1   2   3   4   5

Insert 11: h(11)=11%6=5 (collision!) → try index 0
           [11]   [ ]    [ ]    [ ]    [ ]    [5]
            0      1      2      3      4      5

Insert 17: h(17)=17%6=5 (collision!) → index 0 full → index 1
           [11]   [17]   [ ]    [ ]    [ ]    [5]
            0      1      2      3      4      5

Insert 23: h(23)=23%6=5 → index 5 → index 0 → index 1 → index 2
           [11]   [17]   [23]   [ ]    [ ]    [5]
            0      1      2      3      4      5
hash_table.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define TABLE_SIZE 10

typedef struct Node {
    int key;
    int value;
    struct Node* next;
} Node;

typedef struct HashTable {
    Node* buckets[TABLE_SIZE];
} HashTable;

// Simple hash function
int hash(int key) {
    return key % TABLE_SIZE;
}

// Create new node
Node* createNode(int key, int value) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->key = key;
    newNode->value = value;
    newNode->next = NULL;
    return newNode;
}

// Initialize hash table
void initHashTable(HashTable* ht) {
    for (int i = 0; i < TABLE_SIZE; i++)
        ht->buckets[i] = NULL;
}

// Insert using separate chaining
void insert(HashTable* ht, int key, int value) {
    int index = hash(key);
    Node* newNode = createNode(key, value);
    
    // Insert at head of list
    newNode->next = ht->buckets[index];
    ht->buckets[index] = newNode;
}

// Search
int search(HashTable* ht, int key) {
    int index = hash(key);
    Node* current = ht->buckets[index];
    
    while (current != NULL) {
        if (current->key == key)
            return current->value;
        current = current->next;
    }
    return -1; // Not found
}

// Delete
void delete(HashTable* ht, int key) {
    int index = hash(key);
    Node* current = ht->buckets[index];
    Node* prev = NULL;
    
    while (current != NULL) {
        if (current->key == key) {
            if (prev == NULL)
                ht->buckets[index] = current->next;
            else
                prev->next = current->next;
            free(current);
            return;
        }
        prev = current;
        current = current->next;
    }
}

// Display hash table
void display(HashTable* ht) {
    for (int i = 0; i < TABLE_SIZE; i++) {
        printf("Bucket %d: ", i);
        Node* current = ht->buckets[i];
        while (current != NULL) {
            printf("(%d,%d) → ", current->key, current->value);
            current = current->next;
        }
        printf("NULL\n");
    }
}

int main() {
    HashTable ht;
    initHashTable(&ht);
    
    insert(&ht, 5, 100);
    insert(&ht, 15, 200);  // Collision with 5 (both hash to 5)
    insert(&ht, 25, 300);  // Another collision
    insert(&ht, 7, 400);
    
    printf("Hash Table:\n");
    display(&ht);
    
    printf("\nSearch 15: %d\n", search(&ht, 15));
    printf("Search 100: %d\n", search(&ht, 100));  // Not found
    
    return 0;
}
CaseTime ComplexitySpace
Best/AverageO(1)O(n)
Worst (many collisions)O(n)O(n)

Interpolation Search

Real-world analogy: Looking up a word in a dictionary. If you need "zebra", you don't start in the middle — you flip near the end! Interpolation Search estimates position based on value distribution.

Like Binary Search, but instead of always checking the middle, it calculates a probable position using the formula:

pos = low + ( (key - arr[low]) × (high - low) ) / (arr[high] - arr[low])

Example: arr = [10, 20, 30, 40, 50, 60, 70, 80, 90], key = 85
low=0, high=8

pos = 0 + ( (85 - 10) × (8 - 0) ) / (90 - 10)
    = 0 + (75 × 8) / 80
    = 0 + 600 / 80
    = 7.5 ≈ 7

Check index 7: arr[7] = 80 (85 > 80, so search right side)

Step-by-Step Visual: Searching for 65 in [10, 20, 30, 40, 50, 60, 70, 80, 90]

StepLowHighCalculated Posarr[pos]Action
10867065 < 70, go left
20556065 > 60, go right
365--Not found
interpolation_search.c
#include <stdio.h>

int interpolationSearch(int arr[], int n, int key) {
    int low = 0, high = n - 1;
    
    // Key must be in range [low, high] and array must be uniformly distributed
    while (low <= high && key >= arr[low] && key <= arr[high]) {
        
        // If array has only one element
        if (low == high) {
            if (arr[low] == key) return low;
            return -1;
        }
        
        // Probing the position
        // Formula: low + ((key - arr[low]) * (high - low)) / (arr[high] - arr[low])
        int pos = low + ((key - arr[low]) * (high - low)) / (arr[high] - arr[low]);
        
        // Check if key is at pos
        if (arr[pos] == key)
            return pos;
        
        // If key is larger, search right subarray
        if (arr[pos] < key)
            low = pos + 1;
        // If key is smaller, search left subarray
        else
            high = pos - 1;
    }
    
    return -1; // Not found
}

int main() {
    int arr[] = {10, 20, 30, 40, 50, 60, 70, 80, 90};
    int n = sizeof(arr) / sizeof(arr[0]);
    int key = 65;
    
    int result = interpolationSearch(arr, n, key);
    
    if (result != -1)
        printf("Element %d found at index %d\n", key, result);
    else
        printf("Element %d not found\n", key);
    
    return 0;
}
CaseTime Complexity
Best (uniformly distributed)O(log log n)
AverageO(log log n)
Worst (non-uniform)O(n)
SpaceO(1)

Jump Search

Real-world analogy: Looking for a friend's house on a long street. Instead of checking every house (Linear Search) or calculating exact position (Interpolation), you jump ahead by a fixed number of houses, then check if you've gone too far. If yes, you go back and check one by one in that block.

Jump Search divides the array into blocks of size √n and checks the last element of each block until finding the right one.

Step-by-Step Visual: Searching for 55 in [0, 1, 2, ..., 99]

n = 100, jump = √100 = 10

Array:  [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ..., 50, 51, 52, 53, 54, 55, 56, ..., 99]
Index:   0  1  2  3  4  5  6  7  8  9  10       50  51  52  53  54  55  56       99

Block size = 10

Step 1: Jump to index 9:  arr[9] = 9 < 55 → keep jumping
Step 2: Jump to index 19: arr[19] = 19 < 55 → keep jumping
Step 3: Jump to index 29: arr[29] = 29 < 55 → keep jumping
Step 4: Jump to index 39: arr[39] = 39 < 55 → keep jumping
Step 5: Jump to index 49: arr[49] = 49 < 55 → keep jumping
Step 6: Jump to index 59: arr[59] = 59 > 55 → Too far! Go back to index 50

Linear search from index 50 to 59:
        50, 51, 52, 53, 54, 55 ← Found!
jump_search.c
#include <stdio.h>
#include <math.h>

int jumpSearch(int arr[], int n, int key) {
    // Find optimal block size to jump
    int step = sqrt(n);
    int prev = 0;
    
    // Jump until we find a block where key could be
    while (arr[min(step, n) - 1] < key) {
        prev = step;
        step += sqrt(n);
        
        // If we've gone past the array, key not present
        if (prev >= n)
            return -1;
    }
    
    // Linear search within the identified block
    while (arr[prev] < key) {
        prev++;
        
        // If we reach next block, key not present
        if (prev == min(step, n))
            return -1;
    }
    
    // Check if element found
    if (arr[prev] == key)
        return prev;
    
    return -1;
}

// Helper function
int min(int a, int b) {
    return (a < b) ? a : b;
}

int main() {
    int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
    int n = sizeof(arr) / sizeof(arr[0]);
    int key = 13;
    
    int result = jumpSearch(arr, n, key);
    
    if (result != -1)
        printf("Element %d found at index %d\n", key, result);
    else
        printf("Element not found\n");
    
    return 0;
}
CaseTime Complexity
BestO(1)
Worst/AverageO(√n)
SpaceO(1)

Index Sequential Search

Real-world analogy: A library card catalog. Instead of searching every book, you first check the index (cards organized by first letter), which points you to the right section. Then you search within that smaller section.

Creates an auxiliary index table that maps key ranges to positions in the main array. First search the index, then search the relevant block.

How It Works

Main Array: [3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60]
Index Table (every 5 elements):

Index Entry:    [0]       [1]       [2]       [3]
                ↓         ↓         ↓         ↓
Key:          [15]      [30]      [45]      [60]
Points to:     4         9         14        19

Search for 33:
Step 1: Check index. 30 < 33 < 45, so look in block starting at index 9
Step 2: Linear search from index 9: arr[9]=30, arr[10]=33 ✓ Found!

Without index: 11 comparisons
With index: 2 (index) + 2 (linear) = 4 comparisons
index_sequential.c
#include <stdio.h>

#define INDEX_SIZE 5  // Number of index entries
#define BLOCK_SIZE 10 // Elements per block

typedef struct {
    int key;      // Maximum key in this block
    int position; // Starting position in main array
} IndexEntry;

// Create index table
void createIndex(int arr[], int n, IndexEntry index[]) {
    for (int i = 0; i < INDEX_SIZE && i * BLOCK_SIZE < n; i++) {
        index[i].position = i * BLOCK_SIZE;
        // Store max key in block (last element)
        int end = (i + 1) * BLOCK_SIZE - 1;
        if (end >= n) end = n - 1;
        index[i].key = arr[end];
    }
}

int indexSequentialSearch(int arr[], int n, IndexEntry index[], int key) {
    // Search index table to find correct block
    int block = -1;
    for (int i = 0; i < INDEX_SIZE; i++) {
        if (key <= index[i].key) {
            block = i;
            break;
        }
    }
    
    // If block not found in index
    if (block == -1)
        return -1;
    
    // Calculate search range in main array
    int start = index[block].position;
    int end = start + BLOCK_SIZE;
    if (end > n) end = n;
    
    // Sequential search within block
    for (int i = start; i < end; i++) {
        if (arr[i] == key)
            return i;
        if (arr[i] > key)
            return -1; // Early exit (array is sorted)
    }
    
    return -1;
}

int main() {
    int arr[] = {3, 6, 9, 12, 15, 18, 21, 24, 27, 30,  
                 33, 36, 39, 42, 45, 48, 51, 54, 57, 60};
    int n = sizeof(arr) / sizeof(arr[0]);
    
    IndexEntry index[INDEX_SIZE];
    createIndex(arr, n, index);
    
    printf("Index Table:\n");
    for (int i = 0; i < INDEX_SIZE; i++) {
        printf("Entry %d: key=%d, position=%d\n", 
               i, index[i].key, index[i].position);
    }
    
    int key = 33;
    int result = indexSequentialSearch(arr, n, index, key);
    
    if (result != -1)
        printf("\nElement %d found at index %d\n", key, result);
    else
        printf("\nElement not found\n");
    
    return 0;
}
CaseTime Complexity
Index SearchO(m) where m = index size
Block SearchO(n/m) where m = number of blocks
TotalO(m + n/m) ≈ O(√n) when m = √n
SpaceO(m) for index table

Searching Algorithm Comparison

AlgorithmData RequirementBestAverageWorstSpaceUse When
Linear SearchAnyO(1)O(n)O(n)O(1)Small/unsorted data
Binary SearchSortedO(1)O(log n)O(log n)O(1)Large sorted arrays
Hash SearchHash tableO(1)O(1)O(n)O(n)Frequent lookups
InterpolationSorted + uniformO(1)O(log log n)O(n)O(1)Uniformly distributed
Jump SearchSortedO(1)O(√n)O(√n)O(1)Large sorted, jumping better than binary
Index SequentialSortedO(1)O(√n)O(√n)O(√n)Large files with index

Choosing the Right Search

  • Data is unsorted: Linear Search or Hash Table
  • Data is sorted: Binary Search (most common), Interpolation (uniform data), Jump Search (simple code)
  • Many lookups, static data: Hash Table (preprocessing cost pays off)
  • Large files on disk: Index Sequential Search (minimize disk reads)
  • Memory constrained: Binary or Jump Search (no extra space)