Comparing Search and Traversal Patterns
1. Comparing Backtracking vs DP
| Aspect | Backtracking | Dynamic Programming |
|---|---|---|
| Approach | Explores all possible solutions by building candidates incrementally and abandoning paths that don't lead to a solution. | Breaks problem into overlapping subproblems and stores results of subproblems to avoid redundant computations. |
| State Management | Uses recursion or stack to track current state and backtrack when needed. | Uses memoization or tabulation to store intermediate results. |
| Subproblem Overlap | Does not typically reuse solutions to subproblems. | Relies heavily on overlapping subproblems for efficiency. |
| Time Complexity | Can be exponential in worst case (e.g., N! for permutations). | Often reduces time complexity from exponential to polynomial (e.g., O(n^2) for LCS). |
| Space Complexity | O(d) where d is depth of recursion; can be high due to call stack. | O(n) for memoization or O(n^2) for 2D DP tables. |
| Use Cases | Problems like N-Queens, Sudoku solvers, generating permutations/combinations. | Optimization problems like Longest Common Subsequence, Coin Change, Knapsack. |
| Solution Type | Find one or all valid solutions. | Find optimal solution (min/max value). |
Example: Solving N-Queens with Backtracking
function solveNQueens(n) {
const board = Array(n).fill().map(() => Array(n).fill(0));
const result = [];
function isSafe(row, col) {
for (let i = 0; i < row; i++) {
if (board[i][col] === 1) return false;
if (col - (row - i) >= 0 && board[i][col - (row - i)] === 1) return false;
if (col + (row - i) < n && board[i][col + (row - i)] === 1) return false;
}
return true;
}
function backtrack(row) {
if (row === n) {
result.push(board.map(row => [...row]));
return;
}
for (let col = 0; col < n; col++) {
if (isSafe(row, col)) {
board[row][col] = 1;
backtrack(row + 1);
board[row][col] = 0;
}
}
}
backtrack(0);
return result;
}
Example: Coin Change with DP
function coinChange(coins, amount) {
const dp = Array(amount + 1).fill(Infinity);
dp[0] = 0;
for (let i = 1; i <= amount; i++) {
for (const coin of coins) {
if (coin <= i && dp[i - coin] !== Infinity) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}
return dp[amount] === Infinity ? -1 : dp[amount];
}
// Example usage:
console.log(coinChange([1, 3, 4], 6)); // Output: 2 (3 + 3)
Key Differences Summary
- Backtracking: Recursive exploration with pruning; good for exhaustive search problems.
- Dynamic Programming: Memoization or tabulation to avoid recomputation; ideal for optimization problems with overlapping subproblems.
2. Comparing Backtracking vs Greedy
| Pattern | Description | Approach | Use Cases |
|---|---|---|---|
| Backtracking | Explores all possible solutions by building candidates incrementally and abandoning paths that don't lead to a solution. | Recursive, exhaustive search with pruning | Constraint satisfaction problems, puzzles (e.g., Sudoku), combinatorial optimization |
| Greedy | Makes locally optimal choices at each step, hoping to find a global optimum. | Iterative, local optimization | Activity selection, Huffman coding, minimum spanning tree (Kruskal's), Dijkstra's algorithm |
Example: Solving Subset Sum with Backtracking
function subsetSumBacktrack(nums, target) {
const result = [];
function backtrack(start, currentSum, path) {
if (currentSum === target) {
result.push([...path]);
return;
}
for (let i = start; i < nums.length; i++) {
if (currentSum + nums[i] <= target) {
path.push(nums[i]);
backtrack(i + 1, currentSum + nums[i], path);
path.pop();
}
}
}
backtrack(0, 0, []);
return result;
}
// Example usage:
const nums = [1, 2, 3, 4];
const target = 5;
console.log(subsetSumBacktrack(nums, target)); // [[1,4], [2,3]]
Example: Activity Selection with Greedy
function activitySelection(activities) {
// Sort by finish time
activities.sort((a, b) => a.finish - b.finish);
const selected = [activities[0]];
let lastFinish = activities[0].finish;
for (let i = 1; i < activities.length; i++) {
if (activities[i].start >= lastFinish) {
selected.push(activities[i]);
lastFinish = activities[i].finish;
}
}
return selected;
}
// Example usage:
const activities = [
{ name: 'A', start: 1, finish: 4 },
{ name: 'B', start: 3, finish: 5 },
{ name: 'C', start: 0, finish: 6 },
{ name: 'D', start: 5, finish: 7 },
{ name: 'E', start: 3, finish: 9 },
{ name: 'F', start: 5, finish: 9 },
{ name: 'G', start: 6, finish: 10 },
{ name: 'H', start: 8, finish: 11 },
{ name: 'I', start: 8, finish: 12 },
{ name: 'J', start: 2, finish: 14 },
{ name: 'K', start: 12, finish: 16 }
];
console.log(activitySelection(activities));
// Selected: [A, B, D, G, H, K]
Key Differences
- Backtracking: Systematically explores all possibilities, often with pruning to avoid redundant work.
- Greedy: Makes the best immediate choice at each step, never reconsidering past decisions.
- Optimality: Backtracking guarantees finding a solution if one exists; greedy does not always find the optimal solution.
3. Comparing DP vs Greedy
| Aspect | Dynamic Programming | Greedy Algorithm |
|---|---|---|
| Approach | Breaks problem into overlapping subproblems and solves them optimally | Makes locally optimal choices at each step |
| Optimality | Guarantees global optimum for problems with optimal substructure | Does not always guarantee global optimum |
| Time Complexity | Often O(n^2) or higher due to memoization/storage | Usually O(n log n) or better |
| Space Complexity | Higher due to memoization tables or recursion stack | Lower, typically O(1) for simple cases |
| Problem Types | Optimization problems with overlapping subproblems (e.g., Knapsack, LCS) | Problems where local choices lead to global solution (e.g., Activity Selection, Huffman Coding) |
| Decision Making | Considers all possibilities before making a decision | Makes decisions based on current state without backtracking |
Example: Coin Change Problem
function coinChangeDP(coins, amount) {
const dp = new Array(amount + 1).fill(Infinity);
dp[0] = 0;
for (let i = 1; i <= amount; i++) {
for (const coin of coins) {
if (coin <= i) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}
return dp[amount] === Infinity ? -1 : dp[amount];
}
function coinChangeGreedy(coins, amount) {
coins.sort((a, b) => b - a);
let count = 0;
for (const coin of coins) {
while (amount >= coin) {
amount -= coin;
count++;
}
}
return amount === 0 ? count : -1;
}
// Example usage:
const coins = [1, 3, 4];
const amount = 6;
console.log(coinChangeDP(coins, amount)); // Output: 2 (3 + 3)
console.log(coinChangeGreedy(coins, amount)); // Output: 3 (4 + 1 + 1) - Not optimal
When to Use Which?
- Use DP when the problem has overlapping subproblems and optimal substructure.
- Use Greedy when the problem exhibits the greedy choice property and local optimization leads to global optimum.
4. Comparing Memoization vs Tabulation
| Aspect | Memoization | Tabulation |
|---|---|---|
| Approach | Top-down with caching | Bottom-up with table filling |
| Storage | Hash map or array for storing results | Table (array) to store subproblem solutions |
| Recursion | May use recursion | No recursion |
| Subproblem Dependencies | Only computed subproblems are stored | All subproblems computed in order |
| Space Complexity | O(n) for storage + O(n) for recursion stack | O(n) for storage only |
| Time Complexity | O(n) average case with memoization | O(n) for filling the table |
| Overhead | Higher due to recursive calls | Lower, iterative approach |
| Use Case | When subproblems overlap and recursion is natural | When all subproblems must be solved in order |
Example: Fibonacci Comparison
// Memoization approach
function fibMemo(n, memo = {}) {
if (n in memo) return memo[n];
if (n <= 1) return n;
memo[n] = fibMemo(n - 1, memo) + fibMemo(n - 2, memo);
return memo[n];
}
// Tabulation approach
function fibTab(n) {
if (n <= 1) return n;
const dp = new Array(n + 1);
dp[0] = 0;
dp[1] = 1;
for (let i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
When to Use Which?
- Memoization is preferred when the recursive structure matches the problem naturally and subproblems are not all needed.
- Tabulation is preferred when all subproblems must be computed and performance of iterative solutions is desired.
5. Comparing DFS vs BFS
| Aspect | DFS (Depth-First Search) | BFS (Breadth-First Search) |
|---|---|---|
| Traversal Order | Explores as far as possible down one path before backtracking | Explores all neighbors at the current depth before moving to the next level |
| Data Structure Used | Stack (or recursion) | Queue |
| Memory Usage | Less memory for wide graphs, more for deep ones | Higher memory usage due to storing all nodes at current level |
| Time Complexity | O(V + E) where V = vertices, E = edges | O(V + E) |
| Space Complexity | O(V) in worst case (recursion stack) | O(V) for storing nodes in queue |
| Use Cases | Path finding, topological sorting, cycle detection | Shortest path in unweighted graphs, level-order traversal |
Example: DFS Implementation
function dfs(graph, start) {
const visited = new Set();
const stack = [start];
while (stack.length > 0) {
const node = stack.pop();
if (!visited.has(node)) {
visited.add(node);
console.log(node);
for (const neighbor of graph[node] || []) {
if (!visited.has(neighbor)) {
stack.push(neighbor);
}
}
}
}
}
Example: BFS Implementation
function bfs(graph, start) {
const visited = new Set();
const queue = [start];
while (queue.length > 0) {
const node = queue.shift();
if (!visited.has(node)) {
visited.add(node);
console.log(node);
for (const neighbor of graph[node] || []) {
if (!visited.has(neighbor)) {
queue.push(neighbor);
}
}
}
}
}
Key Differences Summary
- DFS is more memory-efficient for deep graphs but can get stuck in long paths.
- BFS guarantees shortest path in unweighted graphs but uses more memory.
- Both have same time complexity O(V + E).
6. Comparing Tree DFS Variants
| Variant | Description | Use Case | Code Example |
|---|---|---|---|
| Preorder DFS | Visit node, then recursively process left subtree, then right subtree. | Tree serialization, copying tree structure. | |
| Inorder DFS | Process left subtree, visit node, process right subtree. | Binary search tree traversal (sorted order). | |
| Postorder DFS | Process left subtree, right subtree, then visit node. | Tree deletion, calculating sizes or depths. | |
Key Differences Summary
- Preorder: Root first, good for copying or serializing.
- Inorder: Sorted order in BSTs, ideal for ordered processing.
- Postorder: Leaves first, useful for tree cleanup or size calculation.
7. Comparing Graph DFS vs BFS
| Aspect | Depth-First Search (DFS) | Breadth-First Search (BFS) |
|---|---|---|
| Traversal Order | Explores as far as possible along each branch before backtracking | Explores all neighbors at the present depth prior to moving to the next depth level |
| Data Structure Used | Stack (or recursion) | Queue |
| Memory Complexity | O(h) where h is the maximum depth of the tree | O(w) where w is the maximum width of the tree |
| Time Complexity | O(V + E) where V is vertices and E is edges | O(V + E) where V is vertices and E is edges |
| Use Cases | Topological sorting, detecting cycles in a graph, solving puzzles like mazes | Shortest path in unweighted graphs, level-order traversal, finding connected components |
| Path Finding | Does not guarantee shortest path in unweighted graphs | Guarantees shortest path in unweighted graphs |
Example: DFS Implementation in JavaScript
function dfs(graph, start) {
const visited = new Set();
const stack = [start];
while (stack.length > 0) {
const node = stack.pop();
if (!visited.has(node)) {
visited.add(node);
console.log(node);
for (const neighbor of graph[node] || []) {
if (!visited.has(neighbor)) {
stack.push(neighbor);
}
}
}
}
}
Example: BFS Implementation in JavaScript
function bfs(graph, start) {
const visited = new Set();
const queue = [start];
while (queue.length > 0) {
const node = queue.shift();
if (!visited.has(node)) {
visited.add(node);
console.log(node);
for (const neighbor of graph[node] || []) {
if (!visited.has(neighbor)) {
queue.push(neighbor);
}
}
}
}
}
Key Differences Summary
- DFS is more memory-efficient for deep graphs but can get stuck in long paths.
- BFS guarantees the shortest path in unweighted graphs but uses more memory.
- Both have same time complexity O(V + E).
8. Comparing Topological Sort Methods
| Method | Description | Time Complexity | Space Complexity | Use Case |
|---|---|---|---|---|
| Kahn's Algorithm | Uses in-degree tracking and BFS to process nodes with zero in-degrees. | O(V + E) |
O(V) |
When you need to detect cycles and process nodes in topological order. |
| DFS-based | Uses DFS to detect cycles and build the topological order via post-order traversal. | O(V + E) |
O(V) |
When you want to detect cycles and build a topological order using recursion. |
Example: Kahn's Algorithm Implementation
function topologicalSortKahn(graph) {
const inDegree = new Array(graph.length).fill(0);
for (let i = 0; i < graph.length; i++) {
for (let neighbor of graph[i]) {
inDegree[neighbor]++;
}
}
const queue = [];
for (let i = 0; i < inDegree.length; i++) {
if (inDegree[i] === 0) queue.push(i);
}
const result = [];
while (queue.length > 0) {
const node = queue.shift();
result.push(node);
for (let neighbor of graph[node]) {
inDegree[neighbor]--;
if (inDegree[neighbor] === 0) queue.push(neighbor);
}
}
return result.length === graph.length ? result : []; // Return empty if cycle detected
}
Example: DFS-based Implementation
function topologicalSortDFS(graph) {
const visited = new Array(graph.length).fill(false);
const recStack = new Array(graph.length).fill(false);
const stack = [];
function dfs(node) {
visited[node] = true;
recStack[node] = true;
for (let neighbor of graph[node]) {
if (!visited[neighbor]) {
if (dfs(neighbor)) return true;
} else if (recStack[neighbor]) {
return true; // Cycle detected
}
}
recStack[node] = false;
stack.push(node);
return false;
}
for (let i = 0; i < graph.length; i++) {
if (!visited[i]) {
if (dfs(i)) return []; // Return empty on cycle
}
}
return stack.reverse();
}
Key Differences
- Kahn's Algorithm: Iterative, uses queue, easier to understand.
- DFS-based: Recursive, uses stack for post-order, better for cycle detection.
9. Selecting Search Strategy
| Strategy | Description | Use Case | Complexity |
|---|---|---|---|
| Linear Search | Sequentially checks each element until the target is found. | Small datasets or unsorted data where no preprocessing is allowed. | O(n) |
| Binary Search | Divides the sorted array in half repeatedly to find the target. | Large sorted datasets where preprocessing is acceptable. | O(log n) |
| Interpolation Search | Estimates the position of the target based on value distribution. | Uniformly distributed sorted data with known range. | O(log log n) |
| Jump Search | Jumps ahead by fixed steps to find the range, then linearly searches. | Large sorted arrays where binary search is too complex or costly. | O(√n) |
| Exponential Search | Starts with index 1, doubles until the target is found or exceeded. | Unbounded or very large sorted arrays where the size is unknown. | O(log n) |
Example: Choosing Binary Search
const arr = [1, 3, 5, 7, 9, 11, 13];
const target = 7;
function binarySearch(arr, target) {
let left = 0;
let right = arr.length - 1;
while (left <= right) {
let mid = Math.floor((left + right) / 2);
if (arr[mid] === target) return mid;
if (arr[mid] < target) left = mid + 1;
else right = mid - 1;
}
return -1;
}
console.log(binarySearch(arr, target)); // Output: 3
Note: Binary search is optimal for sorted arrays, but requires the array to be sorted. Interpolation search can be faster than binary search for uniformly distributed data.