Practice Focus
Level 3: Intermediate Advanced - Master complex problem-solving strategies and interview preparation
Practice Focus
Complex Problem Decomposition
Breaking Down Complex Problems
Complex problems often require systematic decomposition into smaller, manageable subproblems. This involves identifying patterns, recognizing familiar structures, and applying appropriate algorithms.
🏗️ Real-Life Analogy:
Think of building a house: you don't build everything at once. You break it down into foundation, framing, plumbing, electrical, and finishing. Each part has its own requirements and can be tackled separately.
🎯 Decomposition Strategies:
- Identify Core Problem: Strip away details to find the fundamental challenge
- Recognize Patterns: Map to known problem types (graph, DP, greedy, etc.)
- Define Subproblems: Break into smaller, solvable pieces
- Build Solutions: Combine subproblem solutions systematically
TypeScript
// Example: Word Break Problem Decomposition
// Problem: Can we break string s into words from dictionary?
// Step 1: Identify core problem - sequence segmentation
// Step 2: Recognize pattern - dynamic programming
// Step 3: Define subproblems - can break s[0:i]?
// Step 4: Build solution - combine smaller solutions
class WordBreakSolver {
// Approach 1: Top-down with memoization
wordBreakMemo(s: string, wordDict: string[]): boolean {
const wordSet = new Set(wordDict);
const memo = new Map();
return this.canBreak(s, 0, wordSet, memo);
}
private canBreak(s: string, start: number,
wordSet: Set,
memo: Map): boolean {
// Base case
if (start === s.length) return true;
// Check memo
if (memo.has(start)) return memo.get(start)!;
// Try all possible word endings
for (let end = start + 1; end <= s.length; end++) {
const word = s.substring(start, end);
if (wordSet.has(word) && this.canBreak(s, end, wordSet, memo)) {
memo.set(start, true);
return true;
}
}
memo.set(start, false);
return false;
}
// Approach 2: Bottom-up dynamic programming
wordBreakDP(s: string, wordDict: string[]): boolean {
const wordSet = new Set(wordDict);
const n = s.length;
const dp = new Array(n + 1).fill(false);
dp[0] = true; // Empty string
for (let i = 1; i <= n; i++) {
for (let j = 0; j < i; j++) {
if (dp[j] && wordSet.has(s.substring(j, i))) {
dp[i] = true;
break;
}
}
}
return dp[n];
}
// Approach 3: BFS for finding all solutions
wordBreakAllSolutions(s: string, wordDict: string[]): string[][] {
const wordSet = new Set(wordDict);
const memo = new Map();
return this.findAllBreaks(s, 0, wordSet, memo);
}
private findAllBreaks(s: string, start: number,
wordSet: Set,
memo: Map): string[][] {
if (memo.has(start)) return memo.get(start)!;
const result: string[][] = [];
if (start === s.length) {
result.push([]);
return result;
}
for (let end = start + 1; end <= s.length; end++) {
const word = s.substring(start, end);
if (wordSet.has(word)) {
const suffixBreaks = this.findAllBreaks(s, end, wordSet, memo);
for (const suffix of suffixBreaks) {
result.push([word, ...suffix]);
}
}
}
memo.set(start, result);
return result;
}
}
// Example: Complex scheduling problem
// Problem: Schedule tasks with dependencies and resource constraints
interface Task {
id: string;
duration: number;
dependencies: string[];
resources: string[];
}
class TaskScheduler {
// Decomposition:
// 1. Topological sort for dependencies
// 2. Resource conflict resolution
// 3. Time optimization
scheduleTask(tasks: Task[]): Map {
// Step 1: Build dependency graph
const graph = this.buildDependencyGraph(tasks);
// Step 2: Topological sort
const order = this.topologicalSort(graph);
// Step 3: Schedule with resource constraints
return this.scheduleWithResources(tasks, order);
}
private buildDependencyGraph(tasks: Task[]): Map {
const graph = new Map();
const taskMap = new Map();
for (const task of tasks) {
taskMap.set(task.id, task);
graph.set(task.id, []);
}
for (const task of tasks) {
for (const dep of task.dependencies) {
graph.get(dep)!.push(task.id);
}
}
return graph;
}
private topologicalSort(graph: Map): string[] {
const inDegree = new Map();
const queue: string[] = [];
const result: string[] = [];
// Calculate in-degrees
for (const [node, _] of graph) {
inDegree.set(node, 0);
}
for (const [_, neighbors] of graph) {
for (const neighbor of neighbors) {
inDegree.set(neighbor, inDegree.get(neighbor)! + 1);
}
}
// Find nodes with no dependencies
for (const [node, degree] of inDegree) {
if (degree === 0) queue.push(node);
}
// Process queue
while (queue.length > 0) {
const current = queue.shift()!;
result.push(current);
for (const neighbor of graph.get(current)!) {
const newDegree = inDegree.get(neighbor)! - 1;
inDegree.set(neighbor, newDegree);
if (newDegree === 0) queue.push(neighbor);
}
}
return result;
}
private scheduleWithResources(tasks: Task[],
order: string[]): Map {
const schedule = new Map();
const resourceAvailable = new Map();
const taskMap = new Map();
for (const task of tasks) {
taskMap.set(task.id, task);
}
for (const taskId of order) {
const task = taskMap.get(taskId)!;
let startTime = 0;
// Check dependencies
for (const dep of task.dependencies) {
const depEnd = schedule.get(dep)! + taskMap.get(dep)!.duration;
startTime = Math.max(startTime, depEnd);
}
// Check resource availability
for (const resource of task.resources) {
const available = resourceAvailable.get(resource) || 0;
startTime = Math.max(startTime, available);
}
// Schedule task
schedule.set(taskId, startTime);
// Update resource availability
for (const resource of task.resources) {
resourceAvailable.set(resource, startTime + task.duration);
}
}
return schedule;
}
}
Python
# Problem Decomposition Example: Island Problems
# Shows how similar problems can be solved with pattern recognition
class IslandProblemSolver:
"""Demonstrates decomposition of various island-related problems"""
def __init__(self):
self.directions = [(0, 1), (1, 0), (0, -1), (-1, 0)]
# Problem 1: Number of Islands
def num_islands(self, grid):
"""Count number of islands in grid"""
if not grid or not grid[0]:
return 0
rows, cols = len(grid), len(grid[0])
visited = [[False] * cols for _ in range(rows)]
count = 0
def dfs(r, c):
if (r < 0 or r >= rows or c < 0 or c >= cols or
visited[r][c] or grid[r][c] == '0'):
return
visited[r][c] = True
for dr, dc in self.directions:
dfs(r + dr, c + dc)
for r in range(rows):
for c in range(cols):
if grid[r][c] == '1' and not visited[r][c]:
dfs(r, c)
count += 1
return count
# Problem 2: Max Area of Island
def max_area_of_island(self, grid):
"""Find maximum area of any island"""
if not grid or not grid[0]:
return 0
rows, cols = len(grid), len(grid[0])
max_area = 0
def dfs(r, c):
if (r < 0 or r >= rows or c < 0 or c >= cols or
grid[r][c] == 0):
return 0
grid[r][c] = 0 # Mark visited
area = 1
for dr, dc in self.directions:
area += dfs(r + dr, c + dc)
return area
for r in range(rows):
for c in range(cols):
if grid[r][c] == 1:
max_area = max(max_area, dfs(r, c))
return max_area
# Problem 3: Island Perimeter
def island_perimeter(self, grid):
"""Calculate perimeter of the island"""
if not grid or not grid[0]:
return 0
rows, cols = len(grid), len(grid[0])
perimeter = 0
for r in range(rows):
for c in range(cols):
if grid[r][c] == 1:
# Check all 4 sides
for dr, dc in self.directions:
nr, nc = r + dr, c + dc
# Add to perimeter if edge or water
if (nr < 0 or nr >= rows or nc < 0 or nc >= cols or
grid[nr][nc] == 0):
perimeter += 1
return perimeter
# Problem 4: Surrounded Regions (capture problem)
def solve_surrounded_regions(self, board):
"""Capture regions surrounded by 'X'"""
if not board or not board[0]:
return
rows, cols = len(board), len(board[0])
def dfs(r, c):
if (r < 0 or r >= rows or c < 0 or c >= cols or
board[r][c] != 'O'):
return
board[r][c] = 'E' # Mark edge-connected
for dr, dc in self.directions:
dfs(r + dr, c + dc)
# Mark edge-connected regions
for r in range(rows):
dfs(r, 0)
dfs(r, cols - 1)
for c in range(cols):
dfs(0, c)
dfs(rows - 1, c)
# Capture surrounded regions
for r in range(rows):
for c in range(cols):
if board[r][c] == 'O':
board[r][c] = 'X'
elif board[r][c] == 'E':
board[r][c] = 'O'
# Complex Problem Decomposition: Meeting Scheduler
class MeetingScheduler:
"""Demonstrates decomposition of scheduling problems"""
def find_meeting_time(self, calendars, meeting_duration):
"""
Find common free time for meeting
Decomposition:
1. Merge individual calendars
2. Find free slots
3. Check duration constraint
"""
# Step 1: Merge all busy times
all_busy = []
for calendar in calendars:
all_busy.extend(calendar)
# Step 2: Sort and merge overlapping intervals
all_busy.sort()
merged = []
for start, end in all_busy:
if not merged or merged[-1][1] < start:
merged.append([start, end])
else:
merged[-1][1] = max(merged[-1][1], end)
# Step 3: Find gaps of sufficient duration
free_slots = []
# Check before first meeting
if merged and merged[0][0] >= meeting_duration:
free_slots.append([0, merged[0][0]])
# Check between meetings
for i in range(1, len(merged)):
gap_start = merged[i-1][1]
gap_end = merged[i][0]
if gap_end - gap_start >= meeting_duration:
free_slots.append([gap_start, gap_end])
# Check after last meeting (assuming day ends at 1440 minutes)
if merged and 1440 - merged[-1][1] >= meeting_duration:
free_slots.append([merged[-1][1], 1440])
return free_slots
def schedule_interviews(self, candidates, interviewers, duration):
"""
Complex scheduling with multiple constraints
Decomposition:
1. Model as bipartite matching
2. Apply constraints
3. Optimize for preferences
"""
from collections import defaultdict
# Build availability graph
graph = defaultdict(list)
for candidate in candidates:
for interviewer in interviewers:
# Check if they have common available time
common_slots = self.find_common_slots(
candidate['available'],
interviewer['available'],
duration
)
if common_slots:
graph[candidate['id']].append({
'interviewer': interviewer['id'],
'slots': common_slots,
'preference': candidate['preferences'].get(interviewer['id'], 0)
})
# Use Hungarian algorithm or greedy matching
assignments = self.match_interviews(graph, candidates, interviewers)
return assignments
def find_common_slots(self, slots1, slots2, duration):
"""Find overlapping time slots of sufficient duration"""
common = []
i = j = 0
while i < len(slots1) and j < len(slots2):
start = max(slots1[i][0], slots2[j][0])
end = min(slots1[i][1], slots2[j][1])
if end - start >= duration:
common.append([start, end])
if slots1[i][1] < slots2[j][1]:
i += 1
else:
j += 1
return common
OCaml
(* Problem Decomposition Patterns *)
(* Pattern 1: Recursive decomposition with memoization *)
module WordBreak = struct
let word_break s word_dict =
let word_set = List.fold_left (fun set w ->
StringSet.add w set
) StringSet.empty word_dict in
let memo = Hashtbl.create 100 in
let rec can_break start =
if start = String.length s then true
else if Hashtbl.mem memo start then
Hashtbl.find memo start
else
let result = ref false in
for end_pos = start + 1 to String.length s do
let word = String.sub s start (end_pos - start) in
if StringSet.mem word word_set && can_break end_pos then
result := true
done;
Hashtbl.add memo start !result;
!result
in
can_break 0
(* Find all possible word breaks *)
let all_word_breaks s word_dict =
let word_set = List.fold_left (fun set w ->
StringSet.add w set
) StringSet.empty word_dict in
let memo = Hashtbl.create 100 in
let rec find_breaks start =
if Hashtbl.mem memo start then
Hashtbl.find memo start
else
let results =
if start = String.length s then
[[]]
else
let all_results = ref [] in
for end_pos = start + 1 to String.length s do
let word = String.sub s start (end_pos - start) in
if StringSet.mem word word_set then
let suffix_breaks = find_breaks end_pos in
List.iter (fun suffix ->
all_results := (word :: suffix) :: !all_results
) suffix_breaks
done;
!all_results
in
Hashtbl.add memo start results;
results
in
find_breaks 0
end
(* Pattern 2: Graph-based decomposition *)
module TaskScheduler = struct
type task = {
id: string;
duration: int;
dependencies: string list;
resources: string list;
}
type schedule = (string * int) list
let schedule_tasks tasks =
(* Step 1: Build dependency graph *)
let graph = Hashtbl.create (List.length tasks) in
let in_degree = Hashtbl.create (List.length tasks) in
List.iter (fun task ->
Hashtbl.add graph task.id [];
Hashtbl.add in_degree task.id 0
) tasks;
List.iter (fun task ->
List.iter (fun dep ->
let deps = Hashtbl.find graph dep in
Hashtbl.replace graph dep (task.id :: deps);
let degree = Hashtbl.find in_degree task.id in
Hashtbl.replace in_degree task.id (degree + 1)
) task.dependencies
) tasks;
(* Step 2: Topological sort *)
let queue = Queue.create () in
Hashtbl.iter (fun id degree ->
if degree = 0 then Queue.push id queue
) in_degree;
let order = ref [] in
while not (Queue.is_empty queue) do
let current = Queue.pop queue in
order := current :: !order;
List.iter (fun neighbor ->
let degree = Hashtbl.find in_degree neighbor - 1 in
Hashtbl.replace in_degree neighbor degree;
if degree = 0 then Queue.push neighbor queue
) (Hashtbl.find graph current)
done;
(* Step 3: Schedule with resource constraints *)
let schedule = Hashtbl.create (List.length tasks) in
let resource_available = Hashtbl.create 10 in
let task_map = List.fold_left (fun map task ->
StringMap.add task.id task map
) StringMap.empty tasks in
List.iter (fun task_id ->
let task = StringMap.find task_id task_map in
let start_time = ref 0 in
(* Check dependencies *)
List.iter (fun dep ->
let dep_task = StringMap.find dep task_map in
let dep_end = (Hashtbl.find schedule dep) + dep_task.duration in
start_time := max !start_time dep_end
) task.dependencies;
(* Check resources *)
List.iter (fun resource ->
let available =
try Hashtbl.find resource_available resource
with Not_found -> 0
in
start_time := max !start_time available
) task.resources;
(* Schedule task *)
Hashtbl.add schedule task_id !start_time;
(* Update resources *)
List.iter (fun resource ->
Hashtbl.replace resource_available resource
(!start_time + task.duration)
) task.resources
) (List.rev !order);
schedule
end
(* Pattern 3: Dynamic programming decomposition *)
module IntervalScheduling = struct
type interval = {
start_time: int;
end_time: int;
value: int;
}
(* Maximum value interval scheduling *)
let max_value_schedule intervals =
(* Sort by end time *)
let sorted = List.sort (fun a b ->
compare a.end_time b.end_time
) intervals in
let n = List.length sorted in
let arr = Array.of_list sorted in
let dp = Array.make n 0 in
(* Find latest non-overlapping interval *)
let find_latest_non_overlapping i =
let rec binary_search low high =
if low > high then -1
else
let mid = (low + high) / 2 in
if arr.(mid).end_time <= arr.(i).start_time then
if mid + 1 <= high &&
arr.(mid + 1).end_time <= arr.(i).start_time then
binary_search (mid + 1) high
else mid
else
binary_search low (mid - 1)
in
binary_search 0 (i - 1)
in
(* Fill DP table *)
dp.(0) <- arr.(0).value;
for i = 1 to n - 1 do
let include_current = arr.(i).value in
let latest = find_latest_non_overlapping i in
let include_value =
if latest >= 0 then
include_current + dp.(latest)
else
include_current
in
dp.(i) <- max include_value dp.(i - 1)
done;
dp.(n - 1)
end
Algorithm Design Patterns
Common Algorithm Design Patterns
Recognizing and applying common algorithm design patterns can dramatically simplify problem solving. These patterns provide templates for approaching various problem types.
🎨 Real-Life Analogy:
Like an artist who knows different painting techniques (watercolor, oil, acrylic), a programmer should know different algorithm patterns and when to apply each one.
🔧 Key Design Patterns:
- Two Pointers: Linear traversal with multiple positions
- Sliding Window: Maintain a range while moving through data
- Fast & Slow Pointers: Cycle detection and middle finding
- Merge Intervals: Combining overlapping ranges
- Top K Elements: Using heaps for selection
| Pattern | When to Use | Time Complexity | Example Problems |
|---|---|---|---|
| Two Pointers | Sorted arrays, pairs | O(n) | Two Sum, Container with Water |
| Sliding Window | Subarray/substring | O(n) | Max Sum Subarray, Longest Substring |
| Fast & Slow | Cycle detection | O(n) | Linked List Cycle, Happy Number |
| Merge Intervals | Overlapping ranges | O(n log n) | Meeting Rooms, Insert Interval |
| Top K Elements | Finding k best/worst | O(n log k) | K Closest Points, Top K Frequent |
Optimization Techniques
Advanced Optimization Strategies
Optimization involves improving algorithm performance through various techniques including space-time tradeoffs, preprocessing, caching, and algorithmic improvements.
🏎️ Real-Life Analogy:
Think of optimizing like tuning a race car: you can improve the engine (algorithm), reduce weight (space), add better tires (data structures), or find a shorter track (preprocessing).
⚡ Optimization Strategies:
- Preprocessing: Compute once, use many times
- Caching/Memoization: Store computed results
- Space-Time Tradeoff: Use more memory for speed
- Early Termination: Stop when answer is found
- Pruning: Skip unnecessary computations
TypeScript
// Optimization Example: Range Query Problems
// Problem: Answer multiple range sum queries
// Naive: O(n) per query
// Optimized: O(1) per query with O(n) preprocessing
class RangeQueryOptimizer {
// Technique 1: Prefix Sum for Range Sum
private prefixSum: number[];
constructor(nums: number[]) {
this.prefixSum = new Array(nums.length + 1).fill(0);
for (let i = 0; i < nums.length; i++) {
this.prefixSum[i + 1] = this.prefixSum[i] + nums[i];
}
}
sumRange(left: number, right: number): number {
return this.prefixSum[right + 1] - this.prefixSum[left];
}
}
// Technique 2: Sparse Table for Range Minimum
class SparseTable {
private table: number[][];
private logs: number[];
constructor(nums: number[]) {
const n = nums.length;
const k = Math.floor(Math.log2(n)) + 1;
// Precompute logarithms
this.logs = new Array(n + 1).fill(0);
for (let i = 2; i <= n; i++) {
this.logs[i] = this.logs[Math.floor(i / 2)] + 1;
}
// Build sparse table
this.table = Array(k).fill(null).map(() => Array(n).fill(0));
// Initialize for length 1
for (let i = 0; i < n; i++) {
this.table[0][i] = nums[i];
}
// Build for other lengths
for (let j = 1; j < k; j++) {
for (let i = 0; i + (1 << j) <= n; i++) {
this.table[j][i] = Math.min(
this.table[j - 1][i],
this.table[j - 1][i + (1 << (j - 1))]
);
}
}
}
rangeMin(left: number, right: number): number {
const len = right - left + 1;
const k = this.logs[len];
return Math.min(
this.table[k][left],
this.table[k][right - (1 << k) + 1]
);
}
}
// Technique 3: Binary Indexed Tree for Dynamic Updates
class OptimizedBIT {
private tree: number[];
private nums: number[];
constructor(nums: number[]) {
this.nums = [...nums];
this.tree = new Array(nums.length + 1).fill(0);
// Optimized construction O(n)
for (let i = 0; i < nums.length; i++) {
this.updateDelta(i, nums[i]);
}
}
private updateDelta(index: number, delta: number): void {
index++;
while (index < this.tree.length) {
this.tree[index] += delta;
index += index & (-index);
}
}
update(index: number, value: number): void {
const delta = value - this.nums[index];
this.nums[index] = value;
this.updateDelta(index, delta);
}
sumRange(left: number, right: number): number {
return this.prefixSum(right) - (left > 0 ? this.prefixSum(left - 1) : 0);
}
private prefixSum(index: number): number {
index++;
let sum = 0;
while (index > 0) {
sum += this.tree[index];
index -= index & (-index);
}
return sum;
}
}
// Technique 4: Optimization through Bit Manipulation
class BitOptimizations {
// Count set bits using Brian Kernighan's algorithm
countSetBits(n: number): number {
let count = 0;
while (n) {
n &= n - 1; // Clear rightmost set bit
count++;
}
return count;
}
// Find missing number in O(n) time, O(1) space
findMissingNumber(nums: number[]): number {
let xor = 0;
// XOR all array elements
for (const num of nums) {
xor ^= num;
}
// XOR with all numbers from 0 to n
for (let i = 0; i <= nums.length; i++) {
xor ^= i;
}
return xor;
}
// Check if number is power of 2
isPowerOfTwo(n: number): boolean {
return n > 0 && (n & (n - 1)) === 0;
}
}
// Technique 5: String Matching Optimization
class StringOptimizer {
// Rolling Hash for substring matching
findSubstringRollingHash(text: string, pattern: string): number[] {
const matches: number[] = [];
const base = 256;
const prime = 101;
const m = pattern.length;
const n = text.length;
if (m > n) return matches;
let patternHash = 0;
let textHash = 0;
let h = 1;
// Precompute h = base^(m-1) % prime
for (let i = 0; i < m - 1; i++) {
h = (h * base) % prime;
}
// Initial hashes
for (let i = 0; i < m; i++) {
patternHash = (base * patternHash + pattern.charCodeAt(i)) % prime;
textHash = (base * textHash + text.charCodeAt(i)) % prime;
}
// Slide and check
for (let i = 0; i <= n - m; i++) {
if (patternHash === textHash) {
// Verify to avoid hash collision
if (text.substring(i, i + m) === pattern) {
matches.push(i);
}
}
if (i < n - m) {
textHash = (base * (textHash - text.charCodeAt(i) * h) +
text.charCodeAt(i + m)) % prime;
if (textHash < 0) textHash += prime;
}
}
return matches;
}
}
Python
# Optimization Techniques Examples
import bisect
from collections import deque
import heapq
class OptimizationTechniques:
"""Collection of optimization techniques for common problems"""
# Technique 1: Binary Search Optimization
def search_in_rotated_array(self, nums, target):
"""Find target in rotated sorted array - O(log n)"""
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
# Determine which half is sorted
if nums[left] <= nums[mid]:
# Left half is sorted
if nums[left] <= target < nums[mid]:
right = mid - 1
else:
left = mid + 1
else:
# Right half is sorted
if nums[mid] < target <= nums[right]:
left = mid + 1
else:
right = mid - 1
return -1
# Technique 2: Monotonic Stack Optimization
def next_greater_elements(self, nums):
"""Find next greater element for each element - O(n)"""
n = len(nums)
result = [-1] * n
stack = [] # Monotonic decreasing stack
# Process array twice for circular array
for i in range(2 * n):
while stack and nums[stack[-1]] < nums[i % n]:
result[stack.pop()] = nums[i % n]
if i < n:
stack.append(i)
return result
# Technique 3: Sliding Window with Deque
def max_sliding_window(self, nums, k):
"""Maximum in sliding window - O(n)"""
if not nums:
return []
dq = deque() # Store indices
result = []
for i in range(len(nums)):
# Remove elements outside window
while dq and dq[0] <= i - k:
dq.popleft()
# Remove smaller elements
while dq and nums[dq[-1]] < nums[i]:
dq.pop()
dq.append(i)
# Add to result after first window
if i >= k - 1:
result.append(nums[dq[0]])
return result
# Technique 4: Bucket Sort Optimization
def top_k_frequent_bucket(self, nums, k):
"""Top K frequent elements using buckets - O(n)"""
from collections import Counter
count = Counter(nums)
buckets = [[] for _ in range(len(nums) + 1)]
# Group by frequency
for num, freq in count.items():
buckets[freq].append(num)
# Collect top k
result = []
for i in range(len(buckets) - 1, -1, -1):
result.extend(buckets[i])
if len(result) >= k:
return result[:k]
return result
# Technique 5: Matrix Optimization
def search_2d_matrix(self, matrix, target):
"""Search in row and column sorted matrix - O(m + n)"""
if not matrix or not matrix[0]:
return False
rows, cols = len(matrix), len(matrix[0])
row, col = 0, cols - 1 # Start from top-right
while row < rows and col >= 0:
if matrix[row][col] == target:
return True
elif matrix[row][col] > target:
col -= 1
else:
row += 1
return False
# Advanced Caching Techniques
class LRUCache:
"""Least Recently Used Cache with O(1) operations"""
class Node:
def __init__(self, key=0, value=0):
self.key = key
self.value = value
self.prev = None
self.next = None
def __init__(self, capacity):
self.capacity = capacity
self.cache = {} # key -> node
self.head = self.Node() # Dummy head
self.tail = self.Node() # Dummy tail
self.head.next = self.tail
self.tail.prev = self.head
def _add_to_head(self, node):
"""Add node right after head"""
node.prev = self.head
node.next = self.head.next
self.head.next.prev = node
self.head.next = node
def _remove_node(self, node):
"""Remove node from list"""
prev_node = node.prev
next_node = node.next
prev_node.next = next_node
next_node.prev = prev_node
def get(self, key):
if key not in self.cache:
return -1
# Move to head (mark as recently used)
node = self.cache[key]
self._remove_node(node)
self._add_to_head(node)
return node.value
def put(self, key, value):
if key in self.cache:
# Update existing
node = self.cache[key]
node.value = value
self._remove_node(node)
self._add_to_head(node)
else:
# Add new
node = self.Node(key, value)
self.cache[key] = node
self._add_to_head(node)
if len(self.cache) > self.capacity:
# Remove LRU
lru = self.tail.prev
self._remove_node(lru)
del self.cache[lru.key]
OCaml
(* Optimization Techniques in OCaml *)
(* Technique 1: Memoization for Dynamic Programming *)
module Memoize = struct
(* Generic memoization function *)
let memoize f =
let table = Hashtbl.create 100 in
let rec memoized x =
try Hashtbl.find table x
with Not_found ->
let result = f memoized x in
Hashtbl.add table x result;
result
in
memoized
(* Example: Fibonacci with memoization *)
let fib =
let rec fib_helper memo n =
if n <= 1 then n
else memo (n - 1) + memo (n - 2)
in
memoize fib_helper
(* Longest Common Subsequence with memoization *)
let lcs s1 s2 =
let n1 = String.length s1 in
let n2 = String.length s2 in
let memo = Hashtbl.create (n1 * n2) in
let rec lcs_helper i j =
if i = n1 || j = n2 then 0
else
try Hashtbl.find memo (i, j)
with Not_found ->
let result =
if s1.[i] = s2.[j] then
1 + lcs_helper (i + 1) (j + 1)
else
max (lcs_helper (i + 1) j) (lcs_helper i (j + 1))
in
Hashtbl.add memo (i, j) result;
result
in
lcs_helper 0 0
end
(* Technique 2: Lazy Evaluation for Optimization *)
module LazyOptimization = struct
(* Lazy list for infinite sequences *)
type 'a lazy_list = Nil | Cons of 'a * 'a lazy_list Lazy.t
let rec take n = function
| Nil -> []
| Cons (h, t) ->
if n <= 0 then []
else h :: take (n - 1) (Lazy.force t)
(* Prime numbers using Sieve of Eratosthenes *)
let primes =
let rec sieve = function
| Nil -> Nil
| Cons (p, xs) ->
let rec filter n = function
| Nil -> Nil
| Cons (x, xs) ->
if x mod n = 0 then
filter n (Lazy.force xs)
else
Cons (x, lazy (filter n (Lazy.force xs)))
in
Cons (p, lazy (sieve (filter p (Lazy.force xs))))
in
let rec integers_from n =
Cons (n, lazy (integers_from (n + 1)))
in
sieve (integers_from 2)
end
(* Technique 3: Bit Manipulation Optimizations *)
module BitOptimizations = struct
(* Count set bits *)
let count_bits n =
let rec count n acc =
if n = 0 then acc
else count (n land (n - 1)) (acc + 1)
in
count n 0
(* Find single number (all others appear twice) *)
let find_single nums =
List.fold_left (lxor) 0 nums
(* Power set generation using bits *)
let power_set arr =
let n = Array.length arr in
let total = 1 lsl n in
let result = ref [] in
for i = 0 to total - 1 do
let subset = ref [] in
for j = 0 to n - 1 do
if (i land (1 lsl j)) <> 0 then
subset := arr.(j) :: !subset
done;
result := !subset :: !result
done;
!result
end
(* Technique 4: Functional Data Structure Optimizations *)
module PersistentQueue = struct
(* Amortized O(1) persistent queue *)
type 'a queue = {
front: 'a list;
rear: 'a list;
}
let empty = { front = []; rear = [] }
let enqueue x q = { q with rear = x :: q.rear }
let dequeue = function
| { front = []; rear = [] } -> failwith "Empty queue"
| { front = h :: t; rear } -> (h, { front = t; rear })
| { front = []; rear } ->
let front = List.rev rear in
(List.hd front, { front = List.tl front; rear = [] })
end
(* Technique 5: Parallel Processing Optimization *)
module ParallelOptimization = struct
(* Parallel map using Domain module (OCaml 5.0+) *)
let parallel_map f lst =
let n_domains = Domain.recommended_domain_count () in
let chunk_size = (List.length lst + n_domains - 1) / n_domains in
let rec chunks lst =
match lst with
| [] -> []
| _ ->
let chunk, rest =
try List.split_at chunk_size lst
with _ -> lst, []
in
chunk :: chunks rest
in
let work_chunks = chunks lst in
let domains = List.map (fun chunk ->
Domain.spawn (fun () -> List.map f chunk)
) work_chunks in
List.fold_left (fun acc d ->
acc @ Domain.join d
) [] domains
end
Graded Problem Sets
Progressive Difficulty Training
Practice problems organized by difficulty to build skills systematically. Each problem includes solution approaches and complexity analysis.
🏋️ Real-Life Analogy:
Like training at the gym, you start with lighter weights and gradually increase. Each difficulty level builds the foundation for the next.
🟢 Easy Problems
Foundation problems that introduce core concepts and basic pattern recognition.
Problem 1: Valid Parentheses
Problem: Given a string containing '(', ')', '{', '}', '[', ']', determine if the input string is valid.
Approach: Use a stack to track opening brackets
Hint: Push opening brackets, pop and match closing brackets
Time: O(n), Space: O(n)
Key Insight: Stack naturally handles nested structures
Approach: Use a stack to track opening brackets
Hint: Push opening brackets, pop and match closing brackets
Time: O(n), Space: O(n)
Key Insight: Stack naturally handles nested structures
Problem 2: Two Sum
Problem: Find two numbers in array that add up to target.
Approach: Use hash map to store complements
Hint: For each number x, check if (target - x) exists
Time: O(n), Space: O(n)
Key Insight: Trade space for time with hash lookups
Approach: Use hash map to store complements
Hint: For each number x, check if (target - x) exists
Time: O(n), Space: O(n)
Key Insight: Trade space for time with hash lookups
Problem 3: Maximum Subarray
Problem: Find contiguous subarray with largest sum.
Approach: Kadane's algorithm - track current and global max
Hint: Reset current sum when it becomes negative
Time: O(n), Space: O(1)
Key Insight: Negative prefix never helps future sums
Approach: Kadane's algorithm - track current and global max
Hint: Reset current sum when it becomes negative
Time: O(n), Space: O(1)
Key Insight: Negative prefix never helps future sums
Problem 4: Binary Tree Inorder Traversal
Problem: Return inorder traversal of binary tree.
Approach: Recursive: left → root → right
Hint: Use stack for iterative solution
Time: O(n), Space: O(h) where h is height
Key Insight: Recursion naturally handles tree structure
Approach: Recursive: left → root → right
Hint: Use stack for iterative solution
Time: O(n), Space: O(h) where h is height
Key Insight: Recursion naturally handles tree structure
Problem 5: Merge Two Sorted Lists
Problem: Merge two sorted linked lists into one sorted list.
Approach: Two pointers, compare and advance smaller
Hint: Use dummy head to simplify edge cases
Time: O(m + n), Space: O(1)
Key Insight: Dummy nodes eliminate special cases
Approach: Two pointers, compare and advance smaller
Hint: Use dummy head to simplify edge cases
Time: O(m + n), Space: O(1)
Key Insight: Dummy nodes eliminate special cases
🟡 Medium Problems
Intermediate problems requiring pattern combination and deeper algorithmic thinking.
Problem 1: Longest Substring Without Repeating Characters
Problem: Find length of longest substring without repeating characters.
Approach: Sliding window with hash set
Hint: Expand window until duplicate, then shrink from left
Time: O(n), Space: O(min(m,n)) where m is charset size
Key Insight: Window technique for substring problems
Approach: Sliding window with hash set
Hint: Expand window until duplicate, then shrink from left
Time: O(n), Space: O(min(m,n)) where m is charset size
Key Insight: Window technique for substring problems
Problem 2: Group Anagrams
Problem: Group strings that are anagrams of each other.
Approach: Use sorted string or character count as key
Hint: Anagrams have same sorted characters
Time: O(n * k log k) where k is max string length
Key Insight: Canonical form for grouping
Approach: Use sorted string or character count as key
Hint: Anagrams have same sorted characters
Time: O(n * k log k) where k is max string length
Key Insight: Canonical form for grouping
Problem 3: 3Sum
Problem: Find all unique triplets that sum to zero.
Approach: Sort array, fix one element, two pointers for rest
Hint: Skip duplicates to avoid repeated triplets
Time: O(n²), Space: O(1)
Key Insight: Reduce to 2Sum with sorting
Approach: Sort array, fix one element, two pointers for rest
Hint: Skip duplicates to avoid repeated triplets
Time: O(n²), Space: O(1)
Key Insight: Reduce to 2Sum with sorting
Problem 4: Binary Tree Level Order Traversal
Problem: Return level-by-level traversal of binary tree.
Approach: BFS using queue, track level boundaries
Hint: Process all nodes at current level before next
Time: O(n), Space: O(w) where w is max width
Key Insight: BFS naturally processes by levels
Approach: BFS using queue, track level boundaries
Hint: Process all nodes at current level before next
Time: O(n), Space: O(w) where w is max width
Key Insight: BFS naturally processes by levels
Problem 5: Coin Change
Problem: Find minimum coins needed to make amount.
Approach: Dynamic programming - build up from smaller amounts
Hint: dp[i] = min(dp[i-coin] + 1) for all coins ≤ i
Time: O(amount × coins), Space: O(amount)
Key Insight: Optimal substructure - optimal solution contains optimal subsolutions
Approach: Dynamic programming - build up from smaller amounts
Hint: dp[i] = min(dp[i-coin] + 1) for all coins ≤ i
Time: O(amount × coins), Space: O(amount)
Key Insight: Optimal substructure - optimal solution contains optimal subsolutions
🔴 Hard Problems
Advanced problems requiring sophisticated algorithms and optimization techniques.
Problem 1: Median of Two Sorted Arrays
Problem: Find median of two sorted arrays in O(log(min(m,n))) time.
Approach: Binary search on smaller array to partition both
Hint: Ensure left partition ≤ right partition
Time: O(log(min(m,n))), Space: O(1)
Key Insight: Median divides array into equal halves
Approach: Binary search on smaller array to partition both
Hint: Ensure left partition ≤ right partition
Time: O(log(min(m,n))), Space: O(1)
Key Insight: Median divides array into equal halves
Problem 2: Trapping Rain Water
Problem: Calculate trapped rainwater given elevation map.
Approach: Two pointers from both ends, track max heights
Hint: Water level limited by minimum of left/right max
Time: O(n), Space: O(1)
Key Insight: Water trapped = min(left_max, right_max) - height
Approach: Two pointers from both ends, track max heights
Hint: Water level limited by minimum of left/right max
Time: O(n), Space: O(1)
Key Insight: Water trapped = min(left_max, right_max) - height
Problem 3: Word Ladder
Problem: Find shortest transformation sequence from start to end word.
Approach: BFS treating words as graph nodes
Hint: Each word differs by exactly one character
Time: O(M² × N) where M is word length, N is word count
Key Insight: Shortest path in unweighted graph = BFS
Approach: BFS treating words as graph nodes
Hint: Each word differs by exactly one character
Time: O(M² × N) where M is word length, N is word count
Key Insight: Shortest path in unweighted graph = BFS
Common Interview Questions
Topic-Based Question Categories
Frequently asked interview questions organized by algorithmic topics and data structures.
📚 Real-Life Analogy:
Like studying for an exam, knowing the most common question types helps you prepare effectively and recognize patterns during interviews.
| Topic | Common Questions | Key Techniques | Difficulty Range |
|---|---|---|---|
| Arrays & Strings | Two Sum, Longest Palindrome, Rotate Array, Product Except Self | Two Pointers, Sliding Window, Hash Maps | Easy - Medium |
| Linked Lists | Reverse List, Detect Cycle, Merge Lists, Remove Nth Node | Fast/Slow Pointers, Dummy Nodes | Easy - Medium |
| Trees & Graphs | Tree Traversals, Validate BST, Course Schedule, Clone Graph | DFS, BFS, Recursion, Topological Sort | Medium - Hard |
| Dynamic Programming | Climbing Stairs, House Robber, Edit Distance, Knapsack | Memoization, Tabulation, State Machines | Medium - Hard |
| System Design | Design Cache, Rate Limiter, Chat System, Search Engine | Scalability, Load Balancing, Databases | Hard |
Optimization Challenges
Before/After Optimization Examples
Real optimization scenarios showing how to improve algorithm performance through better techniques and data structures.
🔧 Real-Life Analogy:
Like tuning a car engine for better performance, algorithm optimization involves identifying bottlenecks and applying targeted improvements.
Challenge 1: Range Sum Queries
❌ Before: Naive Approach
Problem: Answer Q range sum queries on array of size N
Approach: For each query, iterate through range
Time: O(Q × N), Space: O(1)
Issue: Recalculating same sums repeatedly
Approach: For each query, iterate through range
Time: O(Q × N), Space: O(1)
Issue: Recalculating same sums repeatedly
✅ After: Prefix Sum Optimization
Approach: Precompute prefix sums, answer queries in O(1)
Time: O(N + Q), Space: O(N)
Improvement: 1000x faster for large Q
Trade-off: Extra space for dramatic time savings
Time: O(N + Q), Space: O(N)
Improvement: 1000x faster for large Q
Trade-off: Extra space for dramatic time savings
Challenge 2: Finding Duplicates
❌ Before: Nested Loops
Problem: Find if array contains duplicates
Approach: Compare every pair of elements
Time: O(N²), Space: O(1)
Issue: Quadratic time complexity
Approach: Compare every pair of elements
Time: O(N²), Space: O(1)
Issue: Quadratic time complexity
✅ After: Hash Set Optimization
Approach: Use hash set to track seen elements
Time: O(N), Space: O(N)
Improvement: Linear vs quadratic time
Trade-off: Space for time - classic optimization pattern
Time: O(N), Space: O(N)
Improvement: Linear vs quadratic time
Trade-off: Space for time - classic optimization pattern
Challenge 3: Top K Elements
❌ Before: Sort Everything
Problem: Find K largest elements in array
Approach: Sort entire array, take last K elements
Time: O(N log N), Space: O(1)
Issue: Sorting more than necessary
Approach: Sort entire array, take last K elements
Time: O(N log N), Space: O(1)
Issue: Sorting more than necessary
✅ After: Min-Heap Optimization
Approach: Maintain min-heap of size K
Time: O(N log K), Space: O(K)
Improvement: Better when K << N
Alternative: QuickSelect for O(N) average case
Time: O(N log K), Space: O(K)
Improvement: Better when K << N
Alternative: QuickSelect for O(N) average case
Challenge 4: String Matching
❌ Before: Brute Force
Problem: Find pattern in text
Approach: Check pattern at every position
Time: O(N × M), Space: O(1)
Issue: Redundant character comparisons
Approach: Check pattern at every position
Time: O(N × M), Space: O(1)
Issue: Redundant character comparisons
✅ After: KMP Algorithm
Approach: Use failure function to skip redundant checks
Time: O(N + M), Space: O(M)
Improvement: Linear time complexity
Key Insight: Preprocess pattern to avoid backtracking
Time: O(N + M), Space: O(M)
Improvement: Linear time complexity
Key Insight: Preprocess pattern to avoid backtracking
Interview Problem Categories
Common Interview Problem Types
Understanding common interview problem categories helps you quickly identify the appropriate approach and data structures to use.
🗂️ Real-Life Analogy:
Like a doctor who recognizes symptoms to diagnose illnesses, recognizing problem patterns helps you prescribe the right algorithmic solution.
| Category | Key Indicators | Common Techniques | Example Problems |
|---|---|---|---|
| Array/String | Sequential data, subarray/substring | Two pointers, Sliding window, Prefix sum | Two Sum, Longest Palindrome, Maximum Subarray |
| Tree/Graph | Hierarchical/connected data | DFS, BFS, Recursion, Backtracking | Valid BST, Shortest Path, Connected Components |
| Dynamic Programming | Optimal substructure, overlapping subproblems | Memoization, Tabulation, State transition | Knapsack, Edit Distance, House Robber |
| Greedy | Local optimal leads to global optimal | Sorting, Priority queue, Proof by exchange | Activity Selection, Huffman Coding, Dijkstra's |
| Backtracking | Explore all possibilities, constraints | Recursion, Pruning, State space tree | N-Queens, Sudoku Solver, Permutations |
💡 Interview Tips:
- Always clarify problem constraints and edge cases
- Start with a brute force solution, then optimize
- Think out loud and explain your approach
- Test your solution with examples
- Analyze time and space complexity