Matías Fernández / System Design ALGORITHMS · PROBLEM SOLVING

ALGORITHMS · PROBLEM SOLVING · INTERVIEWS

Problem Solving: Approach & Patterns

A study guide to go from “I get the idea” to “I can solve it in an interview”.

You will learn a repeatable method for attacking any problem out loud, how to estimate the complexity an interviewer expects, and the eight patterns behind most array and string questions. Every pattern comes with a template, a diagram, worked examples, the classic bugs, and exercises to try before you peek.

All JavaScript in this guide was run and verified with tests in Node.js.

Published
Oct 4, 2026
Reading
~40 min
Language
JavaScript

0. How to use this guide

Interviews reward two things: a clear process and the ability to spot which known technique fits. Sections 1–3 build the process; sections 4–8 teach the patterns; sections 9–10 help you recognize them under pressure and practice until they stick.

The patterns at a glance

FOUNDATIONS

1. Big O in five minutes

An algorithm is a process or set of steps to accomplish a task. In interviews it is not enough for it to be correct: its time complexity (how many operations it performs relative to the input size n) and its space complexity (how much extra memory it uses) are evaluated. Every pattern in this guide exists to lower one of those two.

1.1 The complexities you will see

How many operations for n = 100,000? (log scale) A horizontal bar chart on a logarithmic scale for n equal to 100,000. O(1) is 1 operation, O(log n) about 17, O(n) 100,000, O(n log n) about 1.7 million, O(n squared) 10 billion, and O(2 to the n) and O(n factorial) are infeasible. A dashed guide marks 10 to the 8 operations, roughly one second: everything up to O(n log n) fits; O(n squared) does not. 10⁸ ops ≈ 1 second O(1) 1 O(log n) ~17 O(n) 100,000 O(n log n) ~1.7 million O(n²) 10 billion O(2ⁿ) infeasible O(n!) infeasible
How many operations for n = 100,000? (log scale)
Big ONameTypical examplen = 100,000
O(1)ConstantAccessing arr[i], reading/writing an object or Map1
O(log n)LogarithmicBinary search~17
O(n)LinearA single pass; frequency counter; two pointers; sliding window100,000
O(n log n)LinearithmicSorting (merge sort, Array.prototype.sort)~1.7 million
O(n2)QuadraticTwo nested loops (most naive solutions)10 billion
O(2n)ExponentialGenerating all subsets; recursive fibonacci without memoinfeasible
O(n!)FactorialGenerating all permutationsinfeasible

1.2 Quick rules for computing Big O

  • Drop constants: O(2n) = O(n); O(500) = O(1).
  • Keep only the dominant term: O(n2 + n) = O(n2).
  • Sequential loops add; nested loops multiply.
  • Different inputs, different variables: iterating over a and then b is O(a + b); nested is O(a·b).
  • Watch for hidden loops: indexOf, includes, splice, slice, shift and the spread [...arr] are O(n). A single for that calls indexOf inside is already O(n2) (you will see this in section 4.3).
  • Hash lookups are O(1) on average: Object, Map and Set keys have O(1) average lookup and insertion. This is the foundation of the Frequency Counter.
  • Recursion uses memory: it takes up space on the call stack; a recursion of depth d uses O(d) space.

1.3 Read the constraints to guess the solution

If the problem statement gives the maximum input size, it is telling you what complexity they are looking for. Assuming ~108 simple operations per second:

Size of nAcceptable complexityCandidate patterns
n <= 10–12O(n!), O(2n·n)Backtracking, brute force
n <= 20–25O(2n)Backtracking / subsets, bitmask DP
n <= 500O(n3)Interval DP, three loops
n <= 5,000O(n2)2D DP, two loops
n <= 106O(n) or O(n log n)Frequency counter, two pointers, sliding window, sorting, heap
very large n / 109O(log n) or O(1)Binary search, math

THE METHOD

2. The problem-solving method

George Polya described a five-step strategy in How to Solve It (1945). In a technical interview the method is the same, but you have to do it out loud: the interviewer evaluates the process as much as the code. A widely used interview framework that maps almost 1-to-1 to Polya is UMPIRE: Understand, Match, Plan, Implement, Review, Evaluate.

Polya’s 5 steps, out loud, mapped to UMPIRE A top-to-bottom flow: 1 Understand the Problem, 2 Explore Concrete Examples, 3 Break It Down, 4 Solve or Simplify, 5 Look Back and Refactor. Each step shows its UMPIRE equivalent. A dashed arrow loops from Solve back to Explore Concrete Examples, labeled “stuck? back to examples”. 1 · Understand the ProblemUMPIRE · Understand 2 · Explore Concrete ExamplesUMPIRE · Understand 3 · Break It DownUMPIRE · Match + Plan 4 · Solve / SimplifyUMPIRE · Implement 5 · Look Back and RefactorUMPIRE · Review + Evaluate stuck? back to examples
Polya’s 5 steps, out loud, mapped to UMPIRE
PolyaUMPIREWhat you do in the interview
Understand the ProblemUnderstandRestate the problem, ask questions, pin down inputs/outputs and constraints.
Explore Concrete ExamplesUnderstandWrite 3–5 cases including edge cases; use them later as tests.
Break It DownMatch + PlanIdentify the pattern; write pseudocode or comments with the steps.
Solve / SimplifyImplementCode the solution (or a simplified version first).
Look Back and RefactorReview + EvaluateWalk through the code with an example, fix bugs, analyze Big O and alternatives.

2.1 Understand: questions to ask every time

Start by restating the problem in your own words, naming the inputs and outputs, and checking which terms you do not fully understand. Then these clarifications prevent most misunderstandings:

Types and ranges
Integers or decimals? Can they be negative? What is the maximum input size?
Order
Is the input sorted? If so, think two pointers or binary search right away.
Duplicates
Can there be repeated values? Do they count as distinct?
Strings
Are uppercase and lowercase the same? Do spaces and punctuation count? ASCII only or Unicode?
Output
Values or indices? The first answer or all of them? What if there is no answer (null, -1, undefined, an exception)?
Mutation
Can I modify the input or must I leave it intact?
Priority
Do I optimize for time or memory?

2.2 Explore examples: edge-case checklist

Write simple examples, complex examples, empty inputs and invalid inputs. They clarify the problem now and become your tests later. A concrete list to go through:

