Algorithm Strategies
Definition
Section titled “Definition”An algorithm is a finite sequence of instructions, typically used to solve a class of specific problems or to perform a computation.
~ Wikipedia
The concept
Section titled “The concept”We often rely on highly optimized libraries (like Lodash or React’s reconciliation engine) to do the heavy lifting. However, understanding the underlying strategies can help us write more efficient code, debug performance bottlenecks, and solve complex problems.
Choosing the right strategy can turn a sluggish O(n^2) operation into a snappy O(n) or even O(log n) solution. 🚀
Here are 7 strategies that every developer should have in their toolkit.
Strategies
Section titled “Strategies”1. Frequency Counter
Section titled “1. Frequency Counter”The Gist: Use an object or set to collect frequencies of values. This often allows you to avoid nested loops (O(n^2)) in favor of linear time (O(n)).
This is perfect for comparing data sets, finding duplicates, checking if two strings are anagrams, counting, averaging, finding min or max values.
Example: Anagram Checker
Section titled “Example: Anagram Checker”Check if two strings contain the exact same characters with the same frequency.
function areAnagrams(str1: string, str2: string): boolean { if (str1.length !== str2.length) return false;
const lookup: Record<string, number> = {};
for (const char of str1) { lookup[char] = (lookup[char] || 0) + 1; }
for (const char of str2) { if (!lookup[char]) { return false; } lookup[char] -= 1; }
return true;}2. Two Pointers
Section titled “2. Two Pointers”The Gist: Use two distinct indices to traverse the data structure, often moving towards each other or in the same direction at different speeds.
Great for searching pairs in sorted arrays or detecting cycles (like in a linked list).
Example: E-commerce Budget Matcher
Section titled “Example: E-commerce Budget Matcher”Find two products in a sorted list that sum up exactly to a customer’s gift card balance.
function findProductPair(prices: number[], budget: number): [number, number] | null { let left = 0; let right = prices.length - 1;
while (left < right) { const sum = prices[left] + prices[right];
if (sum === budget) { return [prices[left], prices[right]]; } else if (sum < budget) { // Need a larger sum, move left pointer up left++; } else { // Need a smaller sum, move right pointer down right--; } }
return null;}3. Sliding Window
Section titled “3. Sliding Window”The Gist: Maintain a subset of data (a window) that slides over the input to calculate a continuous result.
This is very useful for data analysis, rate limiting, or string manipulation problems where you need to look at a contiguous sub-array.
Example: Maximum Sum Subarray (Kadane’s Algorithm)
Section titled “Example: Maximum Sum Subarray (Kadane’s Algorithm)”Find the maximum sum of any contiguous subarray in an array of numbers.
function maxSubArray(nums: number[]): number { let maxSum = nums[0]; let currentSum = nums[0];
for (let i = 1; i < nums.length; i++) { currentSum = Math.max(nums[i], currentSum + nums[i]); maxSum = Math.max(maxSum, currentSum); }
return maxSum;}4. Divide and Conquer
Section titled “4. Divide and Conquer”The Gist: Divide the problem into smaller sub-problems, solve them recursively, and combine the results.
Example: Binary Search
Section titled “Example: Binary Search”Imagine you have a sorted array of user logs by timestamp, and you need to find a specific entry. A linear scan is O(n), but binary search is O(log n).
interface Log { id: string; timestamp: number; message: string;}
// NB: Assumes logs are sorted by timestampfunction findLogByTimestamp(logs: Log[], target: number): Log | undefined { let left = 0; let right = logs.length - 1;
while (left <= right) { const mid = Math.floor((left + right) / 2); const current = logs[mid];
if (current.timestamp === target) { return current; } else if (current.timestamp < target) { left = mid + 1; } else { right = mid - 1; } }
return undefined;}5. Greedy Algorithm
Section titled “5. Greedy Algorithm”The Gist: Make the locally optimal choice at each stage with the hope of finding a global optimum.
Greedy algorithms are fast and often used in optimization problems like scheduling, routing, or resource allocation.
Example: Meeting Scheduler
Section titled “Example: Meeting Scheduler”Given a list of meetings with start and end times, find the maximum number of meetings you can attend (assuming you can’t be in two places at once).
interface Meeting { start: number; end: number;}
function maxMeetings(meetings: Meeting[]): number { // Sort by end time to finish meetings as early as possible // This leaves more room for subsequent meetings meetings.sort((a, b) => a.end - b.end);
let count = 0; let lastEndTime = -1;
for (const meeting of meetings) { if (lastEndTime <= meeting.start) { count++; lastEndTime = meeting.end; } }
return count;}6. Backtracking
Section titled “6. Backtracking”The Gist: Explore all potential solutions incrementally and abandon (“backtrack”) a candidate as soon as it determines that the candidate cannot possibly be completed to a valid solution. One way to think about it is like trying to solve a maze.
This is used for generating permutations, combinations, solving puzzles (Sudoku), or pathfinding.
Example: Feature Toggles Combinations (Subsets)
Section titled “Example: Feature Toggles Combinations (Subsets)”Generate all possible combinations of feature flags to test different states of your application.
// Power Set: Generate all subsets of a set of features using backtrackingfunction getAllSubsets(features: string[]): string[][] { const result: string[][] = []; const subset: string[] = [];
function dfs(i: number) { if (features.length <= i) { result.push([...subset]); return; }
// Decision 1: Include the feature subset.push(features[i]); dfs(i + 1);
// Decision 2: Exclude the feature (backtrack) subset.pop(); dfs(i + 1); }
dfs(0); return result;}
// Usage: getAllSubsets(['darkMode', 'betaUI', 'newFeed'])7. Dynamic Programming
Section titled “7. Dynamic Programming”The Gist: Break complex problems into simpler subproblems and store their results (memoization or tabulation) to avoid redundant computations.
It’s essentially “recursion with caching”.
Example: Climbing Stairs
Section titled “Example: Climbing Stairs”You are climbing a staircase. It takes n steps to reach the top. Each time you
can either climb 1 or 2 steps. In how many distinct ways can you climb to the
top?
function climbStairs(stairs: number): number { if (stairs <= 2) return stairs
// Tabulation approach (Bottom-Up) const array: number[] = new Array(stairs + 1) array[1] = 1 array[2] = 2
for (let i = 3; i <= stairs; i++) { array[i] = array[i - 1] + array[i - 2] }
return array[stairs]}Resources
Section titled “Resources”- Introduction to Algorithms by Cormen, Leiserson, Rivest, and Stein
- GeeksForGeeks on Algorithm Design
- Algorithms and Data Structures in TypeScript by Oleksii Trekhleb
- GeeksForGeeks on Greedy Algorithms
- GeeksForGeeks on Backtracking Algorithms
- GeeksForGeeks on Dynamic Programming