Searching Algorithms
Linear and binary search with implementation and analysis.
Linear Search
Sequentially checks each element until the target is found.
#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;
}
| Case | Time Complexity |
|---|---|
| Best | O(1) |
| Average | O(n) |
| Worst | O(n) |
| Space | O(1) |
Binary Search
Works on sorted arrays by repeatedly dividing the search interval in half.
#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);
}
| Case | Time Complexity |
|---|---|
| Best | O(1) |
| Average | O(log n) |
| Worst | O(log n) |
| Space | O(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
#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;
}
| Case | Time Complexity | Space |
|---|---|---|
| Best/Average | O(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]
| Step | Low | High | Calculated Pos | arr[pos] | Action |
|---|---|---|---|---|---|
| 1 | 0 | 8 | 6 | 70 | 65 < 70, go left |
| 2 | 0 | 5 | 5 | 60 | 65 > 60, go right |
| 3 | 6 | 5 | - | - | Not found |
#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;
}
| Case | Time Complexity |
|---|---|
| Best (uniformly distributed) | O(log log n) |
| Average | O(log log n) |
| Worst (non-uniform) | O(n) |
| Space | O(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!
#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;
}
| Case | Time Complexity |
|---|---|
| Best | O(1) |
| Worst/Average | O(√n) |
| Space | O(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
#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;
}
| Case | Time Complexity |
|---|---|
| Index Search | O(m) where m = index size |
| Block Search | O(n/m) where m = number of blocks |
| Total | O(m + n/m) ≈ O(√n) when m = √n |
| Space | O(m) for index table |
Searching Algorithm Comparison
| Algorithm | Data Requirement | Best | Average | Worst | Space | Use When |
|---|---|---|---|---|---|---|
| Linear Search | Any | O(1) | O(n) | O(n) | O(1) | Small/unsorted data |
| Binary Search | Sorted | O(1) | O(log n) | O(log n) | O(1) | Large sorted arrays |
| Hash Search | Hash table | O(1) | O(1) | O(n) | O(n) | Frequent lookups |
| Interpolation | Sorted + uniform | O(1) | O(log log n) | O(n) | O(1) | Uniformly distributed |
| Jump Search | Sorted | O(1) | O(√n) | O(√n) | O(1) | Large sorted, jumping better than binary |
| Index Sequential | Sorted | O(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)