Facebook Pixel

3. Longest Substring Without Repeating Characters

MediumHash TableStringSliding Window
Leetcode Link

Problem Description

Given a string s, you need to find the length of the longest substring that contains no duplicate characters.

A substring is a contiguous sequence of characters within the string. For example, in the string "abcabcbb", some substrings include "abc", "bca", "cab", etc.

The task is to identify the longest possible substring where every character appears at most once (no repeating characters), and return its length.

For instance:

  • If s = "abcabcbb", the longest substring without repeating characters is "abc", which has length 3
  • If s = "bbbbb", the longest substring without repeating characters is "b", which has length 1
  • If s = "pwwkew", the longest substring without repeating characters is "wke", which has length 3

The solution uses a sliding window technique with two pointers (l and r) to maintain a window of non-repeating characters. A counter tracks character occurrences within the current window. When a duplicate is found (count > 1), the left pointer moves right until the duplicate is removed. The maximum window size encountered during this process is the answer.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

When looking for the longest substring without repeating characters, we need to examine different portions of the string. A naive approach would check every possible substring, but this would be inefficient.

The key insight is that once we find a valid substring (with no repeating characters), we can extend it by adding characters to the right as long as they don't create duplicates. When we encounter a duplicate, we don't need to start over completely - we just need to shrink our window from the left until the duplicate is removed.

Think of it like a sliding window that expands and contracts as needed. We keep expanding the window to the right by including new characters. When we hit a duplicate, instead of abandoning the entire window, we contract from the left just enough to eliminate the conflict. This way, we maintain the longest valid window possible at each step.

For example, with string "abcabcbb":

  • Start with window "a", then expand to "ab", then "abc"
  • When we hit the second 'a', we have "abca" which is invalid
  • Instead of restarting, we shrink from left: remove first 'a' to get "bca"
  • Continue this process, always tracking the maximum window size seen

Using a counter to track character frequencies within our window makes it easy to detect duplicates - when any character's count exceeds 1, we know there's a repeat. The two-pointer technique with l (left) and r (right) naturally implements this sliding window, where r - l + 1 gives us the current window size.

This approach ensures we examine each character at most twice (once when expanding right, once when contracting left), making it much more efficient than checking all possible substrings.

Learn more about Sliding Window patterns.

Solution Approach

The solution implements a sliding window approach using two pointers to efficiently find the longest substring without repeating characters.

Data Structures Used:

  • A Counter (hash table) cnt to track the frequency of each character in the current window
  • Two pointers: l (left boundary) and r (right boundary) of the sliding window
  • Variable ans to store the maximum length found

Algorithm Steps:

  1. Initialize the window: Start with an empty Counter cnt, set both the answer ans and left pointer l to 0.

  2. Expand the window: Iterate through the string using enumerate(s) where r is the index (right pointer) and c is the current character.

  3. Add character to window: For each character s[r], increment its count in the Counter: cnt[c] += 1

  4. Handle duplicates: When cnt[c] > 1, it means the current character creates a duplicate in the window. We need to shrink the window from the left:

    while cnt[c] > 1:
        cnt[s[l]] -= 1  # Remove leftmost character from count
        l += 1           # Move left pointer right

    This loop continues until the duplicate is eliminated (when cnt[c] becomes 1).

  5. Update maximum length: After ensuring no duplicates exist in the current window [l, r], calculate the window size as r - l + 1 and update the answer: ans = max(ans, r - l + 1)

  6. Return result: After processing all characters, return ans as the length of the longest substring without repeating characters.

Time Complexity: O(n) where n is the length of the string. Each character is visited at most twice (once by the right pointer and once by the left pointer).

Space Complexity: O(min(m, n)) where m is the size of the character set. The Counter stores at most min(size of character set, n) characters.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the solution with the string s = "pwwkew".

Initial Setup:

  • cnt = {} (empty counter)
  • l = 0 (left pointer)
  • ans = 0 (maximum length)

Step-by-step execution:

Iteration 1: r=0, c='p'

  • Add 'p' to window: cnt = {'p': 1}
  • No duplicate (cnt['p'] = 1)
  • Window is "p" (indices 0 to 0)
  • Update ans: ans = max(0, 0-0+1) = 1

Iteration 2: r=1, c='w'

  • Add 'w' to window: cnt = {'p': 1, 'w': 1}
  • No duplicate (cnt['w'] = 1)
  • Window is "pw" (indices 0 to 1)
  • Update ans: ans = max(1, 1-0+1) = 2

