Skip to Content
Course content

227: Implementing Array and String Algorithms in JavaScript

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was reviewing a PR for a teammate who was building a search feature for a massive inventory of product SKUs. He had written a function to find the first non-repeating character in a string—basically a way to identify a specific unique identifier in a batch of codes. His solution was simple: he looped through the string, and for every character, he used indexOf() and lastIndexOf() to see if they matched. On his machine, with a test string of ten characters, it was instantaneous. But when we pushed it to staging with a dataset of 100,000 characters, the browser tab completely locked up. He had accidentally created an $O(n^2)$ time complexity nightmare. He didn't need a faster computer; he needed a better algorithm.

Optimizing Search with Frequency Maps

The most common mistake I see when developers tackle string or array algorithms is relying too heavily on nested loops. When you find yourself putting a search method (like includes, indexOf, or find) inside another loop, a red flag should go up in your head. In the case of the "first unique character" problem, the trick is to trade a little bit of memory for a lot of speed.

Instead of scanning the string over and over, we can use a Map (or a plain JavaScript object) to create a frequency count. We traverse the string once to count every occurrence, and then traverse it a second time to find the first character that has a count of one. This turns a quadratic time operation into a linear one.

function findFirstUniqueChar(text) {
  const charCount = new Map();

  // First pass: Build the frequency map
  for (const char of text) {
    charCount.set(char, (charCount.get(char) || 0) + 1);
  }

  // Second pass: Find the first char with a count of 1
  for (const char of text) {
    if (charCount.get(char) === 1) {
      return char;
    }
  }

  return null; // No unique character found
}

console.log(findFirstUniqueChar("stress")); // "t"

The Power of Two-Pointer Logic

Another pattern that will save you from writing inefficient code is the "two-pointer" technique. I use this constantly when dealing with sorted arrays. Instead of using nested loops to find a pair of elements that meet a certain condition, you place one pointer at the start and one at the end, then move them toward each other based on the result.

Take the "Two Sum" problem on a sorted array. If you're looking for two numbers that add up to a specific target, you don't need to check every possible combination. If the sum of your current pointers is too high, you move the right pointer inward to decrease the sum. If it's too low, you move the left pointer inward to increase it. It's a clean, elegant way to avoid the $O(n^2)$ trap.

function findPairWithSum(sortedArr, target) {
  let left = 0;
  let right = sortedArr.length - 1;

  while (left < right) {
    const currentSum = sortedArr[left] + sortedArr[right];

    if (currentSum === target) {
      return [sortedArr[left], sortedArr[right]];
    } else if (currentSum < target) {
      left++; // Need a larger sum
    } else {
      right--; // Need a smaller sum
    }
  }

  return null;
}

const numbers = [10, 20, 35, 50, 75, 110];
console.log(findPairWithSum(numbers, 90)); // [20, 70] - wait, 20+70 isn't here, [15, 75]... actually [15, 75] isn't here either. Let's use [20, 70] if 70 was there, or [15, 75]. With the array above: [15, 75] is not possible, but [10, 110] is 120. Let's try target 65: [15, 50]... wait, if the array is [10, 20, 35, 50, 75, 110], and target is 85, it returns [10, 75].

I'll be honest: in a small project, the difference between $O(n)$ and $O(n^2)$ might not be noticeable. But as a professional, your goal isn't just to make it "work"—it's to make it scale. When you start thinking in terms of pointers and maps rather than just "loops within loops," you stop writing code that crashes in production.




📋 Practical Task

Implementing a Longest Substring Without Repeating Characters Finder

Your task is to write a function called findLongestUniqueSubstring(str). This function should take a string and return the length of the longest substring that contains no repeating characters.

To do this efficiently, avoid nested loops that restart the search from every index. Instead, try implementing a "sliding window" approach: use two pointers to maintain a window of unique characters, and move the left pointer forward whenever you encounter a character that is already inside your current window.

Requirements:

  • The function must handle empty strings (return 0).
  • It should be case-sensitive (e.g., "A" and "a" are different characters).
  • Aim for a linear time complexity $O(n)$.
Test Cases:
  • findLongestUniqueSubstring("abcabcbb") should return 3 ("abc").
  • findLongestUniqueSubstring("bbbbb") should return 1 ("b").
  • findLongestUniqueSubstring("pwwkew") should return 3 ("wke").
Rating
0 0

There are no comments for now.

to be the first to leave a comment.