- From Thinking to Actually Solving Problems
- What Exactly Are Problem-Solving Patterns?
- Let's Set Realistic Expectations
- The Pattern Landscape
- The Frequency Counter Pattern: Your First Power Tool
- The Multiple Pointers Pattern: Elegant Search Without the Nested Mess
- The Sliding Window Pattern: Maximum Subarray Magic
- Wrapping Up: Your New Algorithmic Superpowers
Remember that strategic foundation we built in the previous post? Well, now it’s time to roll up our sleeves and get tactical.
From Thinking to Actually Solving Problems
You’ve learned how to think clearly and avoid that dreaded panic when staring at a blank coding editor. That’s huge! But here’s the thing – knowing how to think is just the beginning.
Now we need to build your algorithmic toolbox – a collection of battle-tested patterns that show up in coding problems over and over again. Think of these as your secret weapons.
What Exactly Are Problem-Solving Patterns?
Imagine you’re a carpenter. You don’t reinvent the hammer for every nail, right? Problem-solving patterns are like your coding tools – they’re not copy-paste solutions, but they give you a reliable starting point when you recognize a familiar problem structure.
These patterns are lifesavers when:
- You’re staring at a problem with zero clue where to start
- Your first attempt works but runs slower than molasses
- You keep bumping into similar problems and solving them from scratch every time
- You’re in a coding interview and need to show direction (and confidence!)
Let’s Set Realistic Expectations
Here’s the truth bomb: these patterns won’t magically solve every problem you encounter. In fact, they might only directly apply to about 1 in every 5-10 challenges you face.
But here’s why they’re still incredibly valuable – when they do fit, they can transform a 30-minute head-scratching session into a 5-minute “aha!” moment.
Don’t treat them like magic spells. Think of them as tools in your toolbox. Sometimes you need a hammer, sometimes a screwdriver, and sometimes you need to get creative with a combination of tools.
The Pattern Landscape
Some patterns have fancy academic names that you’ll find in textbooks:
- 🧩 Divide and Conquer (the classic “break it down” approach)
- 🧼 Greedy Algorithms (make the best choice at each step)
- 🔭 Sliding Window (perfect for consecutive elements)
- 🎯 Two Pointers (elegant for sorted arrays)
Others, like the Frequency Counter, don’t have impressive academic titles but are incredibly practical in real-world coding.
Honestly? We care less about the fancy names and more about whether the technique actually helps you solve problems faster.
By the end of this post, you’ll go from “I have absolutely no idea how to approach this” to “Wait… this feels like a sliding window problem.”
Let’s start with one of the most practical patterns that every developer should know:
The Frequency Counter Pattern: Your First Power Tool
This pattern will save you from writing nested loops that make your code slow and your brain hurt.
The core idea: Use a JavaScript object (or Map) to count how often each value appears in your data. This lets you quickly compare collections, detect duplicates, or validate relationships without nested loops.
It’s perfect when you need to:
- Check if two strings are anagrams
- Verify that one array contains the squared values of another
- Compare frequency and contents regardless of order
- Avoid the O(n²) performance trap of nested loops
Problem 1: The Squared Values Challenge
Let’s tackle this classic problem:
Write a function
checkSquares(arrOne, arrTwo)
that returns true if every value in arrOne has its corresponding squared value in arrTwo. The frequencies must match exactly, but order doesn’t matter.
Test cases:
checkSquares([1, 2, 3], [4, 1, 9]) // true
checkSquares([1, 2, 3], [1, 9]) // false (missing 4)
checkSquares([1, 2, 1], [4, 4, 1]) // false (frequency mismatch)
The Beginner’s Approach (Works but Slow)
Most people start with something like this:
function checkSquares(arrOne, arrTwo) {
if (arrOne.length !== arrTwo.length) return false;
for (let i = 0; i < arrOne.length; i++) {
let matchIndex = arrTwo.indexOf(arrOne[i] ** 2);
if (matchIndex === -1) return false;
arrTwo.splice(matchIndex, 1);
}
return true;
}
This works, but there’s a problem: indexOf()
has to scan through the entire array for each element. Inside a loop, that gives us O(n²) time complexity. It’s fine for small arrays, but it’ll crawl with larger datasets.
The Frequency Counter Solution (Much Better)
Instead of searching through arrays repeatedly, let’s count frequencies and compare:
function checkSquares(arrOne, arrTwo) {
if (arrOne.length !== arrTwo.length) return false;
let countMap1 = {};
let countMap2 = {};
// Count frequencies in both arrays
for (let num of arrOne) {
countMap1[num] = (countMap1[num] || 0) + 1;
}
for (let num of arrTwo) {
countMap2[num] = (countMap2[num] || 0) + 1;
}
// Compare frequencies
for (let key in countMap1) {
if (!(key ** 2 in countMap2)) return false;
if (countMap2[key ** 2] !== countMap1[key]) return false;
}
return true;
}
Walking Through an Example
Let’s trace through this with:
let arrOne = [1, 2, 3, 2];
let arrTwo = [9, 1, 4, 4];
After counting frequencies:
countMap1 = { 1: 1, 2: 2, 3: 1 }
countMap2 = { 1: 1, 4: 2, 9: 1 }
Now we check each number in arrOne:
- Does 1² = 1 exist in arrTwo? ✅ Same frequency (1)? ✅
- Does 2² = 4 exist in arrTwo? ✅ Same frequency (2)? ✅
- Does 3² = 9 exist in arrTwo? ✅ Same frequency (1)? ✅
All checks pass – return true!
Why this is better:
- Time Complexity: O(n) instead of O(n²)
- Readable: Clear steps – count, compare, decide
- Versatile: Same technique works for anagrams, duplicate detection, and more
Problem 2: The Anagram Detector
Here’s another classic that’s perfect for the frequency counter pattern:
Write a function
validAnagram(first, second)
that returns true if the two strings are anagrams of each other. An anagram uses the exact same characters with the same frequencies – order doesn’t matter.
Test cases:
validAnagram("", "") // true
validAnagram("anagram", "nagaram") // true
validAnagram("rat", "car") // false
validAnagram("cat", "tac") // true
The Strategy
- Quick exit: If lengths differ, they can’t be anagrams
- Build frequency map: Count each character in the first string
- Validate against second string: For each character, check if it exists and decrement the count
- Success condition: If we make it through without issues, it’s an anagram
The Code
function validAnagram(first, second) {
if (first.length !== second.length) {
return false;
}
const lookup = {};
// Build frequency map for first string
for (let i = 0; i < first.length; i++) {
let letter = first[i];
lookup[letter] ? lookup[letter] += 1 : lookup[letter] = 1;
}
// Validate against second string
for (let i = 0; i < second.length; i++) {
let letter = second[i];
if (!lookup[letter]) {
return false;
} else {
lookup[letter] -= 1;
}
}
return true;
}
Tracing Through “anagram” and “nagaram”
- Build lookup from “anagram”:
lookup = { a: 3, n: 1, g: 1, r: 1, m: 1 }
- Process “nagaram” character by character:
- ‘n’: Found in lookup, decrement →
lookup.n = 0
- ‘a’: Found in lookup, decrement →
lookup.a = 2
- ‘g’: Found in lookup, decrement →
lookup.g = 0
- And so on…
- ‘n’: Found in lookup, decrement →
- Result: All characters validated successfully – return true!
Performance: O(n) time complexity, O(n) space complexity. Much better than nested loops!
The Multiple Pointers Pattern: Elegant Search Without the Nested Mess
If you’ve ever been frustrated by slow nested loops when searching through sorted data, this pattern is about to become your best friend.
What Are Multiple Pointers?
This technique uses two or more pointers (think of them as array indices) that move through your data strategically – either toward each other or in the same direction. The key is avoiding unnecessary comparisons.
Perfect for:
- ✅ Sorted arrays
- ✅ Finding pairs, ranges, or specific conditions
- ✅ Upgrading from O(n²) to O(n) time complexity
Problem 3: Find the Zero-Sum Pair
Write a function
findZeroPair(nums)
that accepts a sorted array of integers and returns the first pair whose sum equals zero. If no such pair exists, return undefined.
Test cases:
findZeroPair([-3, -2, -1, 0, 1, 2, 3]) // [-3, 3]
findZeroPair([-2, 0, 1, 3]) // undefined
findZeroPair([1, 2, 3]) // undefined
The Slow Way (Don’t Do This)
function findZeroPair(nums) {
for (let a = 0; a < nums.length; a++) {
for (let b = a + 1; b < nums.length; b++) {
if (nums[a] + nums[b] === 0) {
return [nums[a], nums[b]];
}
}
}
}
This checks every possible pair – O(n²) time complexity. It works, but it’s inefficient and doesn’t take advantage of the sorted nature of the array.
The Smart Way (Multiple Pointers)
Strategy:
- Place one pointer at the start, another at the end
- While they haven’t met:
- If sum equals zero → 🎉 Found it!
- If sum is too large → move end pointer left
- If sum is too small → move start pointer right
function findZeroPair(nums) {
let start = 0;
let end = nums.length - 1;
while (start < end) {
const sum = nums[start] + nums[end];
if (sum === 0) {
return [nums[start], nums[end]];
} else if (sum > 0) {
end--;
} else {
start++;
}
}
return undefined;
}
Walking Through the Algorithm
Let’s trace through findZeroPair([-4, -3, -2, -1, 0, 1, 2, 5])
:
start = -4, end = 5
→ sum = 1 (too big) → move end leftstart = -4, end = 2
→ sum = -2 (too small) → move start rightstart = -3, end = 2
→ sum = -1 (too small) → move start rightstart = -2, end = 2
→ sum = 0 (perfect!) → return[-2, 2]
Why this works: Since the array is sorted, moving the start pointer right gives us larger values, and moving the end pointer left gives us smaller values. We can confidently eliminate possibilities without checking every combination.
Performance: O(n) time, O(1) space – much better!
Problem 4: Count Unique Values
Here’s another great application where both pointers move in the same direction:
Write a function
countUniqueValues(arr)
that accepts a sorted array and returns the count of unique values.
Test cases:
countUniqueValues([1, 1, 1, 1, 1, 2]) // 2
countUniqueValues([1, 2, 3, 4, 4, 4, 5, 6, 7]) // 7
countUniqueValues([]) // 0
countUniqueValues([-2, -1, -1, 0, 1]) // 4
The Two-Pointer Strategy
Since the array is sorted, duplicates are grouped together. We can use two pointers:
i
tracks the position of the last unique valuej
scans ahead looking for the next different value
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;
}
How It Works
Let’s trace through [1, 1, 2, 3, 3, 4, 5, 5, 6]
:
i = 0, j = 1
:arr[0] = 1, arr[1] = 1
→ same, continuei = 0, j = 2
:arr[0] = 1, arr[2] = 2
→ different! Movei
to 1, setarr[1] = 2
i = 1, j = 3
:arr[1] = 2, arr[3] = 3
→ different! Movei
to 2, setarr[2] = 3
- Continue until we’ve processed all elements…
Final result: i = 5
, so we return 5 + 1 = 6
unique values.
Performance: O(n) time, O(1) space – we’re modifying the array in-place!
The Sliding Window Pattern: Maximum Subarray Magic
When you’re dealing with problems involving contiguous subarrays or substrings, the sliding window technique can dramatically improve performance over brute-force approaches.
Problem 5: Maximum Subarray Sum
Write a function
maxSubarraySum(arr, n)
that finds the maximum sum of n consecutive elements in an array.
Test cases:
maxSubarraySum([1, 2, 5, 2, 8, 1, 5], 2); // 10 (8 + 2)
maxSubarraySum([1, 2, 5, 2, 8, 1, 5], 4); // 17 (2 + 5 + 2 + 8)
maxSubarraySum([], 3); // null
The Brute Force Approach (Slow)
function maxSubarraySum(arr, num) {
if (num > arr.length) return null;
let max = -Infinity;
for (let i = 0; i <= arr.length - num; i++) {
let temp = 0;
for (let j = 0; j < num; j++) {
temp += arr[i + j];
}
if (temp > max) max = temp;
}
return max;
}
This approach recalculates the sum for each possible subarray from scratch. Time complexity: O(n × num).
The Sliding Window Approach (Much Better)
Key insight: Instead of recalculating everything, just slide the window by removing the element that’s leaving and adding the element that’s entering.
function maxSubarraySum(arr, num) {
if (arr.length < num) return null;
let maxSum = 0;
let tempSum = 0;
// Calculate sum of first 'num' elements
for (let i = 0; i < num; i++) {
maxSum += arr[i];
}
tempSum = maxSum;
// Slide the window
for (let i = num; i < arr.length; i++) {
tempSum = tempSum - arr[i - num] + arr[i];
maxSum = Math.max(maxSum, tempSum);
}
return maxSum;
}
Walking Through the Algorithm
Let’s trace maxSubarraySum([1, 2, 5, 2, 8, 1, 5], 4)
:
- Initial sum:
1 + 2 + 5 + 2 = 10
- Slide window: Remove
1
, add8
→2 + 5 + 2 + 8 = 17
- Slide window: Remove
2
, add1
→5 + 2 + 8 + 1 = 16
- Slide window: Remove
5
, add5
→2 + 8 + 1 + 5 = 16
Maximum found: 17
Performance: O(n) time, O(1) space – just one pass through the array!
Wrapping Up: Your New Algorithmic Superpowers
You’ve just learned three fundamental patterns that will transform how you approach coding problems:
- Frequency Counter: Perfect for comparison problems, anagrams, and duplicate detection
- Multiple Pointers: Elegant for sorted arrays, pair finding, and range problems
- Sliding Window: Ideal for contiguous elements, substring problems, and moving calculations
These aren’t just academic concepts – they’re practical tools that will make you a more efficient problem solver. The next time you encounter a coding challenge, pause and ask yourself: “Does this remind me of any patterns I know?”
With practice, you’ll start recognizing these patterns naturally, and what used to be intimidating problems will become approachable puzzles with clear solution paths.
Remember: the goal isn’t to memorize these patterns perfectly, but to understand their core principles so you can adapt them to new situations. Keep practicing, and happy coding!
Thanks for reading!
— Ranjan