Iteration 3: r=2, c='w'

  • Add 'w' to window: cnt = {'p': 1, 'w': 2}
  • Duplicate detected! (cnt['w'] = 2)
  • Shrink window from left:
    • Remove s[0]='p': cnt = {'p': 0, 'w': 2}, l = 1
    • Remove s[1]='w': cnt = {'p': 0, 'w': 1}, l = 2
    • Now cnt['w'] = 1, stop shrinking
  • Window is "w" (indices 2 to 2)
  • Update ans: ans = max(2, 2-2+1) = 2

Iteration 4: r=3, c='k'

  • Add 'k' to window: cnt = {'p': 0, 'w': 1, 'k': 1}
  • No duplicate (cnt['k'] = 1)
  • Window is "wk" (indices 2 to 3)
  • Update ans: ans = max(2, 3-2+1) = 2

Iteration 5: r=4, c='e'

  • Add 'e' to window: cnt = {'p': 0, 'w': 1, 'k': 1, 'e': 1}
  • No duplicate (cnt['e'] = 1)
  • Window is "wke" (indices 2 to 4)
  • Update ans: ans = max(2, 4-2+1) = 3

Iteration 6: r=5, c='w'

  • Add 'w' to window: cnt = {'p': 0, 'w': 2, 'k': 1, 'e': 1}
  • Duplicate detected! (cnt['w'] = 2)
  • Shrink window from left:
    • Remove s[2]='w': cnt = {'p': 0, 'w': 1, 'k': 1, 'e': 1}, l = 3
    • Now cnt['w'] = 1, stop shrinking
  • Window is "kew" (indices 3 to 5)
  • Update ans: ans = max(3, 5-3+1) = 3

Final Result: The longest substring without repeating characters has length 3 (corresponding to "wke" or "kew").

Solution Implementation

1class Solution:
2    def lengthOfLongestSubstring(self, s: str) -> int:
3        from collections import Counter
4      
5        # Counter to track character frequencies in current window
6        char_count = Counter()
7      
8        # Initialize result and left pointer of sliding window
9        max_length = 0
10        left = 0
11      
12        # Iterate through string with right pointer
13        for right, char in enumerate(s):
14            # Add current character to window
15            char_count[char] += 1
16          
17            # Shrink window from left while duplicate exists
18            while char_count[char] > 1:
19                char_count[s[left]] -= 1
20                left += 1
21          
22            # Update maximum length of substring without repeating characters
23            max_length = max(max_length, right - left + 1)
24      
25        return max_length
26
1class Solution {
2    public int lengthOfLongestSubstring(String s) {
3        // Array to track character frequency in current window (ASCII characters)
4        int[] charFrequency = new int[128];
5      
6        // Initialize variables
7        int maxLength = 0;
8        int stringLength = s.length();
9        int left = 0;
10      
11        // Iterate through string with right pointer
12        for (int right = 0; right < stringLength; right++) {
13            // Get current character at right pointer
14            char currentChar = s.charAt(right);
15          
16            // Increment frequency of current character
17            charFrequency[currentChar]++;
18          
19            // Shrink window from left while we have duplicate characters
20            while (charFrequency[currentChar] > 1) {
21                // Decrement frequency of character at left pointer and move left pointer
22                charFrequency[s.charAt(left)]--;
23                left++;
24            }
25          
26            // Update maximum length of substring without repeating characters
27            maxLength = Math.max(maxLength, right - left + 1);
28        }
29      
30        return maxLength;
31    }
32}
33
1class Solution {
2public:
3    int lengthOfLongestSubstring(string s) {
4        // Array to track character frequency in current window (ASCII characters)
5        int charFrequency[128] = {0};
6      
7        // Initialize result and string length
8        int maxLength = 0;
9        int stringLength = s.size();
10      
11        // Sliding window approach with two pointers
12        int left = 0;
13        for (int right = 0; right < stringLength; ++right) {
14            // Add current character to window
15            ++charFrequency[s[right]];
16          
17            // Shrink window from left while duplicate exists
18            while (charFrequency[s[right]] > 1) {
19                // Remove leftmost character and move left pointer
20                --charFrequency[s[left]];
21                ++left;
22            }
23          
24            // Update maximum length found so far
25            // Current window size is (right - left + 1)
26            maxLength = max(maxLength, right - left + 1);
27        }
28      
29        return maxLength;
30    }
31};
32
1/**
2 * Finds the length of the longest substring without repeating characters
3 * @param s - The input string to search for the longest substring
4 * @returns The length of the longest substring without repeating characters
5 */
6function lengthOfLongestSubstring(s: string): number {
7    // Variable to store the maximum length found
8    let maxLength: number = 0;
9  
10    // Map to track character frequencies in the current window
11    const charFrequencyMap: Map<string, number> = new Map<string, number>();
12  
13    // Get the length of the input string
14    const stringLength: number = s.length;
15  
16    // Use sliding window technique with left and right pointers
17    let leftPointer: number = 0;
18  
19    for (let rightPointer: number = 0; rightPointer < stringLength; rightPointer++) {
20        // Get the character at the right pointer
21        const currentChar: string = s[rightPointer];
22      
23        // Add or increment the frequency of the current character
24        charFrequencyMap.set(currentChar, (charFrequencyMap.get(currentChar) || 0) + 1);
25      
26        // Shrink the window from left while we have duplicate characters
27        while (charFrequencyMap.get(currentChar)! > 1) {
28            const leftChar: string = s[leftPointer];
29          
30            // Decrement the frequency of the leftmost character
31            charFrequencyMap.set(leftChar, charFrequencyMap.get(leftChar)! - 1);
32          
33            // Move the left pointer to the right
34            leftPointer++;
35        }
36      
37        // Update the maximum length if current window is larger
38        const currentWindowSize: number = rightPointer - leftPointer + 1;
39        maxLength = Math.max(maxLength, currentWindowSize);
40    }
41  
42    return maxLength;
43}
44

