Topic Overview
Searching Algorithms
Master searching algorithms: linear search, binary search, interpolation search, and their applications in coding interviews.
Searching is the process of finding a specific element in a data structure. Understanding different searching algorithms and their trade-offs is essential for efficient problem-solving.
Why This Matters in Interviews
Searching algorithms are fundamental because:
- Binary search: Appears in many interview problems (search in rotated array, find peak, etc.)
- Optimization: Many problems can be solved by reducing search space
- Complexity analysis: Understanding O(log n) vs O(n) trade-offs
- Problem patterns: Search problems test your ability to handle edge cases
Interviewers use searching problems to assess your understanding of time complexity, your ability to work with sorted data, and your problem-solving approach.
Core Concepts
- Linear search: Sequential search through elements
- Binary search: Divide-and-conquer on sorted data
- Interpolation search: Optimized for uniformly distributed data
- Search space reduction: Narrowing down possibilities
- Search in rotated arrays: Handling special cases
- Lower/upper bounds: Finding insertion points
Detailed Explanation
Linear Search
function linearSearch(arr: number[], target: number): number {
for (let i = 0; i < arr.length; i++) {
if (arr[i] === target) {
return i;
}
}
return -1;
}
// Time: O(n)
// Space: O(1)
// Works on: Any data structure, sorted or unsorted
Binary Search
Iterative:
function binarySearch(arr: number[], target: number): number {
let left = 0;
let right = arr.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (arr[mid] === target) {
return mid;
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
// Time: O(log n)
// Space: O(1)
// Requires: Sorted array
Recursive:
function binarySearchRecursive(
arr: number[],
target: number,
left: number = 0,
right: number = arr.length - 1
): number {
if (left > right) return -1;
const mid = Math.floor((left + right) / 2);
if (arr[mid] === target) {
return mid;
} else if (arr[mid] < target) {
return binarySearchRecursive(arr, target, mid + 1, right);
} else {
return binarySearchRecursive(arr, target, left, mid - 1);
}
}
// Time: O(log n)
// Space: O(log n) - recursion stack
Lower Bound (First occurrence):
function lowerBound(arr: number[], target: number): number {
let left = 0;
let right = arr.length;
while (left < right) {
const mid = Math.floor((left + right) / 2);
if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
}
// Returns: First index where target can be inserted
Upper Bound (Last occurrence + 1):
function upperBound(arr: number[], target: number): number {
let left = 0;
let right = arr.length;
while (left < right) {
const mid = Math.floor((left + right) / 2);
if (arr[mid] <= target) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
}
// Returns: First index where element > target
Interpolation Search
function interpolationSearch(arr: number[], target: number): number {
let left = 0;
let right = arr.length - 1;
while (left <= right && target >= arr[left] && target <= arr[right]) {
// Formula: pos = low + ((target - arr[low]) * (high - low)) / (arr[high] - arr[low])
const pos = left + Math.floor(
((target - arr[left]) * (right - left)) / (arr[right] - arr[left])
);
if (arr[pos] === target) {
return pos;
} else if (arr[pos] < target) {
left = pos + 1;
} else {
right = pos - 1;
}
}
return -1;
}
// Time: O(log log n) average (uniform distribution), O(n) worst
// Space: O(1)
// Best for: Uniformly distributed sorted data
Examples
Search in Rotated Sorted Array
function searchRotated(nums: number[], target: number): number {
let left = 0;
let right = nums.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (nums[mid] === target) {
return mid;
}
// Left half is sorted
if (nums[left] <= nums[mid]) {
if (target >= nums[left] && target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
} else {
// Right half is sorted
if (target > nums[mid] && target <= nums[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return -1;
}
// Time: O(log n)
Find Peak Element
function findPeakElement(nums: number[]): number {
let left = 0;
let right = nums.length - 1;
while (left < right) {
const mid = Math.floor((left + right) / 2);
if (nums[mid] > nums[mid + 1]) {
// Peak is in left half (including mid)
right = mid;
} else {
// Peak is in right half
left = mid + 1;
}
}
return left;
}
// Time: O(log n)
Search in 2D Matrix
function searchMatrix(matrix: number[][], target: number): boolean {
if (matrix.length === 0) return false;
const rows = matrix.length;
const cols = matrix[0].length;
let left = 0;
let right = rows * cols - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
const midValue = matrix[Math.floor(mid / cols)][mid % cols];
if (midValue === target) {
return true;
} else if (midValue < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return false;
}
// Time: O(log(m * n))
Common Pitfalls
- Integer overflow:
(left + right) / 2can overflow. Fix: Useleft + (right - left) / 2 - Off-by-one errors: Wrong boundary conditions. Fix: Be consistent with inclusive/exclusive
- Not handling duplicates: Binary search may return any occurrence. Fix: Use lower/upper bound
- Unsorted data: Binary search requires sorted data. Fix: Sort first or use linear search
- Infinite loops: Wrong termination condition. Fix: Use
left <= rightorleft < rightconsistently - Missing edge cases: Empty array, single element. Fix: Add explicit checks
Interview Questions
Beginner
Q: Explain binary search. What are its time and space complexities?
A:
Binary Search:
- Requirement: Array must be sorted
- Approach: Divide search space in half at each step
- Process: Compare target with middle element, eliminate half, repeat
Time Complexity:
- Best: O(1) - target is middle element
- Average: O(log n) - eliminates half each iteration
- Worst: O(log n) - target not found
Space Complexity:
- Iterative: O(1) - only variables
- Recursive: O(log n) - recursion stack
Example:
Array: [1, 3, 5, 7, 9, 11, 13]
Target: 7
Step 1: mid = 3, arr[3] = 7 ✓ Found!
Key points:
- Only works on sorted data
- Much faster than linear search for large arrays
- Can be implemented iteratively or recursively
Intermediate
Q: Implement binary search to find the first and last occurrence of a target in a sorted array with duplicates.
A:
function searchRange(nums: number[], target: number): number[] {
const first = findFirst(nums, target);
if (first === -1) {
return [-1, -1];
}
const last = findLast(nums, target);
return [first, last];
}
function findFirst(nums: number[], target: number): number {
let left = 0;
let right = nums.length - 1;
let result = -1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (nums[mid] === target) {
result = mid;
right = mid - 1; // Continue searching left
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return result;
}
function findLast(nums: number[], target: number): number {
let left = 0;
let right = nums.length - 1;
let result = -1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (nums[mid] === target) {
result = mid;
left = mid + 1; // Continue searching right
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return result;
}
// Time: O(log n)
// Space: O(1)
Alternative using lower/upper bound:
function searchRangeOptimized(nums: number[], target: number): number[] {
const first = lowerBound(nums, target);
if (first === nums.length || nums[first] !== target) {
return [-1, -1];
}
const last = upperBound(nums, target) - 1;
return [first, last];
}
Senior
Q: Design a search system for a distributed database where data is partitioned across multiple nodes. How do you handle range queries, node failures, and ensure low latency?
A:
class DistributedSearch {
private nodes: Node[];
private partitioner: Partitioner;
private coordinator: Coordinator;
constructor(nodes: Node[]) {
this.nodes = nodes;
this.partitioner = new RangePartitioner(nodes.length);
this.coordinator = new Coordinator(nodes);
}
async search(target: number): Promise<SearchResult> {
// Determine which node(s) to query
const nodeIds = this.partitioner.getNodesForValue(target);
// Query nodes in parallel
const results = await Promise.allSettled(
nodeIds.map(nodeId => this.nodes[nodeId].search(target))
);
// Find successful result
for (const result of results) {
if (result.status === 'fulfilled' && result.value !== null) {
return result.value;
}
}
return null;
}
async rangeQuery(min: number, max: number): Promise<number[]> {
// Get all nodes in range
const nodeIds = this.partitioner.getNodesForRange(min, max);
// Query each node for its portion of range
const promises = nodeIds.map(nodeId =>
this.nodes[nodeId].rangeQuery(
Math.max(min, this.partitioner.getMin(nodeId)),
Math.min(max, this.partitioner.getMax(nodeId))
)
);
const results = await Promise.all(promises);
// Merge and sort results
return this.mergeAndSort(results);
}
private mergeAndSort(results: number[][]): number[] {
// K-way merge of sorted arrays
const heap = new MinHeap<{ value: number; arrayIndex: number; elementIndex: number }>();
const merged: number[] = [];
// Initialize heap
for (let i = 0; i < results.length; i++) {
if (results[i].length > 0) {
heap.insert({
value: results[i][0],
arrayIndex: i,
elementIndex: 0
});
}
}
// Merge
while (!heap.isEmpty()) {
const min = heap.extractMin()!;
merged.push(min.value);
const nextIndex = min.elementIndex + 1;
if (nextIndex < results[min.arrayIndex].length) {
heap.insert({
value: results[min.arrayIndex][nextIndex],
arrayIndex: min.arrayIndex,
elementIndex: nextIndex
});
}
}
return merged;
}
}
class RangePartitioner {
private ranges: Array<{ min: number; max: number; nodeId: number }>;
constructor(numNodes: number) {
// Partition range [0, MAX] across nodes
this.ranges = this.createRanges(numNodes);
}
getNodesForValue(value: number): number[] {
// Find node(s) that contain this value
// For replication, return multiple nodes
return this.ranges
.filter(range => value >= range.min && value <= range.max)
.map(range => range.nodeId);
}
getNodesForRange(min: number, max: number): number[] {
return this.ranges
.filter(range => !(range.max < min || range.min > max))
.map(range => range.nodeId);
}
getMin(nodeId: number): number {
return this.ranges.find(r => r.nodeId === nodeId)!.min;
}
getMax(nodeId: number): number {
return this.ranges.find(r => r.nodeId === nodeId)!.max;
}
private createRanges(numNodes: number): Array<{ min: number; max: number; nodeId: number }> {
const MAX = Number.MAX_SAFE_INTEGER;
const rangeSize = Math.floor(MAX / numNodes);
const ranges = [];
for (let i = 0; i < numNodes; i++) {
ranges.push({
min: i * rangeSize,
max: i === numNodes - 1 ? MAX : (i + 1) * rangeSize - 1,
nodeId: i
});
}
return ranges;
}
}
Features:
- Range partitioning: Distribute data across nodes
- Parallel queries: Query multiple nodes simultaneously
- Range queries: Efficiently query across partitions
- Failure handling: Use replicas if node fails
- Result merging: K-way merge of sorted results
Key Takeaways
- Linear search: O(n), works on any data, simple implementation
- Binary search: O(log n), requires sorted data, very efficient
- Interpolation search: O(log log n) average, best for uniform distribution
- Lower/upper bounds: Useful for finding insertion points and ranges
- Search space reduction: Key technique in many algorithm problems
- Edge cases: Empty arrays, single element, duplicates, rotated arrays
- Optimizations: Use appropriate algorithm based on data characteristics