· career · 7 min read
Mastering JavaScript Algorithms: 10 Essential Patterns Every Engineer Should Know
Learn 10 algorithmic patterns in JavaScript that simplify complex problems, speed up your coding, and help you ace interviews. Each pattern includes when to use it, a clear strategy, a compact JS example, complexity notes, and interview tips.

Introduction
Here’s what you’ll get: the ability to recognize ten recurring algorithmic patterns, choose the right one fast, and implement clean JavaScript solutions during interviews and in production. Read this once and you’ll stop reinventing the wheel when faced with common problems.
Why patterns matter
Patterns condense problem-solving into repeatable steps. They save time, reduce bugs, and make your code communicate intent. Learn them, and you write faster. Learn them well, and you write smarter.
1) Sliding Window
When to use
- When you need to find a subarray or substring that satisfies a condition (max/min/contains/all) and the window is contiguous.
Strategy
- Expand the right end to include elements.
- Shrink the left end when the window no longer satisfies the condition or to try and minimize it.
JS example - longest subarray with sum <= S
function longestSubarrayWithMaxSum(arr, S) {
let left = 0,
sum = 0,
maxLen = 0;
for (let right = 0; right < arr.length; right++) {
sum += arr[right];
while (sum > S && left <= right) {
sum -= arr[left++];
}
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}Complexity: O(n) time, O(1) extra space. Pitfall: works only for contiguous runs and usually for non-negative numbers (unless you adjust the condition).
Interview tip: Describe window growth and shrink triggers before coding.
2) Two Pointers
When to use
- Sorted arrays, or when comparing elements from both ends towards the center. Useful for pair-sum, removing duplicates in-place, and partitioning problems.
Strategy
- Use left and right pointers that move inward based on comparisons.
JS example - pair with target sum in sorted array
function twoSumSorted(arr, target) {
let left = 0,
right = arr.length - 1;
while (left < right) {
const s = arr[left] + arr[right];
if (s === target) return [left, right];
if (s < target) left++;
else right--;
}
return [-1, -1];
}Complexity: O(n) time, O(1) space. Pitfall: Requires sorted data (or a specific ordering).
Interview tip: Explain pointer movement rules: how comparisons drive pointers.
3) Fast & Slow Pointers (Tortoise and Hare)
When to use
- Detect cycles in linked lists or sequences; find cycle lengths or middle points.
Strategy
- Move one pointer twice as fast. If they meet, there’s a cycle.
- To find cycle start, reset one pointer and move both at same speed.
JS example - detect cycle start in linked list
function detectCycle(head) {
let slow = head,
fast = head;
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
if (slow === fast) {
slow = head;
while (slow !== fast) {
slow = slow.next;
fast = fast.next;
}
return slow; // start of cycle
}
}
return null;
}Complexity: O(n) time, O(1) space. Pitfall: Null checks are crucial. Don’t forget to check fast and fast.next.
Interview tip: Be explicit about why resetting slow to head finds the cycle start (mathematical reasoning helps).
4) Binary Search
When to use
- Search in a sorted array, find boundaries, or search on monotonic predicates (yes/no functions over a domain).
Strategy
- Repeatedly half the search space based on mid checks; maintain invariants for left/right bounds.
JS example - first index where arr[i] >= target
function lowerBound(arr, target) {
let l = 0,
r = arr.length; // r is exclusive
while (l < r) {
const m = Math.floor((l + r) / 2);
if (arr[m] < target) l = m + 1;
else r = m;
}
return l;
}Complexity: O(log n) time, O(1) space. Pitfall: Off-by-one mistakes; decide inclusive/exclusive bounds and stick to them.
Interview tip: State loop invariants before coding (what l and r represent at each step).
5) Merge Intervals
When to use
- When you need to combine overlapping ranges, schedule tasks, or compute coverage.
Strategy
- Sort intervals by start, then iterate merging overlapping neighbors.
JS example - merge overlapping intervals
function mergeIntervals(intervals) {
if (!intervals.length) return [];
intervals.sort((a, b) => a[0] - b[0]);
const res = [intervals[0]];
for (let i = 1; i < intervals.length; i++) {
const [s, e] = intervals[i];
const last = res[res.length - 1];
if (s <= last[1]) last[1] = Math.max(last[1], e);
else res.push([s, e]);
}
return res;
}Complexity: O(n log n) due to sort, O(n) extra for result. Pitfall: Remember to sort; forgetting it breaks correctness.
Interview tip: Clarify input constraints (are intervals closed/open? inclusive endpoints?).
6) Cyclic Sort
When to use
- When input contains numbers in a range (e.g., 1..n) and you need to find missing/duplicate elements in O(n) time and O(1) extra space.
Strategy
- Place each number at its correct index by swapping until each number is either in place or invalid.
JS example - find all missing numbers in [1..n]
function findDisappearedNumbers(nums) {
let i = 0;
while (i < nums.length) {
const correct = nums[i] - 1;
if (nums[i] !== nums[correct])
[nums[i], nums[correct]] = [nums[correct], nums[i]];
else i++;
}
const res = [];
for (let j = 0; j < nums.length; j++) if (nums[j] !== j + 1) res.push(j + 1);
return res;
}Complexity: O(n) time, O(1) extra space (ignoring output). Pitfall: Only works when values map to indices (1..n). Be careful with zero-based arrays.
Interview tip: Explain why swapping moves elements to final positions and terminates.
7) In-place Linked List Manipulations (Reverse / Rewire)
When to use
- Reverse a portion of a list, reorder nodes, or delete nodes in O(1) extra space.
Strategy
- Use a few pointers (prev, curr, next) to iteratively rewire next pointers.
JS example - reverse a linked list
function reverseList(head) {
let prev = null,
curr = head;
while (curr) {
const next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
}Complexity: O(n) time, O(1) space. Pitfall: Preserve next pointer before you reassign; otherwise you lose the rest of the list.
Interview tip: Draw small lists and walk through pointer changes step-by-step.
8) Breadth-First Search (BFS)
When to use
- Shortest path in unweighted graphs, level-order traversal in trees, or problems that require exploring by increasing distance.
Strategy
- Use a queue and process nodes level by level. Track visited nodes to avoid cycles.
JS example - level order of a binary tree
function levelOrder(root) {
if (!root) return [];
const res = [],
q = [root];
while (q.length) {
const levelSize = q.length,
level = [];
for (let i = 0; i < levelSize; i++) {
const node = q.shift();
level.push(node.val);
if (node.left) q.push(node.left);
if (node.right) q.push(node.right);
}
res.push(level);
}
return res;
}Complexity: O(V+E) for graphs; O(n) for trees. Pitfall: Using shift() on large arrays is O(n) per op; use an index-based queue or a dequeue for performance-sensitive JS implementations.
Interview tip: Mention and handle visited sets for graphs to prevent infinite loops.
9) Depth-First Search (DFS) & Backtracking
When to use
- Explore all configurations (combinatorics), solve puzzles (permutations/subsets), or perform path-finding that explores deep answers fast.
Strategy
- Recursively or iteratively explore options; backtrack (undo) when a branch is complete or invalid.
JS example - generate permutations
function permute(nums) {
const res = [];
function dfs(path, used) {
if (path.length === nums.length) return res.push([...path]);
for (let i = 0; i < nums.length; i++) {
if (used[i]) continue;
used[i] = true;
path.push(nums[i]);
dfs(path, used);
path.pop();
used[i] = false;
}
}
dfs([], Array(nums.length).fill(false));
return res;
}Complexity: Typically exponential for full search (O(n!)/O(2^n) etc.). Pitfall: Don’t forget to undo state changes (backtrack) before returning from recursion.
Interview tip: State pruning strategies early (e.g., sort to skip duplicates).
10) Dynamic Programming (Memoization & Tabulation)
When to use
- Problems with overlapping subproblems and optimal substructure (e.g., Fibonacci variants, knapsack, edit distance).
Strategy
- Top-down: memoize recursive calls.
- Bottom-up: build a table iteratively.
JS example - Fibonacci with memoization
function fib(n, memo = {}) {
if (n < 2) return n;
if (memo[n] != null) return memo[n];
memo[n] = fib(n - 1, memo) + fib(n - 2, memo);
return memo[n];
}Complexity: Reduces exponential naive recursion to polynomial (often O(n) for Fibonacci). Pitfall: Need correct subproblem definition and careful table size management to avoid memory blow-up.
Interview tip: Convert a recursive relation to a memoized or tabulated form and explain saved computations.
Putting it together - pattern selection checklist
- Is the data sorted or can it be sorted cheaply? -> Two Pointers or Binary Search.
- Are you working with contiguous subarrays/strings? -> Sliding Window.
- Do you need constant-space index-based mapping? -> Cyclic Sort.
- Is the structure a tree/graph? -> BFS/DFS.
- Is there a repeating subproblem? -> Dynamic Programming.
Start by naming the pattern out loud during interviews. That communicates structure and gets you mental scaffolding to code quickly.
Further reading
- LeetCode Patterns: https://leetcode.com
- Grokking the Coding Interview (Educative): https://www.educative.io/courses/grokking-the-coding-interview
- Eloquent JavaScript (good JS idioms): https://eloquentjavascript.net/
References above provide many pattern-centered problems to practice. Drill these patterns, implement the canonical examples, and you’ll gain both speed and confidence.