Input typeEdge cases to test
ArraysEmpty; one element; two elements; all equal; already sorted; reverse order; with negatives; with zeros; with duplicates.
Strings""; one character; all the same ("aaaa"); mixed case; spaces and punctuation; special characters.
Numbers0; negatives; very large numbers (precision: in JS integers are exact up to 253-1).
Windows / kk = 0; k = 1; k = n; k > n.
SearchesElement at the start, at the end, in the middle, missing, smaller than all, larger than all.
Invalid inputsnull / undefined / wrong type (ask whether you need to validate them; often you don't).

2.3 Break it down and simplify

Write the steps as comments before writing code: if you later get stuck on syntax, the interviewer has already seen that you have the idea. If the whole problem is too hard, solve a simpler version first and add the difficult part afterwards.

The most useful technique is: first state the brute force out loud with its Big O, then ask “what work am I repeating?”. Repeated work is exactly what the patterns eliminate:

From brute force to a pattern: find the repeated work Start from the brute force and its Big O, then ask what work you are repeating. Searching inside a loop leads to Frequency Counter; sorted input with pair comparisons leads to Multiple Pointers; recomputing an overlapping subarray leads to Sliding Window; discarding half the search space leads to Divide and Conquer or Binary Search; solving the same subproblems again leads to Dynamic Programming. Brute force + its Big O What work am I repeating? Searching inside a loop? Frequency Counter Sorted input, comparing pairs? Multiple Pointers Recomputing an overlapping subarray? Sliding Window Discarding half the search space? Divide & ConquerBinary Search Same subproblems again and again? Dynamic Programming
From brute force to a pattern: find the repeated work

2.4 Look back before saying “done”

  1. Dry run. Walk through the code line by line with one of your examples, noting the value of each variable.
  2. Edge cases. Mentally test the checklist, especially empty and single-element inputs.
  3. Classic mistakes. Off-by-one indices, < vs <= in the while, undeclared variables, accidentally mutating the input, forgetting the return.
  4. Complexity. State time and space, and whether there is a trade-off (for example, more memory in exchange for less time).
  5. Alternatives. Mention improvements or other approaches even if you don’t implement them.

2.5 Time management in a 45-minute interview

A 45-minute interview as a timeline A Gantt-style timeline: Understand from minute 0 to 5, Examples and brute force from 5 to 10, Pick a pattern from 10 to 15, Implement from 15 to 35, and Review with dry run, tests and final Big O from 35 to 45. The first 15 minutes happen before writing code. before coding Understand 0–5 Examples 5–10 Pick pattern 10–15 Implement 15–35 Review 35–45 0 5 10 15 35 45 min
A 45-minute interview as a timeline

THE METHOD IN ACTION

3. Worked example: charCount

  1. Step 1 — Understand

    • Restate: given a string, return an object where each key is a character and the value is how many times it appears.
    • Inputs: a string. Output: an object.
    • Questions: do I count spaces and punctuation? Are "A" and "a" the same character? Do I include characters with 0 occurrences? Agreed decision: only letters and digits, case-insensitive, only the ones that appear.
  2. Step 2 — Explore examples

    charCount("aaaa")              // { a: 4 }
    charCount("hello")             // { h: 1, e: 1, l: 2, o: 1 }
    charCount("Hello hi")          // { h: 2, e: 1, l: 2, o: 1, i: 1 }   case-insensitive
    charCount("Your PIN is 1234!") // spaces and "!" are ignored; digits count
    charCount("")                  // {}
  3. Step 3 — Break it down

    // create an empty object for the result
    // loop over each character in the string
    //   convert it to lowercase
    //   if it is a letter or a number:
    //     if it is already in the object -> add 1
    //     if it is not                   -> add it with value 1
    //   if it is not alphanumeric: ignore it
    // return the object
  4. Step 4 — Solve

    If the “is alphanumeric” part turns out to be hard, apply Simplify: first count every character without filtering, and then add the filter. Final version:

    function charCount(str) {
      const result = {};
      for (const ch of str.toLowerCase()) {
        if (/[a-z0-9]/.test(ch)) {
          result[ch] = (result[ch] || 0) + 1;
        }
      }
      return result;
    }
  5. Step 5 — Look back & refactor

    Readability
    for...of is clearer than an indexed for, and (result[ch] || 0) + 1 replaces the if/else.
    Performance
    Running the regex on every iteration has a cost. A faster alternative compares the character code: charCodeAt between 48–57 (digits) or 97–122 (a–z). Benchmark before optimizing; in an interview it is enough to mention it.
    Structure
    Map avoids collisions with inherited properties ("constructor", "__proto__") and preserves insertion order.
    Complexity
    O(n) time; O(k) space, where k is the number of distinct characters (at most 36 with this filter, i.e. O(1)).

PATTERN 01 · HASH MAPS

4. Frequency Counter

Instead of searching for a value inside another loop, iterate over the collection once and store counts or presence in an object, Map or Set. After that, each lookup costs O(1).

Use it when
You compare two collections, count occurrences, look for duplicates, or need a complement in unsorted data.
Core idea
One pass stores counts or presence in a Map, Set, or object; after that every lookup is O(1).
Cost
Turns O(n²) into O(n) time in exchange for O(n) memory.
Classic trap
Plain-object keys are always strings, and helpers like splice silently mutate the caller’s array.

4.1 When to use it

  • Comparing two collections: “same elements”, “same frequency”, anagrams, permutations.
  • Questions like “is there a duplicate?”, “how many times does it appear?”, “the first non-repeating one”.
  • Looking for a complement: “two numbers that add up to X” in an unsorted array.
  • Grouping elements by a key (for example, grouping anagrams).

4.2 Template

function frequencyCounter(a, b) {
  if (a.length !== b.length) return false;      // cheap shortcut
  const freq = new Map();
  for (const x of a) freq.set(x, (freq.get(x) || 0) + 1);  // count
  for (const x of b) {                                       // consume
    if (!freq.get(x)) return false;
    freq.set(x, freq.get(x) - 1);
  }
  return true;
}

4.3 Common pitfalls

Take same(arr1, arr2): return true if every value in arr1 has its square in arr2, with the same frequency. The first idea most people write is this:

// Naive version: O(n²), and it empties arr2 as a side effect
function sameNaive(arr1, arr2) {
  if (arr1.length !== arr2.length) return false;
  for (let i = 0; i < arr1.length; i++) {
    const index = arr2.indexOf(arr1[i] ** 2); // hidden O(n) loop
    if (index === -1) return false;
    arr2.splice(index, 1);                    // hidden O(n) loop + mutation
  }
  return true;
}
  • It is O(n2): indexOf and splice are O(n) inside an O(n) loop.
  • It modifies the caller's array: after sameNaive(a, b), b ends up empty or incomplete. If you need to mutate, work on a copy: const copy = [...arr2].
  • Object keys are always strings: if you count with a plain object, key ** 2 only works because JS converts "2" to a number when using **. It is fragile: with a Map keys keep their type.
  • Redundant checks: a guard like if (!(key ** 2 in freq2)) before comparing counts is unnecessary: if the key doesn't exist, the comparison already yields undefined !== n.

Version using Map, with no coercion or mutation:

function same(arr1, arr2) {
  if (arr1.length !== arr2.length) return false;
  const freq1 = new Map();
  const freq2 = new Map();
  for (const v of arr1) freq1.set(v, (freq1.get(v) || 0) + 1);
  for (const v of arr2) freq2.set(v, (freq2.get(v) || 0) + 1);
  for (const [v, count] of freq1) {
    if (freq2.get(v ** 2) !== count) return false;
  }
  return true;
}
same([1, 2, 3], [4, 1, 9]) with two frequency maps arr1 [1, 2, 3] is counted into freq1, a Map of 1 to 1, 2 to 1, 3 to 1. arr2 [4, 1, 9] is counted into freq2, 4 to 1, 1 to 1, 9 to 1. A decision then checks, for each value v in freq1, whether freq2.get(v squared) equals its count. Three separate loops: O(n) time and O(n) space instead of a nested O(n squared) search. arr1 = [1, 2, 3] arr2 = [4, 1, 9] freq1: Map1:1 2:1 3:1 freq2: Map4:1 1:1 9:1 freq2.get(v²)=== count ? 3 separate loops · O(n) time · O(n) space count count
same([1, 2, 3], [4, 1, 9]) with two frequency maps

4.4 Worked example: validAnagram

Count the letters of the first string and “consume” them with the second. If any letter is not available (count 0 or missing), it is not an anagram. Since we already checked that the lengths are equal, there is no need for a third loop to verify that everything ends at 0.

function validAnagram(a, b) {
  if (a.length !== b.length) return false;
  const lookup = {};
  for (const ch of a) lookup[ch] = (lookup[ch] || 0) + 1;
  for (const ch of b) {
    if (!lookup[ch]) return false;
    lookup[ch] -= 1;
  }
  return true;
}

O(n) timeO(1) space for a fixed 26-letter alphabet · O(k) in general

Alternative: sort both strings and compare them: [...a].sort().join('') === [...b].sort().join('') is O(n log n); worth mentioning as the brute force.

4.5 Exercises

EXERCISE 4.A

sameFrequency(n1, n2)

Do two integers have the same digits with the same frequency?

sameFrequency(182, 281)          // true
sameFrequency(34, 14)            // false
sameFrequency(3589578, 5879385)  // true
Show solution
function sameFrequency(n1, n2) {
  const s1 = String(n1), s2 = String(n2);
  if (s1.length !== s2.length) return false;
  const freq = {};
  for (const d of s1) freq[d] = (freq[d] || 0) + 1;
  for (const d of s2) {
    if (!freq[d]) return false;
    freq[d]--;
  }
  return true;
}
EXERCISE 4.B

areThereDuplicates(...args)

Is there any repeated value among the arguments?

Show solution
function areThereDuplicates(...args) {
  return new Set(args).size !== args.length;
}

A Set is a frequency counter that only stores presence. O(n) time and space. The Multiple Pointers alternative is to sort and compare neighbors: O(n log n) time, O(1) extra space.

EXERCISE 4.C

twoSum(nums, target)

Return the indices of two numbers that add up to target in an unsorted array.

Show solution
function twoSum(nums, target) {
  const seen = new Map(); // value -> index
  for (let i = 0; i < nums.length; i++) {
    const need = target - nums[i];
    if (seen.has(need)) return [seen.get(need), i];
    seen.set(nums[i], i);
  }
  return null;
}

A key variant of the pattern: instead of counting, store value → index and look up the complement. It is the most famous interview problem (LeetCode 1). If the array were sorted, you would use Multiple Pointers as in sumZero (section 5.2).

4.6 Practice on LeetCode

  • 242Valid Anagram
  • 1Two Sum
  • 49Group Anagrams
  • 387First Unique Character
  • 383Ransom Note
  • 217Contains Duplicate
  • 128Longest Consecutive Sequence
  • 560Subarray Sum Equals K (with prefix sums, 8.4)

PATTERN 02 · INDICES

5. Multiple Pointers

Use two (or more) indices that traverse the structure and move based on a condition. It avoids the nested loop and usually uses O(1) extra memory. Most of the time it requires the input to be sorted (or sorting it to be acceptable, O(n log n)).

Use it when
The input is sorted (or sorting is acceptable) and you compare pairs, clean up in place, or walk two sequences.
Core idea
Two or more indices move by a rule; each move safely discards at least one candidate.
Cost
O(n) time and O(1) extra memory, plus O(n log n) if you have to sort first.
Classic trap
Using left <= right lets one element pair with itself.

5.1 The three variants

The three ways pointers move Three panels. Opposite ends: left starts at the first cell and right at the last, and they move toward each other. Same direction: i marks where to write and j explores ahead; j always advances. Two sequences: one pointer per array, and whichever is appropriate advances. opposite ends same direction (slow / fast) two sequences left → ← right i writes j explores → i j
The three ways pointers move
VariantHow they moveExamples
Opposite endsleft = 0, right = n-1; they move toward each other until they cross.sumZero, averagePair, palindromes, Container With Most Water, 3Sum.
Same direction (slow / fast)i marks “where I write”, j explores; j always advances.countUniqueValues, removing duplicates in-place, moving zeros to the end.
Two sequencesOne pointer per array/string; whichever is appropriate advances.isSubsequence, merging two sorted arrays (merge sort).

5.2 Why it works: sumZero

function sumZero(arr) {
  let left = 0, right = arr.length - 1;
  while (left < right) {
    const sum = arr[left] + arr[right];
    if (sum === 0) return [arr[left], arr[right]];
    if (sum > 0) right--;
    else left++;
  }
}

With the array sorted: if arr[left] + arr[right] > 0, no pair using arr[right] can add up to 0 (any other left is even larger), so right is discarded. Symmetrically, if the sum is less than 0, left is discarded. Each step eliminates one candidate: n steps in total, O(n). This “I safely discard one candidate” argument is what you should explain in the interview.

sumZero([-4, -3, -2, -1, 0, 1, 2, 5]): each step discards one candidate Four rows trace the two pointers over the sorted array. Step 1: left at -4, right at 5, sum 1 is greater than 0 so right moves left. Step 2: -4 plus 2 is -2, less than 0, so left moves right. Step 3: -3 plus 2 is -1, so left moves right. Step 4: -2 plus 2 is 0, return [-2, 2]. Discarded values are greyed out. left → ← right -4 -3 -2 -1 0 1 2 5 -4 + 5 = 1 > 0 → right-- -4 -3 -2 -1 0 1 2 5 -4 + 2 = -2 < 0 → left++ -4 -3 -2 -1 0 1 2 5 -3 + 2 = -1 < 0 → left++ -4 -3 -2 -1 0 1 2 5 -2 + 2 = 0 → return [-2, 2]
sumZero([-4, -3, -2, -1, 0, 1, 2, 5]): each step discards one candidate

5.3 Worked example: countUniqueValues

Pointer i marks the last unique value found; j explores. When arr[j] differs from arr[i], advance i and copy the new value there. At the end, the first i + 1 elements are the unique ones.

function countUniqueValues(arr) {
  if (arr.length === 0) return 0;
  let i = 0;
  for (let j = 1; j < arr.length; j++) {
    if (arr[i] !== arr[j]) {
      i++;
      arr[i] = arr[j];
    }
  }
  return i + 1;
}
countUniqueValues([1, 1, 2, 3, 3]): i writes, j explores Five rows. With i at 0 and j at 1, both hold 1, nothing happens. With j at 2, 1 differs from 2, so i becomes 1 and arr[1] becomes 2, giving [1, 2, 2, 3, 3]. With j at 3, 2 differs from 3, so i becomes 2 and arr[2] becomes 3, giving [1, 2, 3, 3, 3]. With j at 4, 3 equals 3, nothing happens. The first i + 1 = 3 cells, 1, 2 and 3, are the unique values. 1 1 2 3 3 i j 1 == 1 → nothing 1 1 2 3 3 i j 1 ≠ 2 → i = 1, arr[1] = 2 1 2 2 3 3 i j 2 ≠ 3 → i = 2, arr[2] = 3 1 2 3 3 3 i j 3 == 3 → nothing 1 2 3 3 3 return i + 1 = 3
countUniqueValues([1, 1, 2, 3, 3]): i writes, j explores

This version modifies the input (it is the LeetCode 26 Remove Duplicates technique). If you can't mutate, it is enough to count each time an element differs from the previous one:

function countUniqueValuesPure(arr) {
  let count = 0;
  for (let j = 0; j < arr.length; j++) {
    if (j === 0 || arr[j] !== arr[j - 1]) count++;
  }
  return count;
}

Both: O(n) timeO(1) space

5.4 Exercises

EXERCISE 5.A

averagePair(arr, target)

Is there a pair in the sorted array whose average equals target?

Show solution
function averagePair(arr, target) {
  let left = 0, right = arr.length - 1;
  while (left < right) {
    const avg = (arr[left] + arr[right]) / 2;
    if (avg === target) return true;
    if (avg < target) left++;
    else right--;
  }
  return false;
}
EXERCISE 5.B

isSubsequence(sub, str)

Do the characters of sub appear in str in the same order (not necessarily contiguous)?

isSubsequence('sing', 'sting')       // true
isSubsequence('abc', 'abracadabra')  // true
isSubsequence('abc', 'acb')          // false (order matters)
Show solution
function isSubsequence(sub, str) {
  let i = 0;
  for (let j = 0; j < str.length && i < sub.length; j++) {
    if (sub[i] === str[j]) i++;
  }
  return i === sub.length;
}
EXERCISE 5.C

isPalindrome(s)

Does the string read the same in both directions? Use opposite ends.

Show solution
function isPalindrome(s) {
  let l = 0, r = s.length - 1;
  while (l < r) {
    if (s[l] !== s[r]) return false;
    l++; r--;
  }
  return true;
}

5.5 Practice on LeetCode

  • 167Two Sum II
  • 125Valid Palindrome
  • 26Remove Duplicates from Sorted Array
  • 283Move Zeroes
  • 11Container With Most Water
  • 153Sum
  • 88Merge Sorted Array
  • 141Linked List Cycle (slow/fast, Floyd’s algorithm)

PATTERN 03 · CONTIGUOUS RANGES

6. Sliding Window

Maintain a “window” (a contiguous range [left, right]) over an array or string and update its state incrementally as it moves, instead of recomputing it from scratch. It only applies to contiguous subarrays/substrings.

Use it when
The question is about a contiguous subarray or substring: of size k, or the longest/shortest one that satisfies X.
Core idea
Keep the state of a window and update it as it slides instead of recomputing it from scratch.
Cost
O(n) time; O(1) memory for sums, O(k) for character maps.
Classic trap
The variable window only works with positive numbers; with negatives switch to prefix sums.

6.1 Fixed vs variable window

Fixed-size windowVariable-size window
Signal in the statement“n consecutive elements”, “subarray of size k”.“the longest or shortest subarray/substring that satisfies X”.
MovementWhen an element enters on the right, one leaves on the left.right always advances; left advances only while the window is invalid (or while it remains valid, depending on the problem).
ExamplemaxSubarraySumminSubArrayLen, findLongestSubstring

6.2 Fixed window: maxSubarraySum

The brute force recomputes every window from scratch:

// Naive version: recomputes every window, O(n · num)
function maxSubarraySumNaive(arr, num) {
  if (num > arr.length) return null;
  let max = -Infinity;
  for (let i = 0; i <= arr.length - num; i++) {
    let temp = 0; // writing "temp = 0" without let creates a global
    for (let j = 0; j < num; j++) temp += arr[i + j];
    max = Math.max(max, temp);
  }
  return max;
}
  • It is O(n · num): consecutive windows share all but two elements, and that shared work is repeated.
  • Declare the accumulator: writing temp = 0 without let creates an implicit global variable (and throws in strict mode or in ES modules).
  • Define num <= 0: with num = 0 a careless version returns 0 even though the result isn't well defined. Agree with the interviewer on what to return.
  • Initialize from real data: initializing maxSum with the sum of the first real window (not with 0) is what makes it work with negative numbers. Starting at maxSum = 0 is a classic bug.

Robust sliding-window version — add the element that enters, subtract the one that leaves:

function maxSubarraySum(arr, num) {
  if (num <= 0 || arr.length < num) return null;
  let windowSum = 0;
  for (let i = 0; i < num; i++) windowSum += arr[i];
  let maxSum = windowSum;
  for (let i = num; i < arr.length; i++) {
    windowSum += arr[i] - arr[i - num];
    maxSum = Math.max(maxSum, windowSum);
  }
  return maxSum;
}
maxSubarraySum([2, 6, 9, 2, 1, 8, 5, 6, 3], 3): slide, don’t recompute Seven rows show a window of three cells moving one position right at a time. Each move adds the entering element and subtracts the leaving one: 17, then plus 2 minus 2 equals 17, plus 1 minus 6 equals 12, plus 8 minus 9 equals 11, plus 5 minus 2 equals 14, plus 6 minus 1 equals 19, the maximum, and plus 3 minus 8 equals 14. 2 6 9 2 1 8 5 6 3 first window = 17 2 6 9 2 1 8 5 6 3 +2 − 2 → 17 2 6 9 2 1 8 5 6 3 +1 − 6 → 12 2 6 9 2 1 8 5 6 3 +8 − 9 → 11 2 6 9 2 1 8 5 6 3 +5 − 2 → 14 2 6 9 2 1 8 5 6 3 +6 − 1 → 19 max 2 6 9 2 1 8 5 6 3 +3 − 8 → 14
maxSubarraySum([2, 6, 9, 2, 1, 8, 5, 6, 3], 3): slide, don’t recompute

6.3 Variable window template

let left = 0;
for (let right = 0; right < arr.length; right++) {
  // 1. add arr[right] to the window state
  while (/* the window became invalid (or is already valid, if looking for the minimum) */) {
    // 2. (if looking for the minimum) update the answer here
    // 3. remove arr[left] from the state and advance left
    left++;
  }
  // 4. (if looking for the maximum) update the answer with right - left + 1
}

Each index enters the window once and leaves it once, so even though there is a while inside the for, the total is O(n), not O(n2). Interviewers ask about this often. Here is the template running on exercise 6.A:

minSubArrayLen([2, 3, 1, 2, 4, 3], 7): grow right, shrink left Each row is a moment where the window sum reaches the target 7. Cells 0 to 3 sum to 8, best length 4, then the left edge shrinks. Cells 1 to 4 sum to 10, best stays 4. Cells 2 to 4 sum to 7, best 3. Cells 3 to 5 sum to 9, best 3. Cells 4 to 5 sum to 7, best 2, the answer. Every index enters and leaves the window once, so the whole scan is O(n). 2 3 1 2 4 3 2+3+1+2 = 8 ≥ 7 → best 4, shrink 2 3 1 2 4 3 3+1+2+4 = 10 ≥ 7 → best 4, shrink 2 3 1 2 4 3 1+2+4 = 7 ≥ 7 → best 3, shrink 2 3 1 2 4 3 2+4+3 = 9 ≥ 7 → best 3, shrink 2 3 1 2 4 3 4+3 = 7 ≥ 7 → best 2
minSubArrayLen([2, 3, 1, 2, 4, 3], 7): grow right, shrink left

6.4 Exercises

EXERCISE 6.A

minSubArrayLen(nums, target)

Minimum length of a contiguous subarray with sum >= target (positive integers). Return 0 if none exists.

minSubArrayLen([2,3,1,2,4,3], 7)            // 2  -> [4,3]
minSubArrayLen([1,4,16,22,5,7,8,9,10], 39)  // 3  -> [16,22,5]
minSubArrayLen([1,4,16,22,5,7,8,9,10], 95)  // 0  -> none exists
Show solution
function minSubArrayLen(nums, target) {
  let left = 0, sum = 0, best = Infinity;
  for (let right = 0; right < nums.length; right++) {
    sum += nums[right];
    while (sum >= target) {
      best = Math.min(best, right - left + 1);
      sum -= nums[left];
      left++;
    }
  }
  return best === Infinity ? 0 : best;
}
EXERCISE 6.B

findLongestSubstring(str)

Length of the longest substring without repeating characters.

findLongestSubstring('rithmschool')    // 7  -> 'rithmsc'
findLongestSubstring('thisisawesome')  // 6  -> 'awesom'
findLongestSubstring('bbbbbb')         // 1
Show solution
function findLongestSubstring(str) {
  const lastSeen = {}; // char -> last index
  let left = 0, longest = 0;
  for (let right = 0; right < str.length; right++) {
    const ch = str[right];
    if (lastSeen[ch] !== undefined && lastSeen[ch] >= left) {
      left = lastSeen[ch] + 1;
    }
    lastSeen[ch] = right;
    longest = Math.max(longest, right - left + 1);
  }
  return longest;
}

It combines Sliding Window with Frequency Counter: the object stores the last index of each character, which lets left jump directly instead of advancing one step at a time. The condition lastSeen[ch] >= left prevents the window from moving backward because of old occurrences (try it with 'abba').

O(n) timeO(k) space

6.5 Practice on LeetCode

  • 643Maximum Average Subarray I (fixed)
  • 209Minimum Size Subarray Sum
  • 3Longest Substring Without Repeating Characters
  • 424Longest Repeating Character Replacement
  • 567Permutation in String
  • 438Find All Anagrams
  • 76Minimum Window Substring (hard)

PATTERN 04 · HALVING

7. Divide and Conquer

Split the problem into smaller parts, solve each one (often recursively) and combine the results. The two canonical examples are binary search (one half is discarded: O(log n)) and merge sort (both halves are solved and combined: O(n log n)).

Use it when
The data is sorted, the condition is monotonic, or the problem splits naturally into halves.
Core idea
Discard half of the search space (binary search) or solve both halves and combine them (merge sort).
Cost
O(log n) for binary search; O(n log n) time and O(n) space for merge sort.
Classic trap
Off-by-one bounds: lo <= hi vs lo < hi, and lo = mid causing an infinite loop.

7.1 Bug-free binary search

Binary search is easy to understand and very easy to get wrong. The details that matter:

Precondition
The array must be sorted (or, more generally, the condition must be monotonic).
Invariant
If the value exists, it is inside [lo, hi]. Every branch must preserve this.
Closed interval
lo <= hi with hi = n - 1 to search for an exact value. Using < here would skip the single-element case.
Move past the middle
lo = mid + 1 / hi = mid - 1. Assigning lo = mid can cause an infinite loop.
Overflow
lo + Math.floor((hi - lo) / 2) is the safe form. In JS (lo + hi) / 2 doesn't overflow, but in Java/C++ it does; mentioning it earns points.
function search(arr, val) {
  let lo = 0, hi = arr.length - 1;
  while (lo <= hi) {
    const mid = lo + Math.floor((hi - lo) / 2);
    if (arr[mid] < val) lo = mid + 1;
    else if (arr[mid] > val) hi = mid - 1;
    else return mid;
  }
  return -1;
}
search([1, 3, 5, … 17], 15): half the interval disappears each step Three rows. Row 1: lo 0, hi 8, mid 4 holds 9, which is less than 15, so lo becomes 5. Row 2: lo 5, hi 8, mid 6 holds 13, less than 15, so lo becomes 7. Row 3: lo 7, hi 8, mid 7 holds 15, found, return 7. Cells outside the interval are greyed out. 1 3 5 7 9 11 13 15 17 lo mid hi 9 < 15 → lo = mid + 1 1 3 5 7 9 11 13 15 17 lo mid hi 13 < 15 → lo = mid + 1 1 3 5 7 9 11 13 15 17 lo mid hi 15 === 15 → return 7
search([1, 3, 5, … 17], 15): half the interval disappears each step

7.2 Lower bound: the first position ≥ val

Many problems don't ask “is it there?” but “where is the first/last occurrence?” or “where would I insert it?”. The template with a half-open interval [lo, hi) handles all of those cases:

function lowerBound(arr, val) {
  let lo = 0, hi = arr.length; // [lo, hi)
  while (lo < hi) {
    const mid = lo + Math.floor((hi - lo) / 2);
    if (arr[mid] < val) lo = mid + 1;
    else hi = mid;
  }
  return lo;
}

7.3 Exercises

EXERCISE 7.A

countZeroes(arr)

The array has 1s followed by 0s. Count the 0s in O(log n).

Show solution
function countZeroes(arr) {
  // arr: ones followed by zeros. We look for the first 0.
  let lo = 0, hi = arr.length;
  while (lo < hi) {
    const mid = lo + Math.floor((hi - lo) / 2);
    if (arr[mid] === 1) lo = mid + 1;
    else hi = mid;
  }
  return arr.length - lo;
}
EXERCISE 7.B

sortedFrequency(arr, val)

How many times does val appear in a sorted array? Do it in O(log n); return -1 if it is missing.

Show solution
function sortedFrequency(arr, val) {
  const first = lowerBound(arr, val);
  if (first === arr.length || arr[first] !== val) return -1;
  return lowerBound(arr, val + 1) - first;
}

Trick: the first position of val + 1 is right after the last occurrence of val (valid for integers).

EXERCISE 7.C

findRotatedIndex(arr, val)

Search in a sorted array that was rotated at an unknown point, in O(log n).

findRotatedIndex([6,7,8,9,1,2,3,4], 8)       // 2
findRotatedIndex([6,7,8,9,1,2,3,4], 3)       // 6
findRotatedIndex([37,44,66,102,10,22], 14)   // -1
Show solution
function findRotatedIndex(arr, val) {
  let lo = 0, hi = arr.length - 1;
  while (lo <= hi) {
    const mid = lo + Math.floor((hi - lo) / 2);
    if (arr[mid] === val) return mid;
    if (arr[lo] <= arr[mid]) {            // left half is sorted
      if (arr[lo] <= val && val < arr[mid]) hi = mid - 1;
      else lo = mid + 1;
    } else {                              // right half is sorted
      if (arr[mid] < val && val <= arr[hi]) lo = mid + 1;
      else hi = mid - 1;
    }
  }
  return -1;
}

Key point: at each step, at least one of the two halves is sorted. Check whether the value falls in the sorted half; if not, it is in the other one (LeetCode 33).

findRotatedIndex([6, 7, 8, 9, 1, 2, 3, 4], 3): one half is always sorted Three rows. lo 0, hi 7, mid 3 holds 9; the left half 6 to 9 is sorted and 3 is not in it, so lo becomes 4. lo 4, hi 7, mid 5 holds 2; the left half 1 to 2 is sorted and 3 is not in it, so lo becomes 6. lo 6, mid 6 holds 3: return 6. 6 7 8 9 1 2 3 4 lo mid hi left half [6 … 9] is sorted3 is not in it → lo = mid + 1 6 7 8 9 1 2 3 4 lo mid hi left half [1 … 2] is sorted3 is not in it → lo = mid + 1 6 7 8 9 1 2 3 4 lo mid hi arr[mid] === 3 → return 6
findRotatedIndex([6, 7, 8, 9, 1, 2, 3, 4], 3): one half is always sorted

7.4 Merge sort

The other side of divide and conquer: instead of discarding a half, solve both halves and combine them.

function mergeSort(arr) {
  if (arr.length <= 1) return arr;
  const mid = Math.floor(arr.length / 2);
  return merge(mergeSort(arr.slice(0, mid)), mergeSort(arr.slice(mid)));
}
function merge(a, b) {
  const out = [];
  let i = 0, j = 0;
  while (i < a.length && j < b.length) out.push(a[i] <= b[j] ? a[i++] : b[j++]);
  while (i < a.length) out.push(a[i++]);
  while (j < b.length) out.push(b[j++]);
  return out;
}
mergeSort([4, 1, 3, 2]): split to single elements, merge back in order A tree. The split phase divides [4, 1, 3, 2] into [4, 1] and [3, 2], then into [4], [1], [3] and [2]. The merge phase combines [4] and [1] into [1, 4], [3] and [2] into [2, 3], and finally [1, 4] and [2, 3] into [1, 2, 3, 4]. There are log n levels and each level processes n elements. split · log n levels merge · n work per level [4, 1, 3, 2] [4, 1] [3, 2] [4] [1] [3] [2] [1, 4] [2, 3] [1, 2, 3, 4]
mergeSort([4, 1, 3, 2]): split to single elements, merge back in order

There are log n levels of splitting and at each level merge processes n elements: O(n log n) time, O(n) space. The merge function is an example of Multiple Pointers over two sequences.

7.5 Binary search on the answer

When the problem asks for “the minimum value X such that a condition holds” and the condition is monotonic (if it holds for X, it holds for every larger value), you can binary search over the range of possible answers instead of over an array. Examples: LeetCode 875 Koko Eating Bananas, 1011 Capacity to Ship Packages, 69 Sqrt(x).

7.6 Practice on LeetCode

  • 704Binary Search
  • 35Search Insert Position
  • 34First and Last Position
  • 33Search in Rotated Sorted Array
  • 153Find Minimum in Rotated Sorted Array
  • 912Sort an Array (merge sort)
  • 875Koko Eating Bananas

NEXT LEVEL

8. Beyond the core four

Dynamic Programming, Greedy and Backtracking show up as soon as problems involve choices; Prefix Sum quietly powers many array questions. Here is enough of each to recognize it and write a first solution.

8.1 Dynamic Programming (DP)

It applies when the problem can be expressed in terms of smaller subproblems that repeat (“overlapping subproblems”) and the optimal solution is built from the optimal solutions of the subproblems (“optimal substructure”). The idea: solve each subproblem only once and store the result.

Top-down (memoization)
Recursion + cache. Write the natural recursion first and add a memo object to it.
Bottom-up (tabulation)
Fill a table from the base cases upward. Avoids the risk of stack overflow.
Signals
“Number of ways to...”, “minimum/maximum cost to...”, “is it possible to reach...?”, sequential decisions.
Recipe
1) define the state (dp[i] = what it means), 2) the recurrence, 3) the base cases, 4) the computation order, 5) where the answer is.
function fibMemo(n, memo = {}) {
  if (n <= 2) return 1;
  if (memo[n]) return memo[n];
  return (memo[n] = fibMemo(n - 1, memo) + fibMemo(n - 2, memo));
}
fibMemo(5): the repeated subproblem is answered from the memo The call tree of fib(5). fib(5) calls fib(4) and fib(3). fib(4) calls fib(3) and fib(2); that fib(3) calls fib(2) and fib(1), the base cases. The second fib(3), under fib(5), is a memo hit: it returns the stored value, so its children fib(2) and fib(1), drawn dashed, are never computed. fib(5) fib(4) fib(3)memo hit fib(3) fib(2) = 1 fib(2) fib(1) fib(2) fib(1) dashed = work the memo skips
fibMemo(5): the repeated subproblem is answered from the memo

