Advanced Algorithms
Level 3: Intermediate Advanced - Master sophisticated algorithms for complex problem solving
Advanced Algorithms
Advanced Graph Algorithms
DFS/BFS Variations & Topological Sort
Beyond basic traversal, DFS and BFS can be adapted for complex graph problems including cycle detection, topological sorting, and finding strongly connected components.
🗺️ Real-Life Analogy:
Think of topological sort as planning a course schedule: you must take prerequisites before advanced courses. DFS helps find the right order by exploring dependencies deeply.
🔍 Dijkstra's Algorithm Step-by-Step:
Graph: A→B(4), A→C(2), B→C(1), B→D(5), C→D(8), C→E(10), D→E(2)
Source: A, Find shortest paths to all vertices
Step 1: Initialize distances
A B C D E
0 ∞ ∞ ∞ ∞ ← A is source (distance 0)
Step 2: Process A (current = A, distance = 0)
A B C D E
0 4 2 ∞ ∞ ← Update neighbors of A
↑ ↑
A→B(4) A→C(2)
Step 3: Process C (current = C, distance = 2)
A B C D E
0 3 2 10 12 ← Update via C: B=min(4,2+1)=3, D=2+8=10, E=2+10=12
↑ ↑ ↑
C→B(1) C→D(8) C→E(10)
Step 4: Process B (current = B, distance = 3)
A B C D E
0 3 2 8 12 ← Update via B: D=min(10,3+5)=8
↑
B→D(5)
Step 5: Process D (current = D, distance = 8)
A B C D E
0 3 2 8 10 ← Update via D: E=min(12,8+2)=10
↑
D→E(2)
Final shortest paths from A:
A→A: 0, A→B: 3, A→C: 2, A→D: 8, A→E: 10
Paths: A, A→C→B, A→C, A→C→B→D, A→C→B→D→E
🌍 Real-World Applications:
🗺️ GPS Navigation: Finding shortest routes between locations considering traffic and road conditions
🌐 Network Routing: Determining optimal paths for data packets across internet infrastructure
🎮 Game AI: Pathfinding for NPCs and optimal movement in game worlds
📊 Social Networks: Finding shortest connection paths between users (degrees of separation)
🏭 Supply Chain: Optimizing logistics and delivery routes to minimize cost and time
🔍 Advanced Graph Applications:
- Topological Sort: Order tasks with dependencies
- Cycle Detection: Find circular dependencies
- Strongly Connected Components: Find groups with mutual reachability
- Bipartite Checking: Two-coloring problems
TypeScript
// Bellman-Ford Algorithm for shortest paths with negative weights
class BellmanFord {
findShortestPaths(edges: [number, number, number][], vertices: number, source: number):
{ distances: number[], hasNegativeCycle: boolean } {
const distances = new Array(vertices).fill(Infinity);
distances[source] = 0;
// Relax edges V-1 times
for (let i = 0; i < vertices - 1; i++) {
for (const [u, v, weight] of edges) {
if (distances[u] !== Infinity && distances[u] + weight < distances[v]) {
distances[v] = distances[u] + weight;
}
}
}
// Check for negative cycles
let hasNegativeCycle = false;
for (const [u, v, weight] of edges) {
if (distances[u] !== Infinity && distances[u] + weight < distances[v]) {
hasNegativeCycle = true;
break;
}
}
return { distances, hasNegativeCycle };
}
}
// Floyd-Warshall Algorithm for all-pairs shortest paths
class FloydWarshall {
allPairsShortestPaths(matrix: number[][]): number[][] {
const n = matrix.length;
const dist = matrix.map(row => [...row]);
// Initialize with infinity for no direct path
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
if (i !== j && dist[i][j] === 0) {
dist[i][j] = Infinity;
}
}
}
// Try all intermediate vertices
for (let k = 0; k < n; k++) {
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
if (dist[i][k] + dist[k][j] < dist[i][j]) {
dist[i][j] = dist[i][k] + dist[k][j];
}
}
}
}
return dist;
}
}
// Topological Sort using DFS
class TopologicalSort {
private visited: Set = new Set();
private stack: number[] = [];
topologicalSort(graph: Map): number[] {
this.visited.clear();
this.stack = [];
for (const vertex of graph.keys()) {
if (!this.visited.has(vertex)) {
this.dfs(vertex, graph);
}
}
return this.stack.reverse();
}
private dfs(vertex: number, graph: Map): void {
this.visited.add(vertex);
const neighbors = graph.get(vertex) || [];
for (const neighbor of neighbors) {
if (!this.visited.has(neighbor)) {
this.dfs(neighbor, graph);
}
}
this.stack.push(vertex);
}
}
Python
# Bellman-Ford Algorithm
class BellmanFord:
def find_shortest_paths(self, edges, vertices, source):
"""Find shortest paths with negative edge detection"""
distances = [float('inf')] * vertices
distances[source] = 0
# Relax edges V-1 times
for _ in range(vertices - 1):
for u, v, weight in edges:
if distances[u] != float('inf') and distances[u] + weight < distances[v]:
distances[v] = distances[u] + weight
# Check for negative cycles
has_negative_cycle = False
for u, v, weight in edges:
if distances[u] != float('inf') and distances[u] + weight < distances[v]:
has_negative_cycle = True
break
return distances, has_negative_cycle
# Floyd-Warshall Algorithm
class FloydWarshall:
def all_pairs_shortest_paths(self, matrix):
"""Find shortest paths between all pairs of vertices"""
n = len(matrix)
dist = [row[:] for row in matrix]
# Initialize with infinity for no direct path
for i in range(n):
for j in range(n):
if i != j and dist[i][j] == 0:
dist[i][j] = float('inf')
# Try all intermediate vertices
for k in range(n):
for i in range(n):
for j in range(n):
if dist[i][k] + dist[k][j] < dist[i][j]:
dist[i][j] = dist[i][k] + dist[k][j]
return dist
def has_negative_cycle(self, matrix):
"""Check if graph has negative cycle"""
dist = self.all_pairs_shortest_paths(matrix)
# Check diagonal for negative values
for i in range(len(dist)):
if dist[i][i] < 0:
return True
return False
# Topological Sort
class TopologicalSort:
def __init__(self):
self.visited = set()
self.stack = []
def topological_sort(self, graph):
"""Perform topological sort using DFS"""
self.visited.clear()
self.stack = []
for vertex in graph:
if vertex not in self.visited:
self._dfs(vertex, graph)
return self.stack[::-1]
def _dfs(self, vertex, graph):
"""DFS helper for topological sort"""
self.visited.add(vertex)
for neighbor in graph.get(vertex, []):
if neighbor not in self.visited:
self._dfs(neighbor, graph)
self.stack.append(vertex)
OCaml
(* Bellman-Ford Algorithm *)
module BellmanFord = struct
type edge = int * int * int (* u, v, weight *)
let find_shortest_paths edges vertices source =
let distances = Array.make vertices max_int in
distances.(source) <- 0;
(* Relax edges V-1 times *)
for _ = 1 to vertices - 1 do
List.iter (fun (u, v, weight) ->
if distances.(u) <> max_int && distances.(u) + weight < distances.(v) then
distances.(v) <- distances.(u) + weight
) edges
done;
(* Check for negative cycles *)
let has_negative_cycle = ref false in
List.iter (fun (u, v, weight) ->
if distances.(u) <> max_int && distances.(u) + weight < distances.(v) then
has_negative_cycle := true
) edges;
(distances, !has_negative_cycle)
end
(* Floyd-Warshall Algorithm *)
module FloydWarshall = struct
let all_pairs_shortest_paths matrix =
let n = Array.length matrix in
let dist = Array.make_matrix n n 0 in
(* Initialize distance matrix *)
for i = 0 to n - 1 do
for j = 0 to n - 1 do
dist.(i).(j) <- if i = j then 0
else if matrix.(i).(j) = 0 then max_int
else matrix.(i).(j)
done
done;
(* Try all intermediate vertices *)
for k = 0 to n - 1 do
for i = 0 to n - 1 do
for j = 0 to n - 1 do
if dist.(i).(k) <> max_int && dist.(k).(j) <> max_int then
let new_dist = dist.(i).(k) + dist.(k).(j) in
if new_dist < dist.(i).(j) then
dist.(i).(j) <- new_dist
done
done
done;
dist
end
(* Topological Sort *)
module TopologicalSort = struct
let topological_sort graph =
let visited = Hashtbl.create 16 in
let stack = ref [] in
let rec dfs vertex =
if not (Hashtbl.mem visited vertex) then begin
Hashtbl.add visited vertex true;
let neighbors = try Hashtbl.find graph vertex with Not_found -> [] in
List.iter dfs neighbors;
stack := vertex :: !stack
end
in
Hashtbl.iter (fun vertex _ -> dfs vertex) graph;
!stack
end
| Algorithm | Time Complexity | Space Complexity | Use Case |
|---|---|---|---|
| Bellman-Ford | O(VE) | O(V) | Negative weight edges |
| Floyd-Warshall | O(V³) | O(V²) | All-pairs shortest paths |
| Topological Sort | O(V + E) | O(V) | Task scheduling |
String Algorithms
Pattern Matching Algorithms
String algorithms optimize pattern searching beyond naive O(nm) approaches. KMP uses failure functions, while Rabin-Karp employs rolling hashes for efficient pattern matching.
🔍 Real-Life Analogy:
Think of finding a word in a book: instead of checking every position character by character, KMP remembers partial matches like knowing "comput" in "computer" means you can skip ahead when you see "com" again.
📝 String Algorithm Applications:
- Text Editors: Find and replace functionality
- DNA Sequencing: Pattern matching in genetic data
- Plagiarism Detection: Finding similar text segments
- Network Security: Intrusion detection patterns
TypeScript
// KMP (Knuth-Morris-Pratt) Algorithm
class KMPMatcher {
// Build failure function (longest proper prefix which is also suffix)
private buildFailureFunction(pattern: string): number[] {
const m = pattern.length;
const failure = new Array(m).fill(0);
let j = 0;
for (let i = 1; i < m; i++) {
while (j > 0 && pattern[i] !== pattern[j]) {
j = failure[j - 1];
}
if (pattern[i] === pattern[j]) {
j++;
}
failure[i] = j;
}
return failure;
}
// Find all occurrences of pattern in text
search(text: string, pattern: string): number[] {
if (pattern.length === 0) return [];
const n = text.length;
const m = pattern.length;
const failure = this.buildFailureFunction(pattern);
const matches: number[] = [];
let j = 0;
for (let i = 0; i < n; i++) {
while (j > 0 && text[i] !== pattern[j]) {
j = failure[j - 1];
}
if (text[i] === pattern[j]) {
j++;
}
if (j === m) {
matches.push(i - m + 1);
j = failure[j - 1];
}
}
return matches;
}
}
// Rabin-Karp Algorithm
class RabinKarpMatcher {
private readonly prime = 101;
private readonly base = 256;
search(text: string, pattern: string): number[] {
const n = text.length;
const m = pattern.length;
const matches: number[] = [];
if (m > n) return matches;
// Calculate hash values
let patternHash = 0;
let textHash = 0;
let h = 1;
// Calculate h = base^(m-1) % prime
for (let i = 0; i < m - 1; i++) {
h = (h * this.base) % this.prime;
}
// Calculate initial hash values
for (let i = 0; i < m; i++) {
patternHash = (this.base * patternHash + pattern.charCodeAt(i)) % this.prime;
textHash = (this.base * textHash + text.charCodeAt(i)) % this.prime;
}
// Slide the pattern over text
for (let i = 0; i <= n - m; i++) {
// Check if hash values match
if (patternHash === textHash) {
// Verify character by character
let j;
for (j = 0; j < m; j++) {
if (text[i + j] !== pattern[j]) break;
}
if (j === m) matches.push(i);
}
// Calculate hash for next window
if (i < n - m) {
textHash = (this.base * (textHash - text.charCodeAt(i) * h) +
text.charCodeAt(i + m)) % this.prime;
// Handle negative hash value
if (textHash < 0) textHash += this.prime;
}
}
return matches;
}
}
// Example: Find repeated DNA sequences
function findRepeatedDnaSequences(s: string): string[] {
const seen = new Set();
const repeated = new Set();
for (let i = 0; i <= s.length - 10; i++) {
const sequence = s.substring(i, i + 10);
if (seen.has(sequence)) {
repeated.add(sequence);
}
seen.add(sequence);
}
return Array.from(repeated);
}
Python
# KMP (Knuth-Morris-Pratt) Algorithm
class KMP:
def build_failure_function(self, pattern):
"""Build the failure function for KMP"""
m = len(pattern)
failure = [0] * m
j = 0
for i in range(1, m):
while j > 0 and pattern[i] != pattern[j]:
j = failure[j - 1]
if pattern[i] == pattern[j]:
j += 1
failure[i] = j
return failure
def search(self, text, pattern):
"""Find all occurrences of pattern in text"""
if not pattern:
return []
n, m = len(text), len(pattern)
failure = self.build_failure_function(pattern)
matches = []
j = 0
for i in range(n):
while j > 0 and text[i] != pattern[j]:
j = failure[j - 1]
if text[i] == pattern[j]:
j += 1
if j == m:
matches.append(i - m + 1)
j = failure[j - 1]
return matches
# Rabin-Karp Algorithm
class RabinKarp:
def __init__(self, base=256, prime=101):
self.base = base
self.prime = prime
def search(self, text, pattern):
"""Find all occurrences using rolling hash"""
n, m = len(text), len(pattern)
matches = []
if m > n:
return matches
# Calculate hash values
pattern_hash = 0
text_hash = 0
h = 1
# h = base^(m-1) % prime
for i in range(m - 1):
h = (h * self.base) % self.prime
# Calculate initial hash values
for i in range(m):
pattern_hash = (self.base * pattern_hash + ord(pattern[i])) % self.prime
text_hash = (self.base * text_hash + ord(text[i])) % self.prime
# Slide the pattern over text
for i in range(n - m + 1):
# Check if hash values match
if pattern_hash == text_hash:
# Verify character by character
if text[i:i+m] == pattern:
matches.append(i)
# Calculate hash for next window
if i < n - m:
text_hash = (self.base * (text_hash - ord(text[i]) * h) +
ord(text[i + m])) % self.prime
# Handle negative hash value
if text_hash < 0:
text_hash += self.prime
return matches
# Z-Algorithm for pattern matching
def z_algorithm(s):
"""Compute Z array for string matching"""
n = len(s)
z = [0] * n
l = r = 0
for i in range(1, n):
if i > r:
l = r = i
while r < n and s[r - l] == s[r]:
r += 1
z[i] = r - l
r -= 1
else:
k = i - l
if z[k] < r - i + 1:
z[i] = z[k]
else:
l = i
while r < n and s[r - l] == s[r]:
r += 1
z[i] = r - l
r -= 1
return z
def z_search(text, pattern):
"""Find all occurrences using Z-algorithm"""
combined = pattern + "$" + text
z = z_algorithm(combined)
matches = []
pattern_len = len(pattern)
for i in range(pattern_len + 1, len(combined)):
if z[i] == pattern_len:
matches.append(i - pattern_len - 1)
return matches
# Example: Longest common substring
def longest_common_substring(s1, s2):
"""Find the longest common substring of two strings"""
m, n = len(s1), len(s2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
max_len = 0
ending_pos = 0
for i in range(1, m + 1):
for j in range(1, n + 1):
if s1[i-1] == s2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
if dp[i][j] > max_len:
max_len = dp[i][j]
ending_pos = i
return s1[ending_pos - max_len:ending_pos]
OCaml
(* KMP Algorithm *)
module KMP = struct
let build_failure_function pattern =
let m = String.length pattern in
let failure = Array.make m 0 in
let j = ref 0 in
for i = 1 to m - 1 do
while !j > 0 && pattern.[i] <> pattern.[!j] do
j := failure.(!j - 1)
done;
if pattern.[i] = pattern.[!j] then
incr j;
failure.(i) <- !j
done;
failure
let search text pattern =
if String.length pattern = 0 then []
else
let n = String.length text in
let m = String.length pattern in
let failure = build_failure_function pattern in
let matches = ref [] in
let j = ref 0 in
for i = 0 to n - 1 do
while !j > 0 && text.[i] <> pattern.[!j] do
j := failure.(!j - 1)
done;
if text.[i] = pattern.[!j] then
incr j;
if !j = m then begin
matches := (i - m + 1) :: !matches;
j := failure.(!j - 1)
end
done;
List.rev !matches
end
(* Rabin-Karp Algorithm *)
module RabinKarp = struct
let base = 256
let prime = 101
let hash_string s start len =
let rec hash i acc =
if i >= start + len then acc
else
let acc' = (base * acc + Char.code s.[i]) mod prime in
hash (i + 1) acc'
in
hash start 0
let search text pattern =
let n = String.length text in
let m = String.length pattern in
let matches = ref [] in
if m <= n then begin
let pattern_hash = hash_string pattern 0 m in
let text_hash = ref (hash_string text 0 m) in
(* Calculate h = base^(m-1) mod prime *)
let h = ref 1 in
for i = 1 to m - 1 do
h := (!h * base) mod prime
done;
for i = 0 to n - m do
if !text_hash = pattern_hash then begin
(* Verify character by character *)
let mismatch = ref false in
for j = 0 to m - 1 do
if text.[i + j] <> pattern.[j] then
mismatch := true
done;
if not !mismatch then
matches := i :: !matches
end;
(* Calculate hash for next window *)
if i < n - m then begin
text_hash := (base * (!text_hash - Char.code text.[i] * !h) +
Char.code text.[i + m]) mod prime;
if !text_hash < 0 then
text_hash := !text_hash + prime
end
done
end;
List.rev !matches
end
(* Z-Algorithm *)
module ZAlgorithm = struct
let compute_z_array s =
let n = String.length s in
let z = Array.make n 0 in
let l = ref 0 in
let r = ref 0 in
for i = 1 to n - 1 do
if i > !r then begin
l := i;
r := i;
while !r < n && s.[!r - !l] = s.[!r] do
incr r
done;
z.(i) <- !r - !l;
decr r
end else begin
let k = i - !l in
if z.(k) < !r - i + 1 then
z.(i) <- z.(k)
else begin
l := i;
while !r < n && s.[!r - !l] = s.[!r] do
incr r
done;
z.(i) <- !r - !l;
decr r
end
end
done;
z
let search text pattern =
let combined = pattern ^ "$" ^ text in
let z = compute_z_array combined in
let pattern_len = String.length pattern in
let matches = ref [] in
for i = pattern_len + 1 to String.length combined - 1 do
if z.(i) = pattern_len then
matches := (i - pattern_len - 1) :: !matches
done;
List.rev !matches
end
| Algorithm | Preprocessing | Searching | Space | Best For |
|---|---|---|---|---|
| KMP | O(m) | O(n) | O(m) | No backtracking needed |
| Rabin-Karp | O(m) | O(n) average | O(1) | Multiple pattern search |
| Z-Algorithm | O(m + n) | O(m + n) | O(m + n) | Pattern variations |
Advanced Sorting
Non-Comparison Based Sorting
While comparison-based sorts have a lower bound of O(n log n), non-comparison sorts like Counting Sort and Radix Sort can achieve O(n) time complexity under specific conditions.
📬 Real-Life Analogy:
Think of sorting mail by ZIP code: instead of comparing each letter with others, you create bins for each ZIP code and distribute letters directly into appropriate bins.
🎯 When to Use:
- Counting Sort: Small range of integers
- Radix Sort: Fixed-width integers or strings
- Bucket Sort: Uniformly distributed data
- External Sort: Data too large for memory
TypeScript
// Counting Sort
function countingSort(arr: number[], maxValue: number): number[] {
const count = new Array(maxValue + 1).fill(0);
const output = new Array(arr.length);
// Count occurrences
for (const num of arr) {
count[num]++;
}
// Transform to cumulative count
for (let i = 1; i <= maxValue; i++) {
count[i] += count[i - 1];
}
// Build output array (stable sort)
for (let i = arr.length - 1; i >= 0; i--) {
output[count[arr[i]] - 1] = arr[i];
count[arr[i]]--;
}
return output;
}
// Radix Sort
function radixSort(arr: number[]): number[] {
if (arr.length === 0) return arr;
const getMax = (arr: number[]) => Math.max(...arr);
const getDigit = (num: number, place: number) =>
Math.floor(Math.abs(num) / Math.pow(10, place)) % 10;
const maxNum = getMax(arr);
const maxDigits = Math.floor(Math.log10(maxNum)) + 1;
let result = [...arr];
for (let k = 0; k < maxDigits; k++) {
const buckets: number[][] = Array.from({ length: 10 }, () => []);
for (const num of result) {
const digit = getDigit(num, k);
buckets[digit].push(num);
}
result = buckets.flat();
}
return result;
}
// Bucket Sort
function bucketSort(arr: number[]): number[] {
if (arr.length === 0) return arr;
const n = arr.length;
const buckets: number[][] = Array.from({ length: n }, () => []);
// Find range
const minVal = Math.min(...arr);
const maxVal = Math.max(...arr);
const range = maxVal - minVal;
// Distribute to buckets
for (const num of arr) {
const index = Math.floor(((num - minVal) / range) * (n - 1));
buckets[index].push(num);
}
// Sort individual buckets and concatenate
const result: number[] = [];
for (const bucket of buckets) {
bucket.sort((a, b) => a - b);
result.push(...bucket);
}
return result;
}
// External Merge Sort for large datasets
class ExternalSort {
private chunkSize: number;
constructor(chunkSize: number = 1000000) {
this.chunkSize = chunkSize;
}
// Simulate sorting large file
sort(data: number[]): number[] {
const chunks: number[][] = [];
// Step 1: Sort chunks that fit in memory
for (let i = 0; i < data.length; i += this.chunkSize) {
const chunk = data.slice(i, i + this.chunkSize);
chunk.sort((a, b) => a - b);
chunks.push(chunk);
}
// Step 2: K-way merge
return this.kWayMerge(chunks);
}
private kWayMerge(chunks: number[][]): number[] {
const result: number[] = [];
const indices = new Array(chunks.length).fill(0);
// Min heap for efficient minimum finding
const heap: [number, number][] = []; // [value, chunk index]
// Initialize heap with first element from each chunk
for (let i = 0; i < chunks.length; i++) {
if (chunks[i].length > 0) {
heap.push([chunks[i][0], i]);
}
}
// Min heapify
heap.sort((a, b) => a[0] - b[0]);
while (heap.length > 0) {
const [value, chunkIndex] = heap.shift()!;
result.push(value);
indices[chunkIndex]++;
if (indices[chunkIndex] < chunks[chunkIndex].length) {
heap.push([chunks[chunkIndex][indices[chunkIndex]], chunkIndex]);
heap.sort((a, b) => a[0] - b[0]);
}
}
return result;
}
}
Python
# Counting Sort
def counting_sort(arr, max_value):
"""Sort array using counting sort"""
count = [0] * (max_value + 1)
output = [0] * len(arr)
# Count occurrences
for num in arr:
count[num] += 1
# Transform to cumulative count
for i in range(1, max_value + 1):
count[i] += count[i - 1]
# Build output array (stable sort)
for i in range(len(arr) - 1, -1, -1):
output[count[arr[i]] - 1] = arr[i]
count[arr[i]] -= 1
return output
# Radix Sort
def radix_sort(arr):
"""Sort array using radix sort"""
if not arr:
return arr
def get_digit(num, place):
return (abs(num) // (10 ** place)) % 10
max_num = max(arr)
max_digits = len(str(max_num))
result = arr[:]
for digit_place in range(max_digits):
buckets = [[] for _ in range(10)]
for num in result:
digit = get_digit(num, digit_place)
buckets[digit].append(num)
result = []
for bucket in buckets:
result.extend(bucket)
return result
# Bucket Sort
def bucket_sort(arr):
"""Sort array using bucket sort"""
if not arr:
return arr
n = len(arr)
buckets = [[] for _ in range(n)]
# Find range
min_val = min(arr)
max_val = max(arr)
range_val = max_val - min_val
if range_val == 0:
return arr
# Distribute to buckets
for num in arr:
index = int(((num - min_val) / range_val) * (n - 1))
buckets[index].append(num)
# Sort individual buckets and concatenate
result = []
for bucket in buckets:
bucket.sort()
result.extend(bucket)
return result
# External Sort using heapq
import heapq
class ExternalSort:
def __init__(self, chunk_size=1000000):
self.chunk_size = chunk_size
def sort(self, data):
"""Sort large dataset using external merge sort"""
chunks = []
# Step 1: Sort chunks that fit in memory
for i in range(0, len(data), self.chunk_size):
chunk = sorted(data[i:i + self.chunk_size])
chunks.append(chunk)
# Step 2: K-way merge using heap
return self.k_way_merge(chunks)
def k_way_merge(self, chunks):
"""Merge sorted chunks using a min heap"""
result = []
heap = []
# Initialize heap with iterators
for i, chunk in enumerate(chunks):
if chunk:
# Push (value, chunk_index, element_index)
heapq.heappush(heap, (chunk[0], i, 0))
while heap:
value, chunk_idx, elem_idx = heapq.heappop(heap)
result.append(value)
# Add next element from same chunk if available
if elem_idx + 1 < len(chunks[chunk_idx]):
next_value = chunks[chunk_idx][elem_idx + 1]
heapq.heappush(heap, (next_value, chunk_idx, elem_idx + 1))
return result
OCaml
(* Counting Sort *)
let counting_sort arr max_value =
let n = Array.length arr in
let count = Array.make (max_value + 1) 0 in
let output = Array.make n 0 in
(* Count occurrences *)
Array.iter (fun x -> count.(x) <- count.(x) + 1) arr;
(* Transform to cumulative count *)
for i = 1 to max_value do
count.(i) <- count.(i) + count.(i - 1)
done;
(* Build output array *)
for i = n - 1 downto 0 do
let value = arr.(i) in
output.(count.(value) - 1) <- value;
count.(value) <- count.(value) - 1
done;
output
(* Radix Sort *)
let radix_sort arr =
if Array.length arr = 0 then arr
else
let get_digit num place =
(abs num / int_of_float (10.0 ** float_of_int place)) mod 10
in
let max_num = Array.fold_left max arr.(0) arr in
let max_digits = String.length (string_of_int max_num) in
let result = ref (Array.copy arr) in
for digit_place = 0 to max_digits - 1 do
let buckets = Array.init 10 (fun _ -> ref []) in
(* Distribute to buckets *)
Array.iter (fun num ->
let digit = get_digit num digit_place in
buckets.(digit) := num :: !(buckets.(digit))
) !result;
(* Collect from buckets *)
let collected = ref [] in
Array.iter (fun bucket ->
collected := !collected @ (List.rev !bucket)
) buckets;
result := Array.of_list !collected
done;
!result
| Algorithm | Time Complexity | Space Complexity | Stable | Constraints |
|---|---|---|---|---|
| Counting Sort | O(n + k) | O(k) | Yes | k = range of input |
| Radix Sort | O(d × n) | O(n) | Yes | d = number of digits |
| Bucket Sort | O(n) average | O(n) | Yes | Uniform distribution |
| External Sort | O(n log n) | O(M) | Yes | M = memory limit |
Divide & Conquer Algorithms
Advanced Divide & Conquer Techniques
Divide and conquer algorithms solve problems by breaking them into smaller subproblems, solving them independently, and combining results. Advanced techniques optimize the division and combination steps.
🧩 Real-Life Analogy:
Think of assembling a large jigsaw puzzle: divide it into sections (sky, ground, etc.), solve each section independently with different people, then combine the completed sections.
🎯 Key Divide & Conquer Algorithms:
- Closest Pair of Points: Geometric divide and conquer
- Fast Fourier Transform: Signal processing
- Karatsuba Multiplication: Large number multiplication
- Strassen's Matrix Multiplication: Efficient matrix operations
TypeScript
// Closest Pair of Points
interface Point {
x: number;
y: number;
}
class ClosestPair {
private distance(p1: Point, p2: Point): number {
return Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2);
}
private bruteForce(points: Point[]): number {
let minDist = Infinity;
for (let i = 0; i < points.length; i++) {
for (let j = i + 1; j < points.length; j++) {
minDist = Math.min(minDist, this.distance(points[i], points[j]));
}
}
return minDist;
}
private stripClosest(strip: Point[], d: number): number {
let minDist = d;
strip.sort((a, b) => a.y - b.y);
for (let i = 0; i < strip.length; i++) {
for (let j = i + 1; j < strip.length && (strip[j].y - strip[i].y) < minDist; j++) {
minDist = Math.min(minDist, this.distance(strip[i], strip[j]));
}
}
return minDist;
}
private closestPairRec(px: Point[], py: Point[]): number {
const n = px.length;
if (n <= 3) {
return this.bruteForce(px);
}
const mid = Math.floor(n / 2);
const midpoint = px[mid];
const pyl = py.filter(point => point.x <= midpoint.x);
const pyr = py.filter(point => point.x > midpoint.x);
const dl = this.closestPairRec(px.slice(0, mid), pyl);
const dr = this.closestPairRec(px.slice(mid), pyr);
const d = Math.min(dl, dr);
const strip = py.filter(point => Math.abs(point.x - midpoint.x) < d);
return Math.min(d, this.stripClosest(strip, d));
}
findClosestPair(points: Point[]): number {
const px = [...points].sort((a, b) => a.x - b.x);
const py = [...points].sort((a, b) => a.y - b.y);
return this.closestPairRec(px, py);
}
}
// Karatsuba Multiplication
class KaratsubaMultiplication {
multiply(x: bigint, y: bigint): bigint {
if (x < 10n || y < 10n) {
return x * y;
}
const xStr = x.toString();
const yStr = y.toString();
const n = Math.max(xStr.length, yStr.length);
const half = Math.floor(n / 2);
const multiplier = 10n ** BigInt(half);
const high1 = x / multiplier;
const low1 = x % multiplier;
const high2 = y / multiplier;
const low2 = y % multiplier;
const z0 = this.multiply(low1, low2);
const z1 = this.multiply(low1 + high1, low2 + high2);
const z2 = this.multiply(high1, high2);
return z2 * (multiplier ** 2n) + (z1 - z2 - z0) * multiplier + z0;
}
}
// Matrix Multiplication (Strassen's Algorithm)
class StrassenMatrix {
multiply(A: number[][], B: number[][]): number[][] {
const n = A.length;
if (n <= 2) {
return this.standardMultiply(A, B);
}
const half = Math.floor(n / 2);
// Divide matrices into quadrants
const A11 = this.getSubMatrix(A, 0, 0, half);
const A12 = this.getSubMatrix(A, 0, half, half);
const A21 = this.getSubMatrix(A, half, 0, half);
const A22 = this.getSubMatrix(A, half, half, half);
const B11 = this.getSubMatrix(B, 0, 0, half);
const B12 = this.getSubMatrix(B, 0, half, half);
const B21 = this.getSubMatrix(B, half, 0, half);
const B22 = this.getSubMatrix(B, half, half, half);
// Calculate 7 products (Strassen's method)
const M1 = this.multiply(this.add(A11, A22), this.add(B11, B22));
const M2 = this.multiply(this.add(A21, A22), B11);
const M3 = this.multiply(A11, this.subtract(B12, B22));
const M4 = this.multiply(A22, this.subtract(B21, B11));
const M5 = this.multiply(this.add(A11, A12), B22);
const M6 = this.multiply(this.subtract(A21, A11), this.add(B11, B12));
const M7 = this.multiply(this.subtract(A12, A22), this.add(B21, B22));
// Calculate result quadrants
const C11 = this.add(this.subtract(this.add(M1, M4), M5), M7);
const C12 = this.add(M3, M5);
const C21 = this.add(M2, M4);
const C22 = this.add(this.subtract(this.add(M1, M3), M2), M6);
// Combine quadrants
return this.combineMatrices(C11, C12, C21, C22);
}
private standardMultiply(A: number[][], B: number[][]): number[][] {
const n = A.length;
const result = Array(n).fill(0).map(() => Array(n).fill(0));
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
for (let k = 0; k < n; k++) {
result[i][j] += A[i][k] * B[k][j];
}
}
}
return result;
}
private getSubMatrix(matrix: number[][], startRow: number, startCol: number, size: number): number[][] {
const result = Array(size).fill(0).map(() => Array(size).fill(0));
for (let i = 0; i < size; i++) {
for (let j = 0; j < size; j++) {
result[i][j] = matrix[startRow + i][startCol + j];
}
}
return result;
}
private add(A: number[][], B: number[][]): number[][] {
const n = A.length;
const result = Array(n).fill(0).map(() => Array(n).fill(0));
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
result[i][j] = A[i][j] + B[i][j];
}
}
return result;
}
private subtract(A: number[][], B: number[][]): number[][] {
const n = A.length;
const result = Array(n).fill(0).map(() => Array(n).fill(0));
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
result[i][j] = A[i][j] - B[i][j];
}
}
return result;
}
private combineMatrices(C11: number[][], C12: number[][], C21: number[][], C22: number[][]): number[][] {
const half = C11.length;
const n = half * 2;
const result = Array(n).fill(0).map(() => Array(n).fill(0));
for (let i = 0; i < half; i++) {
for (let j = 0; j < half; j++) {
result[i][j] = C11[i][j];
result[i][j + half] = C12[i][j];
result[i + half][j] = C21[i][j];
result[i + half][j + half] = C22[i][j];
}
}
return result;
}
}
Python
# Closest Pair of Points
import math
from typing import List, Tuple
class Point:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
class ClosestPair:
def distance(self, p1: Point, p2: Point) -> float:
return math.sqrt((p1.x - p2.x)**2 + (p1.y - p2.y)**2)
def brute_force(self, points: List[Point]) -> float:
min_dist = float('inf')
n = len(points)
for i in range(n):
for j in range(i + 1, n):
min_dist = min(min_dist, self.distance(points[i], points[j]))
return min_dist
def strip_closest(self, strip: List[Point], d: float) -> float:
min_dist = d
strip.sort(key=lambda p: p.y)
for i in range(len(strip)):
j = i + 1
while j < len(strip) and (strip[j].y - strip[i].y) < min_dist:
min_dist = min(min_dist, self.distance(strip[i], strip[j]))
j += 1
return min_dist
def closest_pair_rec(self, px: List[Point], py: List[Point]) -> float:
n = len(px)
if n <= 3:
return self.brute_force(px)
mid = n // 2
midpoint = px[mid]
pyl = [point for point in py if point.x <= midpoint.x]
pyr = [point for point in py if point.x > midpoint.x]
dl = self.closest_pair_rec(px[:mid], pyl)
dr = self.closest_pair_rec(px[mid:], pyr)
d = min(dl, dr)
strip = [point for point in py if abs(point.x - midpoint.x) < d]
return min(d, self.strip_closest(strip, d))
def find_closest_pair(self, points: List[Point]) -> float:
px = sorted(points, key=lambda p: p.x)
py = sorted(points, key=lambda p: p.y)
return self.closest_pair_rec(px, py)
# Karatsuba Multiplication
class KaratsubaMultiplication:
def multiply(self, x: int, y: int) -> int:
if x < 10 or y < 10:
return x * y
n = max(len(str(x)), len(str(y)))
half = n // 2
multiplier = 10 ** half
high1, low1 = divmod(x, multiplier)
high2, low2 = divmod(y, multiplier)
z0 = self.multiply(low1, low2)
z1 = self.multiply(low1 + high1, low2 + high2)
z2 = self.multiply(high1, high2)
return z2 * (multiplier ** 2) + (z1 - z2 - z0) * multiplier + z0
# Matrix Multiplication (Strassen's Algorithm)
class StrassenMatrix:
def multiply(self, A: List[List[int]], B: List[List[int]]) -> List[List[int]]:
n = len(A)
if n <= 2:
return self.standard_multiply(A, B)
half = n // 2
# Divide matrices into quadrants
A11 = self.get_sub_matrix(A, 0, 0, half)
A12 = self.get_sub_matrix(A, 0, half, half)
A21 = self.get_sub_matrix(A, half, 0, half)
A22 = self.get_sub_matrix(A, half, half, half)
B11 = self.get_sub_matrix(B, 0, 0, half)
B12 = self.get_sub_matrix(B, 0, half, half)
B21 = self.get_sub_matrix(B, half, 0, half)
B22 = self.get_sub_matrix(B, half, half, half)
# Calculate 7 products (Strassen's method)
M1 = self.multiply(self.add(A11, A22), self.add(B11, B22))
M2 = self.multiply(self.add(A21, A22), B11)
M3 = self.multiply(A11, self.subtract(B12, B22))
M4 = self.multiply(A22, self.subtract(B21, B11))
M5 = self.multiply(self.add(A11, A12), B22)
M6 = self.multiply(self.subtract(A21, A11), self.add(B11, B12))
M7 = self.multiply(self.subtract(A12, A22), self.add(B21, B22))
# Calculate result quadrants
C11 = self.add(self.subtract(self.add(M1, M4), M5), M7)
C12 = self.add(M3, M5)
C21 = self.add(M2, M4)
C22 = self.add(self.subtract(self.add(M1, M3), M2), M6)
# Combine quadrants
return self.combine_matrices(C11, C12, C21, C22)
def standard_multiply(self, A: List[List[int]], B: List[List[int]]) -> List[List[int]]:
n = len(A)
result = [[0] * n for _ in range(n)]
for i in range(n):
for j in range(n):
for k in range(n):
result[i][j] += A[i][k] * B[k][j]
return result
def get_sub_matrix(self, matrix: List[List[int]], start_row: int, start_col: int, size: int) -> List[List[int]]:
return [[matrix[start_row + i][start_col + j] for j in range(size)] for i in range(size)]
def add(self, A: List[List[int]], B: List[List[int]]) -> List[List[int]]:
n = len(A)
return [[A[i][j] + B[i][j] for j in range(n)] for i in range(n)]
def subtract(self, A: List[List[int]], B: List[List[int]]) -> List[List[int]]:
n = len(A)
return [[A[i][j] - B[i][j] for j in range(n)] for i in range(n)]
def combine_matrices(self, C11, C12, C21, C22):
half = len(C11)
n = half * 2
result = [[0] * n for _ in range(n)]
for i in range(half):
for j in range(half):
result[i][j] = C11[i][j]
result[i][j + half] = C12[i][j]
result[i + half][j] = C21[i][j]
result[i + half][j + half] = C22[i][j]
return result
OCaml
(* Closest Pair of Points *)
module ClosestPair = struct
type point = { x: float; y: float }
let distance p1 p2 =
sqrt ((p1.x -. p2.x) ** 2.0 +. (p1.y -. p2.y) ** 2.0)
let brute_force points =
let rec loop i j min_dist =
if i >= Array.length points then min_dist
else if j >= Array.length points then loop (i + 1) (i + 2) min_dist
else
let dist = distance points.(i) points.(j) in
loop i (j + 1) (min min_dist dist)
in
if Array.length points < 2 then infinity
else loop 0 1 infinity
let strip_closest strip d =
Array.sort (fun p1 p2 -> compare p1.y p2.y) strip;
let min_dist = ref d in
for i = 0 to Array.length strip - 1 do
let j = ref (i + 1) in
while !j < Array.length strip && (strip.(!j).y -. strip.(i).y) < !min_dist do
min_dist := min !min_dist (distance strip.(i) strip.(!j));
incr j
done
done;
!min_dist
let rec closest_pair_rec px py =
let n = Array.length px in
if n <= 3 then brute_force px
else
let mid = n / 2 in
let midpoint = px.(mid) in
let pyl = Array.of_list (List.filter (fun p -> p.x <= midpoint.x) (Array.to_list py)) in
let pyr = Array.of_list (List.filter (fun p -> p.x > midpoint.x) (Array.to_list py)) in
let dl = closest_pair_rec (Array.sub px 0 mid) pyl in
let dr = closest_pair_rec (Array.sub px mid (n - mid)) pyr in
let d = min dl dr in
let strip = Array.of_list (List.filter (fun p -> abs_float (p.x -. midpoint.x) < d) (Array.to_list py)) in
min d (strip_closest strip d)
let find_closest_pair points =
let px = Array.copy points in
let py = Array.copy points in
Array.sort (fun p1 p2 -> compare p1.x p2.x) px;
Array.sort (fun p1 p2 -> compare p1.y p2.y) py;
closest_pair_rec px py
end
(* Karatsuba Multiplication *)
module Karatsuba = struct
let rec multiply x y =
if x < 10 || y < 10 then x * y
else
let x_str = string_of_int x in
let y_str = string_of_int y in
let n = max (String.length x_str) (String.length y_str) in
let half = n / 2 in
let multiplier = int_of_float (10.0 ** float_of_int half) in
let high1 = x / multiplier in
let low1 = x mod multiplier in
let high2 = y / multiplier in
let low2 = y mod multiplier in
let z0 = multiply low1 low2 in
let z1 = multiply (low1 + high1) (low2 + high2) in
let z2 = multiply high1 high2 in
z2 * (multiplier * multiplier) + (z1 - z2 - z0) * multiplier + z0
end
| Algorithm | Time Complexity | Traditional | Improvement |
|---|---|---|---|
| Closest Pair | O(n log n) | O(n²) | Divide plane by x-coordinate |
| Karatsuba | O(n^1.585) | O(n²) | 3 multiplications instead of 4 |
| Strassen's | O(n^2.807) | O(n³) | 7 multiplications instead of 8 |
| FFT | O(n log n) | O(n²) | Divide by even/odd indices |