The 5-Step Strategy That Changed How I Solve Any Coding Problem

Learning Problem Solving Patterns In DSA


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

  1. Quick exit: If lengths differ, they can’t be anagrams
  2. Build frequency map: Count each character in the first string
  3. Validate against second string: For each character, check if it exists and decrement the count
  4. 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”

  1. Build lookup from “anagram”: lookup = { a: 3, n: 1, g: 1, r: 1, m: 1 }
  2. 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…
  3. 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:

  1. Place one pointer at the start, another at the end
  2. 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]):

  1. start = -4, end = 5 → sum = 1 (too big) → move end left
  2. start = -4, end = 2 → sum = -2 (too small) → move start right
  3. start = -3, end = 2 → sum = -1 (too small) → move start right
  4. start = -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 value
  • j 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, continue
  • i = 0, j = 2: arr[0] = 1, arr[2] = 2 → different! Move i to 1, set arr[1] = 2
  • i = 1, j = 3: arr[1] = 2, arr[3] = 3 → different! Move i to 2, set arr[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):

  1. Initial sum: 1 + 2 + 5 + 2 = 10
  2. Slide window: Remove 1, add 82 + 5 + 2 + 8 = 17
  3. Slide window: Remove 2, add 15 + 2 + 8 + 1 = 16
  4. Slide window: Remove 5, add 52 + 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:

  1. Frequency Counter: Perfect for comparison problems, anagrams, and duplicate detection
  2. Multiple Pointers: Elegant for sorted arrays, pair finding, and range problems
  3. 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

Leave a Comment

Your email address will not be published. Required fields are marked *