Without memo, recursive fibonacci is O(2n); with memo it is O(n). fibMemo(50) returns instantly.

Climbing Stairs (bottom-up)

function climbStairs(n) {
  let prev = 1, curr = 1; // ways to reach step 0 and step 1
  for (let i = 2; i <= n; i++) [prev, curr] = [curr, prev + curr];
  return curr;
}

Number of ways to climb n steps taking 1 or 2 at a time: ways(n) = ways(n-1) + ways(n-2). Since only the two previous values are used, space drops to O(1).

Coin Change

function coinChange(coins, amount) {
  const dp = new Array(amount + 1).fill(Infinity);
  dp[0] = 0;
  for (let a = 1; a <= amount; a++) {
    for (const c of coins) {
      if (c <= a) dp[a] = Math.min(dp[a], dp[a - c] + 1);
    }
  }
  return dp[amount] === Infinity ? -1 : dp[amount];
}

dp[a] = minimum number of coins to make amount a. O(amount · coins).

8.2 Greedy

At each step, take the decision that looks best locally, without going back. It is simple and fast, but only correct if you can justify that the local choice never prevents reaching the optimum.

Maximum number of non-overlapping intervals

function maxNonOverlapping(intervals) {
  const sorted = [...intervals].sort((a, b) => a[1] - b[1]); // by end
  let count = 0, lastEnd = -Infinity;
  for (const [start, end] of sorted) {
    if (start >= lastEnd) { count++; lastEnd = end; }
  }
  return count;
}

