Skip to content

Algorithm Strategies

An algorithm is a finite sequence of instructions, typically used to solve a class of specific problems or to perform a computation.

~ Wikipedia

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.

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.

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;
}

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).

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;
}

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;
}

The Gist: Divide the problem into smaller sub-problems, solve them recursively, and combine the results.

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 timestamp
function 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;
}

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.

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;
}

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 backtracking
function 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'])

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”.

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]
}