← Back to DevBytes

Solving Longest Substring Without Repeating in JavaScript: Step-by-Step Guide

Introduction to the Longest Substring Without Repeating Characters Problem

The "Longest Substring Without Repeating Characters" is one of the most classic algorithmic problems you will encounter in coding interviews and competitive programming. Given a string, the goal is to find the length of the longest contiguous substring that contains no duplicate characters. While the problem statement is deceptively simple, solving it efficiently requires a solid understanding of the sliding window technique and hash-based lookups.

In this tutorial, we will walk through the problem from brute force to optimal solution, explain the underlying mechanics, and discuss best practices for writing clean, performant JavaScript code.

What Is the Longest Substring Without Repeating Characters?

A substring is a contiguous sequence of characters within a string. The problem asks you to identify the longest such sequence where every character appears at most once. For example:

Notice that the answer is the length, not the substring itself, although returning the substring is a common variation.

Why This Problem Matters

This problem is a staple in technical interviews at companies like Google, Amazon, and Microsoft because it tests several fundamental skills at once:

Mastering this problem gives you a reusable mental model for solving dozens of similar problems, such as "Longest Substring with At Most K Distinct Characters" or "Minimum Window Substring."

Approach 1: Brute Force

The most intuitive approach is to generate every possible substring and check whether it contains duplicate characters. While easy to understand, this approach is inefficient.

Algorithm

Code Example

function lengthOfLongestSubstringBruteForce(s) {
  let maxLen = 0;

  for (let i = 0; i < s.length; i++) {
    const seen = new Set();
    for (let j = i; j < s.length; j++) {
      if (seen.has(s[j])) {
        break;
      }
      seen.add(s[j]);
      maxLen = Math.max(maxLen, j - i + 1);
    }
  }

  return maxLen;
}

console.log(lengthOfLongestSubstringBruteForce("abcabcbb")); // 3
console.log(lengthOfLongestSubstringBruteForce("bbbbb"));    // 1
console.log(lengthOfLongestSubstringBruteForce("pwwkew"));   // 3

Complexity Analysis

The brute force approach has a time complexity of O(n²) in the average case, because we examine every pair of indices. In the worst case, where all characters are unique, the inner set operations push the complexity toward O(n³). The space complexity is O(min(n, m)), where m is the size of the character set.

This is acceptable for very short strings but will not scale to inputs of thousands of characters.

Approach 2: Sliding Window with a Set

The sliding window technique improves on brute force by avoiding redundant checks. Instead of restarting from every index, we maintain a window [left, right] that always contains a valid substring with no repeating characters. As we expand the window to the right, we shrink it from the left whenever a duplicate is detected.

Algorithm

Code Example

function lengthOfLongestSubstringSet(s) {
  const seen = new Set();
  let left = 0;
  let maxLen = 0;

  for (let right = 0; right < s.length; right++) {
    while (seen.has(s[right])) {
      seen.delete(s[left]);
      left++;
    }
    seen.add(s[right]);
    maxLen = Math.max(maxLen, right - left + 1);
  }

  return maxLen;
}

console.log(lengthOfLongestSubstringSet("abcabcbb")); // 3
console.log(lengthOfLongestSubstringSet("dvdf"));     // 3

Complexity Analysis

Each character is added to and removed from the set at most once, so the time complexity is O(n). The space complexity is O(min(n, m)) for the set. This is a significant improvement over brute force and is the approach most interviewers expect.

Approach 3: Sliding Window with a Map (Optimal)

The set-based sliding window still has a subtle inefficiency: when a duplicate is found, we shrink the window one character at a time. We can skip directly past the previous occurrence of the duplicate by storing each character's most recent index in a Map. This lets us jump left straight to the position after the earlier occurrence.

Algorithm

Code Example

function lengthOfLongestSubstring(s) {
  const charIndex = new Map();
  let left = 0;
  let maxLen = 0;

  for (let right = 0; right < s.length; right++) {
    const ch = s[right];

    if (charIndex.has(ch) && charIndex.get(ch) >= left) {
      left = charIndex.get(ch) + 1;
    }

    charIndex.set(ch, right);
    maxLen = Math.max(maxLen, right - left + 1);
  }

  return maxLen;
}

