Problem Solving Patterns

You're faced with a tough challenge. What are the steps you can take to make it solvable? This section will focus on coding blueprints or archetypes or patterns that you can keep in your back pocket to help solve some problems.

Here's the patterns we'll talk about:

  1. Frequency counter pattern

  2. Multiple pointers pattern

  3. Sliding Window Pattern

  4. Divide and Conquer Pattern

Frequency Counter Pattern

When you need to compare values, especially how often those values appear, you can collect them in objects/sets using the frequency counter pattern.

Often, the naive solution to these comparisons is O(n^2) because it uses nested loops. Take the following example:

// This function compares arr1 to arr2 to see if arr2 contains the squared versions of all numbers in arr1
const same = (arr1, arr2) => {
  if (arr1.length !== arr2.length) return false; // <= Short circuit for a fast return

  // FIRST LOOP
  for (let i = 0; i < arr1.length; i++) {
    const correctIndex = arr2.indexOf(arr1[i] * arr1[i]); // <= SECOND LOOP
    if (correctIndex === -1) return false;

    arr2.splice(correctIndex, 1);
  }

  return true;
};

same([1, 2, 3], [4, 1, 9]); // <= returns true

Notice how we have a nested loop, making the time complexity quadratic. Here's the frequency counter solution:

Notice that the frequency counter pattern only has a time complexity of O(3n) because there are 3 loops, which can be simplified to O(n).

Multiple Pointers Pattern

The multiple pointers pattern maintains several pointers corresponding to positions/indices in a data structure, moving those pointers from the beginning, from the end, or towards the middle depending on some set conditions.

Let's see a naive vs. multiple pointer solution to a problem:

Sliding Window Pattern

The sliding window pattern is a way to obtain a subset of data by creating a window that you move around and change size depending on certain conditions.

Here's the naive solution to a problem that requires tracking a subset of data:

Here's the sliding window solution:

Note: The key difference between solutions is that the sliding window removes the redundancy of looping through every number and adding them up. You know every number in the middle is shared between iterations. You can just remove the left edge and add to the right edge.

Divide and Conquer Pattern

The divide and conquer pattern is the process of dividing a data set into smaller chunks and repeating some process on those subsets of data. (By dividing the data, it can significantly decrease time complexity.)

Let's take a look at search algorithms quickly to show off the value of divide and conquer.

In the naive solution, we implement linear search, which is O(n) because it loops through the data set once.

In the divide and conquer solution, we perform binary search, which is O(log n). The basic idea is this:

  1. Find the middle point of a data set.

  2. Compare if it's larger, smaller, or equal to the value you're searching for.

    • If it's larger, grab the subset of numbers BEFORE the middle point

    • If it's smaller, grab the subset of numbers AFTER the middle point

    • If it's equal, return its index!

  3. Eventually, you either find the number or you run out of the ability to cut your data set in half, so you return -1.

Note: Binary search is specifically O(log 2 n) because its halving the data set with each attempt (basically the opposite of doubling it with every step, which is 2^x).

Last updated