Sort by end and always take the one that ends first (it leaves the most free room). Correct and O(n log n).

Coins [1, 3, 4], amount 6: where greedy fails From amount 6, greedy always takes the largest coin: minus 4 leaves 2, minus 1 leaves 1, minus 1 leaves 0, for 3 coins. The optimal answer found by dynamic programming takes 3 twice: 6 minus 3 leaves 3, minus 3 leaves 0, for 2 coins. greedy: largest coin first DP: coinChange amount = 6 2 left 1 left 0 left 3 coins 3 left 0 left 2 coins −4 −1 −1 −3 −3
Coins [1, 3, 4], amount 6: where greedy fails

Kadane’s algorithm

Another famous greedy algorithm: maximum subarray sum (LeetCode 53), which can also be seen as DP.

function maxSubarray(nums) {
  let best = nums[0], curr = nums[0];
  for (let i = 1; i < nums.length; i++) {
    curr = Math.max(nums[i], curr + nums[i]);
    best = Math.max(best, curr);
  }
  return best;
}

8.3 Backtracking

Explore all candidate solutions by building them step by step, and “undo” the last choice to try the next one. It is used to generate combinations, permutations, subsets, or to solve puzzles (N-Queens, Sudoku). The complexity is exponential; the key is to prune branches that cannot lead to a valid solution.