console.log(lengthOfLongestSubstring("abcabcbb")); // 3
console.log(lengthOfLongestSubstring("bbbbb"));    // 1
console.log(lengthOfLongestSubstring("pwwkew"));   // 3
console.log(lengthOfLongestSubstring(""));         // 0
console.log(lengthOfLongestSubstring(" "));        // 1

Why This Is Optimal

This version performs exactly one pass over the string, with constant-time map operations. The time complexity is O(n) and the space complexity is O(min(n, m)). The charIndex.get(ch) >= left check is critical: it ensures we only jump left forward when the previous occurrence is actually inside the current window. Without this check, we might incorrectly shrink the window based on a stale index.

Returning the Substring Itself

Sometimes the problem asks for the substring rather than its length. The same sliding window logic applies — we just need to record the start and end indices of the best window.

function longestSubstringWithoutRepeating(s) {
  const charIndex = new Map();
  let left = 0;
  let maxLen = 0;
  let bestStart = 0;

  for (let right = 0; right < s.length; right++) {
    const ch = s[right];

    if (charIndex.has(ch) && charIndex.get(ch) >= left) {
      left = charIndex.get(ch) + 1;
    }

    charIndex.set(ch, right);

    if (right - left + 1 > maxLen) {
      maxLen = right - left + 1;
      bestStart = left;
    }
  }

  return s.slice(bestStart, bestStart + maxLen);
}

console.log(longestSubstringWithoutRepeating("abcabcbb")); // "abc"
console.log(longestSubstringWithoutRepeating("pwwkew"));   // "wke"

Handling Edge Cases

A robust solution must handle several edge cases gracefully:

Unicode-Safe Version

function lengthOfLongestSubstringUnicode(s) {
  const chars = [...s]; // Splits into code points, handling surrogate pairs
  const charIndex = new Map();
  let left = 0;
  let maxLen = 0;

  for (let right = 0; right < chars.length; right++) {
    const ch = chars[right];

    if (charIndex.has(ch) && charIndex.get(ch) >= left) {
      left = charIndex.get(ch) + 1;
    }

    charIndex.set(ch, right);
    maxLen = Math.max(maxLen, right - left + 1);
  }

  return maxLen;
}

console.log(lengthOfLongestSubstringUnicode("😀abc😀")); // 4

Best Practices

1. Choose the Right Data Structure

Use a Map when you need to store indices, and a Set when you only need membership checks. For ASCII-only inputs, a fixed-size array of length 128 can be even faster than a hash map because it avoids hashing overhead.

2. Avoid Off-by-One Errors

The condition charIndex.get(ch) >= left is easy to get wrong. Always test with inputs like "abba", where the window must correctly move forward and never backward.

3. Prefer Early Returns for Trivial Cases

if (!s || s.length === 0) return 0;
if (s.length === 1) return 1;

These guards improve readability and can short-circuit unnecessary work.

4. Write Test Cases

Always validate your solution against a range of inputs:

const testCases = [
  { input: "abcabcbb", expected: 3 },
  { input: "bbbbb", expected: 1 },
  { input: "pwwkew", expected: 3 },
  { input: "", expected: 0 },
  { input: " ", expected: 1 },
  { input: "au", expected: 2 },
  { input: "abba", expected: 2 },
  { input: "dvdf", expected: 3 },
];

for (const { input, expected } of testCases) {
  const result = lengthOfLongestSubstring(input);
  console.assert(result === expected, `Failed for "${input}": got ${result}, expected ${expected}`);
}

5. Document Your Complexity

Always state the time and space complexity in comments or documentation. Interviewers and teammates appreciate clarity about performance characteristics.

Common Mistakes to Avoid

Conclusion

The Longest Substring Without Repeating Characters problem is a perfect showcase for the sliding window pattern. Starting from a brute force O(n²) solution, we refined our approach using a set and then a map to achieve an optimal O(n) algorithm. Along the way, we covered edge cases, Unicode handling, and best practices for writing maintainable JavaScript. The sliding window technique you learned here is a transferable skill — once you internalize the mechanics of expanding and contracting a window with hash-based lookups, you will be equipped to tackle a wide family of substring and subarray problems with confidence.

— Ad —

Google AdSense will appear here after approval

← Back to all articles