Sorting & Searching
Level 2: Intermediate - Master efficient sorting and searching algorithms
Sorting & Searching Algorithms
Merge Sort
Divide and Conquer Approach
Merge Sort is a stable, divide-and-conquer algorithm that divides the array into halves, recursively sorts them, and then merges the sorted halves back together.
š Real-Life Analogy:
Think of organizing a large pile of papers by date. You split the pile in half, ask two friends to sort each half, then merge the two sorted piles by comparing the top papers from each pile.
TypeScript
function mergeSort(arr: number[]): number[] {
if (arr.length <= 1) {
return arr;
}
const mid = Math.floor(arr.length / 2);
const left = mergeSort(arr.slice(0, mid));
const right = mergeSort(arr.slice(mid));
return merge(left, right);
}
function merge(left: number[], right: number[]): number[] {
const result: number[] = [];
let leftIndex = 0;
let rightIndex = 0;
// Compare elements and merge in sorted order
while (leftIndex < left.length && rightIndex < right.length) {
if (left[leftIndex] <= right[rightIndex]) {
result.push(left[leftIndex]);
leftIndex++;
} else {
result.push(right[rightIndex]);
rightIndex++;
}
}
// Add remaining elements
while (leftIndex < left.length) {
result.push(left[leftIndex]);
leftIndex++;
}
while (rightIndex < right.length) {
result.push(right[rightIndex]);
rightIndex++;
}
return result;
}
// Example usage
const unsorted = [64, 34, 25, 12, 22, 11, 90];
const sorted = mergeSort(unsorted);
console.log(sorted); // [11, 12, 22, 25, 34, 64, 90]
Python
def merge_sort(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
return merge(left, right)
def merge(left, right):
result = []
left_index = 0
right_index = 0
# Compare elements and merge in sorted order
while left_index < len(left) and right_index < len(right):
if left[left_index] <= right[right_index]:
result.append(left[left_index])
left_index += 1
else:
result.append(right[right_index])
right_index += 1
# Add remaining elements
while left_index < len(left):
result.append(left[left_index])
left_index += 1
while right_index < len(right):
result.append(right[right_index])
right_index += 1
return result
# Example usage
unsorted = [64, 34, 25, 12, 22, 11, 90]
sorted_arr = merge_sort(unsorted)
print(sorted_arr) # [11, 12, 22, 25, 34, 64, 90]
OCaml
let rec merge_sort = function
| [] | [_] as lst -> lst
| lst ->
let rec split lst acc1 acc2 = function
| 0 -> (List.rev acc1, acc2)
| n -> match lst with
| [] -> (List.rev acc1, acc2)
| h :: t -> split t (h :: acc1) acc2 (n - 1)
in
let len = List.length lst in
let (left, right) = split lst [] lst (len / 2) in
merge (merge_sort left) (merge_sort right)
and merge left right =
let rec merge_aux acc = function
| ([], right) -> List.rev_append acc right
| (left, []) -> List.rev_append acc left
| (h1 :: t1, h2 :: t2) ->
if h1 <= h2 then
merge_aux (h1 :: acc) (t1, h2 :: t2)
else
merge_aux (h2 :: acc) (h1 :: t1, t2)
in
merge_aux [] (left, right)
(* Example usage *)
let unsorted = [64; 34; 25; 12; 22; 11; 90]
let sorted = merge_sort unsorted
(* sorted = [11; 12; 22; 25; 34; 64; 90] *)
šÆ Key Characteristics:
- Stable: Maintains relative order of equal elements
- Consistent: Always O(n log n) time complexity
- Divide & Conquer: Breaks problem into smaller subproblems
- Space: Requires O(n) additional space
| Case | Time Complexity | Space Complexity |
|---|---|---|
| Best | O(n log n) | O(n) |
| Average | O(n log n) | O(n) |
| Worst | O(n log n) | O(n) |
Quick Sort
Partition-Based Sorting
Quick Sort selects a 'pivot' element and partitions the array so that elements smaller than the pivot come before it, and elements greater come after it. It then recursively sorts the partitions.
šÆ Real-Life Analogy:
Think of organizing people by height. You pick one person as a reference (pivot), then arrange everyone shorter to their left and everyone taller to their right. Repeat this process for each group.
TypeScript
function quickSort(arr: number[], low: number = 0, high: number = arr.length - 1): void {
if (low < high) {
// Partition the array and get the pivot index
const pivotIndex = partition(arr, low, high);
// Recursively sort elements before and after partition
quickSort(arr, low, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, high);
}
}
function partition(arr: number[], low: number, high: number): number {
// Choose the rightmost element as pivot
const pivot = arr[high];
let i = low - 1; // Index of smaller element
for (let j = low; j < high; j++) {
// If current element is smaller than or equal to pivot
if (arr[j] <= pivot) {
i++;
[arr[i], arr[j]] = [arr[j], arr[i]]; // Swap elements
}
}
// Place pivot in correct position
[arr[i + 1], arr[high]] = [arr[high], arr[i + 1]];
return i + 1;
}
// Example usage
const numbers = [64, 34, 25, 12, 22, 11, 90];
quickSort(numbers);
console.log(numbers); // [11, 12, 22, 25, 34, 64, 90]
Python
def quick_sort(arr, low=0, high=None):
if high is None:
high = len(arr) - 1
if low < high:
# Partition the array and get the pivot index
pivot_index = partition(arr, low, high)
# Recursively sort elements before and after partition
quick_sort(arr, low, pivot_index - 1)
quick_sort(arr, pivot_index + 1, high)
def partition(arr, low, high):
# Choose the rightmost element as pivot
pivot = arr[high]
i = low - 1 # Index of smaller element
for j in range(low, high):
# If current element is smaller than or equal to pivot
if arr[j] <= pivot:
i += 1
arr[i], arr[j] = arr[j], arr[i] # Swap elements
# Place pivot in correct position
arr[i + 1], arr[high] = arr[high], arr[i + 1]
return i + 1
# Example usage
numbers = [64, 34, 25, 12, 22, 11, 90]
quick_sort(numbers)
print(numbers) # [11, 12, 22, 25, 34, 64, 90]
OCaml
let rec quick_sort = function
| [] -> []
| pivot :: rest ->
let smaller = List.filter (fun x -> x <= pivot) rest in
let larger = List.filter (fun x -> x > pivot) rest in
(quick_sort smaller) @ [pivot] @ (quick_sort larger)
(* Alternative in-place version using arrays *)
let quick_sort_array arr =
let rec sort low high =
if low < high then
let pivot_index = partition arr low high in
sort low (pivot_index - 1);
sort (pivot_index + 1) high
and partition arr low high =
let pivot = arr.(high) in
let i = ref (low - 1) in
for j = low to high - 1 do
if arr.(j) <= pivot then (
incr i;
let temp = arr.(!i) in
arr.(!i) <- arr.(j);
arr.(j) <- temp
)
done;
let temp = arr.(!i + 1) in
arr.(!i + 1) <- arr.(high);
arr.(high) <- temp;
!i + 1
in
sort 0 (Array.length arr - 1)
(* Example usage *)
let numbers = [|64; 34; 25; 12; 22; 11; 90|]
let () = quick_sort_array numbers
(* numbers is now [|11; 12; 22; 25; 34; 64; 90|] *)
| Case | Time Complexity | Space Complexity |
|---|---|---|
| Best | O(n log n) | O(log n) |
| Average | O(n log n) | O(log n) |
| Worst | O(n²) | O(n) |
Binary Search
Efficient Search in Sorted Arrays
Binary Search repeatedly divides a sorted array in half, comparing the target with the middle element to determine which half to search next.
š Real-Life Analogy:
Think of finding a word in a dictionary. You open to the middle, see if your word comes before or after, then repeat with the appropriate half until you find it.
TypeScript
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; // Found the target
} else if (arr[mid] < target) {
left = mid + 1; // Search right half
} else {
right = mid - 1; // Search left half
}
}
return -1; // Target not found
}
// Recursive version
function binarySearchRecursive(
arr: number[],
target: number,
left: number = 0,
right: number = arr.length - 1
): number {
if (left > right) {
return -1; // Target not found
}
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);
}
}
// Example usage
const sortedArray = [11, 12, 22, 25, 34, 64, 90];
console.log(binarySearch(sortedArray, 25)); // 3
console.log(binarySearch(sortedArray, 50)); // -1
Python
def binary_search(arr, target):
left = 0
right = len(arr) - 1
while left <= right:
mid = (left + right) // 2
if arr[mid] == target:
return mid # Found the target
elif arr[mid] < target:
left = mid + 1 # Search right half
else:
right = mid - 1 # Search left half
return -1 # Target not found
# Recursive version
def binary_search_recursive(arr, target, left=0, right=None):
if right is None:
right = len(arr) - 1
if left > right:
return -1 # Target not found
mid = (left + right) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
return binary_search_recursive(arr, target, mid + 1, right)
else:
return binary_search_recursive(arr, target, left, mid - 1)
# Example usage
sorted_array = [11, 12, 22, 25, 34, 64, 90]
print(binary_search(sorted_array, 25)) # 3
print(binary_search(sorted_array, 50)) # -1
OCaml
let binary_search arr target =
let rec search left right =
if left > right then -1
else
let mid = (left + right) / 2 in
if arr.(mid) = target then mid
else if arr.(mid) < target then search (mid + 1) right
else search left (mid - 1)
in
search 0 (Array.length arr - 1)
(* Alternative iterative version *)
let binary_search_iterative arr target =
let left = ref 0 in
let right = ref (Array.length arr - 1) in
let found = ref false in
let result = ref (-1) in
while !left <= !right && not !found do
let mid = (!left + !right) / 2 in
if arr.(mid) = target then (
result := mid;
found := true
) else if arr.(mid) < target then
left := mid + 1
else
right := mid - 1
done;
!result
(* Example usage *)
let sorted_array = [|11; 12; 22; 25; 34; 64; 90|]
let index = binary_search sorted_array 25 (* Returns 3 *)
let not_found = binary_search sorted_array 50 (* Returns -1 *)
šÆ Prerequisites & Applications:
- Prerequisite: Array must be sorted
- Search Range: Finding insertion point for new elements
- Variants: Lower bound, upper bound searches
- Applications: Database indexing, finding peaks, rotated arrays
| Case | Time Complexity | Space Complexity |
|---|---|---|
| Best | O(1) | O(1) |
| Average | O(log n) | O(1) |
| Worst | O(log n) | O(1) |
Heap Sort
Heap-Based Sorting
Heap Sort builds a max heap from the array, then repeatedly extracts the maximum element and places it at the end of the sorted portion.
š Real-Life Analogy:
Think of a tournament where you always know who the strongest player is. You remove the strongest, reorganize to find the next strongest, and repeat until everyone is ranked.
šÆ Key Advantages:
- In-Place: Sorts with O(1) extra space
- Consistent: Always O(n log n) time complexity
- Not Stable: May change relative order of equal elements
- Selection-Based: Good for finding k largest/smallest elements
| Case | Time Complexity | Space Complexity |
|---|---|---|
| Best | O(n log n) | O(1) |
| Average | O(n log n) | O(1) |
| Worst | O(n log n) | O(1) |