function backtrack(state) {
  if (/* state is a complete solution */) { save(copy of state); return; }
  for (const option of validOptions(state)) {
    choose(option);        // modify state
    backtrack(state);      // explore
    undo(option);          // go back
  }
}

Subsets

function subsets(nums) {
  const result = [];
  const path = [];
  function backtrack(start) {
    result.push([...path]);
    for (let i = start; i < nums.length; i++) {
      path.push(nums[i]);      // choose
      backtrack(i + 1);        // explore
      path.pop();              // undo
    }
  }
  backtrack(0);
  return result;
}
subsets([1, 2, 3]): choose, explore, undo A decision tree rooted at the empty path. Choosing 1, 2 or 3 creates [1], [2] and [3]. From [1], choosing 2 or 3 gives [1, 2] and [1, 3]; from [2], choosing 3 gives [2, 3]; from [1, 2], choosing 3 gives [1, 2, 3]. Every node is saved as a copy of the path, for 8 subsets in total. Returning from a child undoes the choice with path.pop(). [ ] [1] [2] [3] [1, 2] [1, 3] [2, 3] [1, 2, 3] every node → result.push([...path]) return → path.pop() (undo) 8 subsets = 2³ +1 +2 +3 +2 +3 +3 +3
subsets([1, 2, 3]): choose, explore, undo

