Evaluating Pattern Selection Criteria
1. Evaluating Time-Space Tradeoffs
| Pattern | Time Complexity | Space Complexity | Tradeoff Notes |
|---|---|---|---|
| Brute Force | O(n^k) |
O(1) |
Simple but inefficient for large inputs. Good for small datasets. |
| Divide & Conquer | O(n log n) |
O(log n) |
Reduces problem size recursively. Uses stack space for recursion. |
| Dynamic Programming | O(n^2) |
O(n) |
Optimizes overlapping subproblems. Tradeoff: extra space for memoization. |
| Greedy Algorithm | O(n log n) |
O(1) |
Efficient and simple, but not always optimal. No extra space needed. |
| Backtracking | O(k^n) |
O(n) |
Explores all possibilities. High time cost, moderate space usage. |
Example: Time-Space Tradeoff Analysis
function analyzeTradeoffs() {
const patterns = [
{ name: "Brute Force", time: "O(n^k)", space: "O(1)" },
{ name: "Divide & Conquer", time: "O(n log n)", space: "O(log n)" },
{ name: "Dynamic Programming", time: "O(n^2)", space: "O(n)" },
{ name: "Greedy Algorithm", time: "O(n log n)", space: "O(1)" },
{ name: "Backtracking", time: "O(k^n)", space: "O(n)" }
];
patterns.forEach(pattern => {
console.log(`${pattern.name}: ${pattern.time} / ${pattern.space}`);
});
}
Key Considerations
- Time Complexity determines how runtime scales with input size.
- Space Complexity reflects memory usage during execution.
- Choose Dynamic Programming for optimal substructure problems.
- Use Greedy when local choices lead to global optimum.
- Brute Force is acceptable for small datasets or prototyping.
2. Determining When to Combine Patterns
| Pattern Combination Strategy | Description | Use Case | Notes |
|---|---|---|---|
| Sequential Composition | Applying patterns in sequence where output of one feeds into another | Processing pipelines, data transformation workflows | Each pattern must be compatible with the next's output format |
| Parallel Composition | Applying multiple patterns simultaneously to the same input | Feature extraction, ensemble methods in ML | Requires synchronization or aggregation logic to merge results |
| Hybrid Composition | Mixing different pattern types within a single solution | Complex systems with multiple concerns (e.g., caching + retry logic) | Must ensure coherence between pattern behaviors |
Example: Sequential Composition in Data Processing
const processData = (data) => {
const validated = validate(data);
const transformed = transform(validated);
const normalized = normalize(transformed);
return normalized;
};
Comparison: Pattern Combination Approaches
- Sequential: Linear flow, easy to debug but less flexible
- Parallel: High throughput, harder to manage dependencies
- Hybrid: Flexible and powerful but can increase complexity
Guidelines for Combining Patterns
- Ensure each pattern has a well-defined role and interface
- Validate that combining patterns does not introduce performance bottlenecks
- Consider the trade-offs between modularity and composition complexity
- Use design patterns like Decorator or Adapter to facilitate combination
3. Selecting Based on Input Size
| Pattern | Best Input Size Range | Description | Notes |
|---|---|---|---|
| Divide and Conquer | Large |
Splits problem into smaller subproblems | Efficient for large datasets with recursive structure |
| Dynamic Programming | Medium to Large |
Solves problems by combining solutions of subproblems | Optimal for overlapping subproblems; memoization helps |
| Greedy Algorithms | Small to Medium |
Makes locally optimal choices at each step | Effective when global optimum can be achieved through local choices |
| Backtracking | Small to Medium |
Explores all possible solutions systematically | Useful for constraint satisfaction and combinatorial problems |
| Brute Force | Very Small |
Enumerates all possibilities to find solution | Simple but inefficient for large inputs; useful for verification |
Example: Choosing Pattern Based on Input Size
const inputSize = 10000;
let pattern;
if (inputSize > 10000) {
pattern = "Divide and Conquer";
} else if (inputSize > 1000) {
pattern = "Dynamic Programming";
} else if (inputSize > 100) {
pattern = "Greedy Algorithm";
} else {
pattern = "Brute Force";
}
console.log("Recommended pattern:", pattern); // Output: Divide and Conquer
Note: The choice of pattern should also consider time complexity, space constraints, and problem structure in addition to input size.
4. Selecting Based on Time Limits
| Pattern | Description | Time Complexity | Use Case |
|---|---|---|---|
| Brute Force | Checks all possible solutions | O(2^n) or O(n!) |
Small input sizes, exhaustive search required |
| Divide and Conquer | Splits problem into smaller subproblems | O(n log n) |
Sorting, searching, optimization problems |
| Dynamic Programming | Breaks problem into overlapping subproblems | O(n^2) or O(n^3) |
Optimization problems with overlapping subproblems |
| Greedy Algorithms | Makes locally optimal choices at each step | O(n log n) |
Activity selection, minimum spanning tree |
| Backtracking | Explores all possibilities and backtracks if invalid | O(k^n) where k is branching factor |
Constraint satisfaction, puzzles |
Example: Time Complexity Comparison
// Brute Force - O(2^n)
function bruteForceSubsetSum(arr, target) {
const result = [];
const n = arr.length;
for (let i = 0; i < (1 << n); i++) {
let sum = 0;
for (let j = 0; j < n; j++) {
if ((i & (1 << j)) !== 0) sum += arr[j];
}
if (sum === target) result.push(i);
}
return result;
}
// Divide and Conquer - O(n log n)
function mergeSort(arr) {
if (arr.length <= 1) return arr;
const mid = Math.floor(arr.length / 2);
const left = mergeSort(arr.slice(0, mid));
const right = mergeSort(arr.slice(mid));
return merge(left, right);
}
function merge(left, right) {
const result = [];
let i = 0, j = 0;
while (i < left.length && j < right.length) {
if (left[i] <= right[j]) result.push(left[i++]);
else result.push(right[j++]);
}
return result.concat(left.slice(i)).concat(right.slice(j));
}
// Dynamic Programming - O(n^2)
function longestCommonSubsequence(str1, str2) {
const dp = Array(str1.length + 1).fill().map(() => Array(str2.length + 1).fill(0));
for (let i = 1; i <= str1.length; i++) {
for (let j = 1; j <= str2.length; j++) {
if (str1[i - 1] === str2[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
return dp[str1.length][str2.length];
}
Time Complexity Guidelines
- Small Inputs (< 100 elements): Brute Force acceptable
- Medium Inputs (100-1000 elements): Divide and Conquer preferred
- Large Inputs (> 1000 elements): Dynamic Programming or Greedy
- Optimization Problems: Use DP when subproblems overlap
- Constraint Satisfaction: Backtracking for exhaustive search
5. Selecting Based on Space Limits
| Pattern | Space Complexity | Use Case | Notes |
|---|---|---|---|
| Brute Force | O(1) |
Small input size, no constraints | Simple but inefficient for large datasets |
| Dynamic Programming | O(n) to O(n^2) |
Overlapping subproblems | Optimal for problems with memoization |
| Divide and Conquer | O(log n) for divide steps |
Recursive problem solving | Memory usage depends on recursion depth |
| Greedy Algorithm | O(1) |
Local optimization leads to global | Efficient but not always optimal |
| Backtracking | O(n!) worst case |
Exploring all possibilities | High space due to recursion stack |
Example: Space Complexity Comparison
// Brute Force - O(1) space
function bruteForce(arr) {
let max = arr[0];
for (let i = 1; i < arr.length; i++) {
if (arr[i] > max) max = arr[i];
}
return max;
}
// Dynamic Programming - O(n) space
function dpMax(arr) {
const dp = new Array(arr.length);
dp[0] = arr[0];
for (let i = 1; i < arr.length; i++) {
dp[i] = Math.max(arr[i], dp[i-1] + arr[i]);
}
return Math.max(...dp);
}
Note: Space complexity is critical in memory-constrained environments like embedded systems or mobile apps.
Warning: Backtracking algorithms can lead to exponential space usage in worst-case scenarios.
6. Selecting Simpler Pattern When Sufficient
Example: Choosing Linear Search Over Binary Search
// Linear search for small dataset
const data = [5, 2, 8, 1, 9];
function linearSearch(arr, target) {
for (let i = 0; i < arr.length; i++) {
if (arr[i] === target) return i;
}
return -1;
}
// Binary search for large sorted dataset
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;
}
| Pattern | Use Case | Advantages | Disadvantages |
|---|---|---|---|
| Linear Search | Small datasets (n < 100) | Simple, no sorting needed | O(n) time complexity |
| Binary Search | Large sorted datasets (n > 1000) | O(log n) time complexity | Requires sorted data, more complex |
Decision Criteria
- Dataset size: For small datasets, linear search is simpler and faster due to low overhead.
- Data structure: Binary search requires sorted data, which may not be available.
- Access patterns: If data is frequently updated, maintaining sorted order may be costly.
7. Evaluating Pattern Composability
| Pattern | Composability Score | Description | Notes |
|---|---|---|---|
| Strategy | NEW 9/10 | Defines a family of algorithms, encapsulates each one, and makes them interchangeable | Highly composable with other patterns like Factory or Template Method |
| Observer | 8/10 | Defines a one-to-many dependency between objects so that when one object changes state, all dependents are notified | Can be composed with Mediator for complex event systems |
| Decorator | 7/10 | Attaches new behaviors or responsibilities to objects dynamically | Works well with Factory for dynamic object creation and decoration |
| Command | 6/10 | Encapsulates a request as an object, thereby allowing for parameterization of clients with queues or logs | Can be composed with Memento to support undo/redo functionality |
| State | 5/10 | Allows an object to alter its behavior when its internal state changes | Often composed with Strategy for state transitions |
Example: Strategy + Observer Composability
class SortStrategy {
sort(data) { throw new Error("Not implemented"); }
}
class BubbleSort extends SortStrategy {
sort(data) { return data.sort(); }
}
class MergeSort extends SortStrategy {
sort(data) { return this.mergeSort(data); }
}
class DataObserver {
constructor() {
this.observers = [];
}
notify(data) {
this.observers.forEach(observer => observer.update(data));
}
subscribe(observer) {
this.observers.push(observer);
}
}
class SortableData extends DataObserver {
constructor(strategy = new BubbleSort()) {
super();
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
processData(data) {
const sorted = this.strategy.sort(data);
this.notify(sorted);
return sorted;
}
}
// Usage
const data = [3, 1, 4, 1, 5];
const observer = {
update: (sorted) => console.log("Sorted data:", sorted)
};
const sortable = new SortableData(new MergeSort());
sortable.subscribe(observer);
sortable.processData(data);
Note: Composability is a key factor in selecting design patterns. Patterns that can be combined effectively often lead to more flexible and maintainable systems.
8. Adapting Patterns to Constraints
| Constraint Type | Description | Pattern Adaptation Strategy | Example |
|---|---|---|---|
| Time Complexity | Restriction on execution time | Choose patterns with lower O(n) complexity | for (let i = 0; i < n; i++) { ... } instead of nested loops |
| Space Complexity | Restriction on memory usage | Optimize for in-place operations or reuse data structures | let sum = 0; for (let i = 0; i < arr.length; i++) sum += arr[i]; |
| Input Size | Limits on data volume | Use streaming or chunking techniques | processStream(data, (chunk) => { ... }); |
| Data Access | Restrictions on data retrieval methods | Preprocess or cache data to minimize access patterns | const cached = new Map(); |
| Concurrency | Restrictions on parallel execution | Use thread-safe or lock-free patterns | const queue = new ConcurrentQueue(); |
Example: Adapting to Time Complexity Constraint
// Inefficient O(n^2) approach
function findDuplicates(arr) {
const duplicates = [];
for (let i = 0; i < arr.length; i++) {
for (let j = i + 1; j < arr.length; j++) {
if (arr[i] === arr[j]) {
duplicates.push(arr[i]);
}
}
}
return duplicates;
}
// Efficient O(n) approach using Set
function findDuplicatesOptimized(arr) {
const seen = new Set();
const duplicates = new Set();
for (let i = 0; i < arr.length; i++) {
if (seen.has(arr[i])) {
duplicates.add(arr[i]);
} else {
seen.add(arr[i]);
}
}
return Array.from(duplicates);
}
Note: When adapting patterns to constraints, prioritize performance characteristics that align with the specific limitations of your environment or use case.
9. Optimizing Pattern Selection
Example: Selecting Patterns Based on Complexity Metrics
const patterns = [
{ name: "Divide and Conquer", complexity: "O(log n)", useCase: "Sorting, searching" },
{ name: "Dynamic Programming", complexity: "O(n^2)", useCase: "Optimization problems" },
{ name: "Greedy Algorithm", complexity: "O(n)", useCase: "Activity selection" }
];
function selectOptimalPattern(patterns) {
return patterns.reduce((best, current) => {
if (current.complexity < best.complexity) return current;
return best;
});
}
const optimal = selectOptimalPattern(patterns);
console.log("Selected pattern:", optimal.name);
| Pattern | Time Complexity | Space Complexity | Best Use Case |
|---|---|---|---|
| Divide and Conquer | O(log n) |
O(log n) |
Sorting, searching |
| Dynamic Programming | O(n^2) |
O(n^2) |
Optimization problems |
| Greedy Algorithm | O(n) |
O(1) |
Activity selection |