Time and Space Complexity

Time Complexity: O(n), where n is the length of the string s.

The algorithm uses a sliding window approach with two pointers (l and r). The right pointer r iterates through each character in the string exactly once via the enumerate loop. The left pointer l also moves through the string, but each character is visited at most twice - once when the right pointer includes it in the window and at most once when the left pointer removes it from the window. Since each character is processed a constant number of times, the overall time complexity is linear.

Space Complexity: O(min(n, |Σ|)) or more precisely O(|Σ|), where |Σ| represents the size of the character set.

The Counter dictionary cnt stores the frequency of characters in the current window. In the worst case, the dictionary can contain all unique characters from the string. For ASCII characters, |Σ| = 128 (or 256 for extended ASCII). For Unicode characters, this could be larger. However, the space used is bounded by both the size of the character set and the length of the string, hence O(min(n, |Σ|)). Since the character set size is typically considered constant (like 128 for ASCII), the space complexity is effectively O(1) or O(|Σ|) where |Σ| = 128 for standard ASCII characters.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Forgetting to Handle Empty String

Issue: The code doesn't explicitly check for empty strings. While the current implementation handles it correctly (returns 0), not being aware of edge cases can lead to issues in modified versions.

Example:

s = ""  # Should return 0

Solution: While the current code handles this implicitly, you can add an explicit check for clarity:

if not s:
    return 0

Pitfall 2: Inefficient Character Removal from Counter

Issue: Using Counter with decrement operations doesn't automatically remove keys with zero count, potentially leading to memory inefficiency with very long strings.

Example:

# After cnt[s[left]] -= 1, if count becomes 0, the key still exists in Counter
# This can accumulate unnecessary keys over time

Solution: Clean up zero-count entries:

while char_count[char] > 1:
    char_count[s[left]] -= 1
    if char_count[s[left]] == 0:
        del char_count[s[left]]  # Remove key when count reaches 0
    left += 1

Pitfall 3: Using Regular Dictionary Instead of Counter

Issue: Attempting to increment a non-existent key in a regular dictionary causes KeyError.

Wrong approach:

char_count = {}  # Regular dict instead of Counter
char_count[char] += 1  # KeyError if char not in dict

Solution: Either use Counter or handle dictionary initialization:

# Option 1: Use Counter (recommended)
char_count = Counter()

# Option 2: Use regular dict with get()
char_count = {}
char_count[char] = char_count.get(char, 0) + 1

Pitfall 4: Off-by-One Error in Window Size Calculation

Issue: Incorrectly calculating window size as right - left instead of right - left + 1.

Wrong calculation:

max_length = max(max_length, right - left)  # Missing +1

Solution: Always remember that for a window from index left to right (inclusive), the size is:

window_size = right - left + 1

Pitfall 5: Not Understanding When to Update Maximum Length

Issue: Updating the maximum length before ensuring the window is valid (no duplicates).

Wrong sequence:

for right, char in enumerate(s):
    char_count[char] += 1
    max_length = max(max_length, right - left + 1)  # Wrong: updating before removing duplicates
    while char_count[char] > 1:
        char_count[s[left]] -= 1
        left += 1

Solution: Always update the maximum length after ensuring the window contains no duplicates:

for right, char in enumerate(s):
    char_count[char] += 1
    while char_count[char] > 1:  # First: ensure no duplicates
        char_count[s[left]] -= 1
        left += 1
    max_length = max(max_length, right - left + 1)  # Then: update maximum
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following is a good use case for backtracking?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More