Permutations

function permutations(nums) {
  const result = [], path = [], used = new Array(nums.length).fill(false);
  function backtrack() {
    if (path.length === nums.length) { result.push([...path]); return; }
    for (let i = 0; i < nums.length; i++) {
      if (used[i]) continue;
      used[i] = true; path.push(nums[i]);
      backtrack();
      path.pop(); used[i] = false;
    }
  }
  backtrack();
  return result;
}

Subsets: O(2n · n)Permutations: O(n! · n)

8.4 Prefix Sum

Precompute prefix[i] = sum of the first i elements; then the sum of any range [i, j] is prefix[j+1] - prefix[i] in O(1).

function rangeSums(nums) {
  const prefix = [0];
  for (const n of nums) prefix.push(prefix[prefix.length - 1] + n);
  return (i, j) => prefix[j + 1] - prefix[i]; // sum of nums[i..j]
}
Prefix sums: any range sum becomes one subtraction nums is [3, 1, 4, 1, 5] and prefix is [0, 3, 4, 8, 9, 14], where each prefix entry sits on the boundary before the matching nums element. The range nums[1..3], that is 1, 4 and 1, sums to prefix[4] minus prefix[1], 9 minus 3, which is 6. nums prefix 0 1 2 3 4 0 1 2 3 4 5 3 1 4 1 5 0 3 4 8 9 14 sum(nums[1..3]) = prefix[4] − prefix[1] = 9 − 3 = 6
Prefix sums: any range sum becomes one subtraction

