Searching Algorithms
Searching Algorithms#
Searching algorithms locate specific elements within data structures. Modern languages provide highly optimized built-in search methods that leverage appropriate data structures for optimal performance.
Think of finding a book: searching page-by-page is slow (linear search), but using an index or organized catalog enables instant retrieval (binary search, hash lookup).
Search Strategy Selection#
graph TD
A[Search Strategy] --> B{Data Sorted?}
B -->|Yes| C[Binary Search - O log n]
B -->|No| D{Need Fast Lookup?}
D -->|Yes| E[Hash Table - O 1]
D -->|No| F[Linear Search - O n]
style C fill:#c8e6c9
style E fill:#e3f2fd
style F fill:#ffecb3
Time Complexity#
| Algorithm | Time | Space | Requirements |
|---|---|---|---|
| Hash Lookup | O(1) avg | O(n) | Hash table/set |
| Binary Search | O(log n) | O(1) | Sorted array |
| Linear Search | O(n) | O(1) | Any collection |
| BFS/DFS | O(V + E) | O(V) | Graph traversal |
Hash-Based Search (Fastest)#
Use hash tables/sets for O(1) average-case lookups when order doesn't matter.
import java.util.*;
// HashMap lookup - O(1)
Map<String, Integer> map = new HashMap<>();
map.put("apple", 5);
map.put("banana", 3);
boolean exists = map.containsKey("apple"); // O(1)
Integer value = map.get("apple"); // O(1) -> 5
// HashSet membership - O(1)
Set<String> set = new HashSet<>();
set.add("apple");
set.add("banana");
boolean found = set.contains("apple"); // O(1) -> true
// HashMap lookup - O(1)
val map = mutableMapOf<String, Int>()
map["apple"] = 5
map["banana"] = 3
val exists = map.containsKey("apple") // O(1) -> true
val value = map["apple"] // O(1) -> 5
// HashSet membership - O(1)
val set = mutableSetOf<String>()
set.add("apple")
set.add("banana")
val found = set.contains("apple") // O(1) -> true
// Map lookup - O(1)
const map = new Map<string, number>();
map.set("apple", 5);
map.set("banana", 3);
const exists = map.has("apple"); // O(1) -> true
const value = map.get("apple"); // O(1) -> 5
// Set membership - O(1)
const set = new Set<string>();
set.add("apple");
set.add("banana");
const found = set.has("apple"); // O(1) -> true
// Map lookup - O(1)
Map<String, int> map = {};
map["apple"] = 5;
map["banana"] = 3;
bool exists = map.containsKey("apple"); // O(1) -> true
int? value = map["apple"]; // O(1) -> 5
// Set membership - O(1)
Set<String> set = {};
set.add("apple");
set.add("banana");
bool found = set.contains("apple"); // O(1) -> true
// Dictionary lookup - O(1)
var dict = [String: Int]()
dict["apple"] = 5
dict["banana"] = 3
let exists = dict.keys.contains("apple") // O(1) -> true
let value = dict["apple"] // O(1) -> 5
// Set membership - O(1)
var set = Set<String>()
set.insert("apple")
set.insert("banana")
let found = set.contains("apple") // O(1) -> true
Binary Search (Sorted Data)#
Efficiently searches sorted arrays by repeatedly halving the search space. O(log n) time complexity.
import java.util.Arrays;
// Built-in binary search - O(log n)
int[] sortedArray = {1, 3, 5, 7, 9, 11, 13, 15};
int index = Arrays.binarySearch(sortedArray, 7); // Returns 3
// Custom implementation
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; // Not found
}
// Built-in binary search - O(log n)
val sortedArray = intArrayOf(1, 3, 5, 7, 9, 11, 13, 15)
val index = sortedArray.binarySearch(7) // Returns 3
// Custom implementation
fun binarySearch(arr: IntArray, target: Int): Int {
var left = 0
var right = arr.size - 1
while (left <= right) {
val mid = left + (right - left) / 2
when {
arr[mid] == target -> return mid
arr[mid] < target -> left = mid + 1
else -> right = mid - 1
}
}
return -1
}
// Custom binary search - O(log n)
function binarySearch(arr: number[], target: number): number {
let left = 0, right = arr.length - 1;
while (left <= right) {
const mid = Math.floor(left + (right - left) / 2);
if (arr[mid] === target) return mid;
if (arr[mid] < target) left = mid + 1;
else right = mid - 1;
}
return -1;
}
const sortedArray = [1, 3, 5, 7, 9, 11, 13, 15];
const index = binarySearch(sortedArray, 7); // Returns 3
// Custom binary search - O(log n)
int binarySearch(List<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;
}
List<int> sortedArray = [1, 3, 5, 7, 9, 11, 13, 15];
int index = binarySearch(sortedArray, 7); // Returns 3
// Custom binary search - O(log n)
func binarySearch(_ arr: [Int], target: Int) -> Int {
var left = 0, right = arr.count - 1
while left <= right {
let mid = left + (right - left) / 2
if arr[mid] == target { return mid }
if arr[mid] < target { left = mid + 1 }
else { right = mid - 1 }
}
return -1
}
let sortedArray = [1, 3, 5, 7, 9, 11, 13, 15]
let index = binarySearch(sortedArray, target: 7) // Returns 3
import bisect
# Built-in binary search - O(log n)
sorted_array = [1, 3, 5, 7, 9, 11, 13, 15]
index = bisect.bisect_left(sorted_array, 7) # Returns 3
# Custom implementation
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
if arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
index = binary_search(sorted_array, 7) # Returns 3
Linear Search (Unsorted Data)#
Checks each element sequentially. O(n) time complexity. Use when data is unsorted and small, or hash tables aren't available.
// Linear search - O(n)
function linearSearch(arr: number[], target: number): number {
for (let i = 0; i < arr.length; i++) {
if (arr[i] === target) return i;
}
return -1;
}
// Using built-in methods
const arr = [4, 2, 7, 1, 9];
const found = arr.includes(7); // true
const index = arr.indexOf(7); // 2
Graph Search (BFS/DFS)#
For searching in graphs and trees. See Graphs section for detailed implementations.
- Breadth-First Search (BFS): Level-by-level exploration using a queue. Finds shortest path in unweighted graphs
- Depth-First Search (DFS): Deep exploration using recursion/stack. Good for pathfinding and cycle detection
Applications#
- Fast Lookups: Use hash tables/sets for O(1) membership testing
- Sorted Data: Use binary search for O(log n) searches in sorted arrays
- Range Queries: Binary search to find boundaries in sorted data
- Graph Navigation: BFS for shortest paths, DFS for connectivity
- Database Indexing: B-trees combine aspects of binary search for disk-based storage
Search Algorithm Selection#
| Scenario | Algorithm | Reason |
|---|---|---|
| Frequent lookups, no order needed | Hash table | O(1) average case |
| Sorted array | Binary search | O(log n) efficient |
| Unsorted small dataset | Linear search | Simple, adequate |
| Graph/tree traversal | BFS/DFS | Natural fit |
| Range queries | Binary search | Find boundaries |
Key Insight: Choose the data structure first (hash table, sorted array, graph), then use the appropriate built-in search method. Custom implementations rarely outperform well-tested standard libraries.