Combined with a Frequency Counter it solves “number of subarrays with sum k” even with negatives, which is exactly where Sliding Window fails:

function subarraySum(nums, k) {
  const seen = new Map([[0, 1]]); // prefix -> how many times
  let sum = 0, count = 0;
  for (const n of nums) {
    sum += n;
    count += seen.get(sum - k) || 0;
    seen.set(sum, (seen.get(sum) || 0) + 1);
  }
  return count;
}

8.5 Other patterns you will meet later

PatternWhenExample
BFS / DFSGraphs, trees, grids; BFS for unweighted shortest path.Number of Islands, Level Order Traversal
Heap / Top-K“the k largest/most frequent”, merging k sorted lists.Top K Frequent Elements, Kth Largest
Monotonic stack“the next greater/smaller element”.Daily Temperatures, Next Greater Element
Fast & slow pointersCycles in linked lists, finding the middle.Linked List Cycle, Middle of the List
Union-FindConnected components, detecting cycles in undirected graphs.Number of Provinces, Redundant Connection
Merge intervalsOverlapping intervals: sort by start and merge.Merge Intervals, Insert Interval

CHEAT SHEET

9. How to recognize the pattern

The hardest part of an interview is not implementing the pattern, but realizing which one to use. Walk this decision chain from the most specific signal to the most general:

Which pattern? Ask the most specific question first A chain of yes or no questions. All combinations, permutations or subsets: Backtracking. How many ways, minimum cost or sequential decisions: Dynamic Programming. Range sums or sum equals k with negatives: Prefix Sum plus hash map. Contiguous subarray or substring: Sliding Window. Sorted data with a search or “minimum X such that”: Binary Search. Sorted data with pairs, triplets or an in-place partition: Multiple Pointers. Comparing collections, counting or finding a complement: Frequency Counter. If nothing fits, write the brute force and ask what work is repeated and whether sorting first helps. All combinations /permutations / subsets? Backtracking How many ways? Min cost?Sequential decisions? Dynamic Programming Range sums / sum = kwith negatives? Prefix Sum+ hash map Contiguous subarrayor substring? Sliding Window Sorted data: search or“minimum X such that”? Binary Search Sorted: pairs, triplets,in-place partition? Multiple Pointers Compare collections,count, find a complement? Frequency Counter Brute force first: what workis repeated? Does sorting help? yes no yes no yes no yes no yes no yes no yes no
Which pattern? Ask the most specific question first

And the same signals as a reference table:

If the problem says / has...Try firstTypical complexity
Compare two collections, anagrams, “same frequency”, duplicatesFrequency CounterO(n) / O(n)
“Two numbers that add up to X” with an unsorted arrayFrequency Counter (hash map)O(n) / O(n)
Sorted array + find pairs/tripletsMultiple PointersO(n) / O(1)
Remove duplicates in-place, partitionMultiple Pointers (slow/fast)O(n) / O(1)
Contiguous subarray/substring of size kFixed Sliding WindowO(n) / O(1)
Longest/shortest contiguous subarray/substring that satisfies XVariable Sliding WindowO(n) / O(k)
Range sums, subarrays with sum k and there are negativesPrefix Sum + hash mapO(n) / O(n)
Search in sorted data, “minimum X such that...”Binary SearchO(log n)
Sorting, counting inversions, problem that splits in halvesDivide and ConquerO(n log n)
“How many ways...?”, “minimum cost”, sequential decisionsDynamic ProgrammingPolynomial
Choosing intervals/tasks, “the maximum number of...” with a natural orderGreedy (with justification)O(n log n)
“All combinations/permutations/subsets”BacktrackingExponential

MAKE IT STICK

10. Practice plan

10.1 How to practice each problem

How to practice one problem A loop of five steps: solve out loud with a 25-minute timer; if stuck, take one hint, the pattern name, and 10 more minutes; if you read the solution, rewrite it from memory the next day; log the problem, pattern, mistake and Big O; and solve it again after 3, 7 and 21 days, which loops back to the first step. Solve out loud25-minute timer Stuck? Take one hintthe pattern name, +10 min Read the solution?Rewrite it from memory tomorrow Log itpattern · mistake · Big O Solve it againafter 3, 7 and 21 days spaced repetition
How to practice one problem
  1. Simulate the interview. Read the problem and apply the 5 steps out loud (or in writing), as if there were an interviewer.
  2. Use a timer. 25 minutes for an easy/medium problem. If you didn't get it, look at just one hint (the pattern's name), and take 10 more minutes.
  3. Rewrite from memory. If you read the solution, close it and rewrite it from memory the next day.
  4. Keep a log. Problem, pattern, mistake you made, Big O. Review the log every week.
  5. Space your repetitions. Solve each problem again after 3, 7 and 21 days.

10.2 A two-week plan

The two-week plan at a glance A Gantt chart over 14 days: day 1 method and Big O; days 2 to 3 Frequency Counter; days 4 to 5 Multiple Pointers; days 6 to 8 Sliding Window; days 9 to 10 Divide and Conquer; days 11 to 12 an introduction to DP, Greedy and Backtracking; day 13 an unlabeled mix of problems; day 14 a mock interview. Method + Big O 1 Frequency Counter 2–3 Multiple Pointers 4–5 Sliding Window 6–8 Divide and Conquer 9–10 DP / Greedy / Backtrack 11–12 Unlabeled mix 13 Mock interview 14 1 2 3 4 5 6 7 8 9 10 11 12 13 14 day
The two-week plan at a glance
DaysFocusProblems
1Method + Big ORe-read sections 1–3. Solve charCount and validAnagram without looking.
2–3Frequency CountersameFrequency, areThereDuplicates, LC 1, 242, 49, 383, 387.
4–5Multiple PointerscountUniqueValues, averagePair, isSubsequence, LC 167, 125, 26, 283, 11.
6–8Sliding WindowmaxSubarraySum, minSubArrayLen, findLongestSubstring, LC 643, 209, 3, 424, 567.
9–10Divide and ConquercountZeroes, sortedFrequency, findRotatedIndex, LC 704, 35, 34, 33, 153.
11–12DP / Greedy / Backtracking (intro)LC 70, 322, 53, 435, 78, 46.
13Unlabeled mix5 random problems from the ones above without knowing the pattern beforehand.
14Mock interview2 problems in 45 minutes each, out loud, with someone or recording yourself.

10.3 Resources

  • George Polya, How to Solve It (1945) — the source of the five-step strategy in section 2.
  • Colt Steele, JavaScript Algorithms and Data Structures Masterclass — the course this guide grew out of; the exercises follow its practice set.
  • LeetCode (leetcode.com) — the problem numbers cited in each section. The NeetCode 150 list (neetcode.io) organizes problems by pattern.
  • Gayle Laakmann McDowell, Cracking the Coding Interview — chapters on Big O and interview technique.
  • Big-O Cheat Sheet (bigocheatsheet.com) — complexities of data structures and sorting algorithms.
  • VisuAlgo (visualgo.net) — step-by-step visualizations of searching, sorting and more.

Problem Solving: Approach & Patterns — an interview study guideBack to top ↑