3. Longest Substring Without Repeating Characters
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.
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) andr
(right boundary) of the sliding window - Variable
ans
to store the maximum length found
Algorithm Steps:
-
Initialize the window: Start with an empty Counter
cnt
, set both the answerans
and left pointerl
to 0. -
Expand the window: Iterate through the string using
enumerate(s)
wherer
is the index (right pointer) andc
is the current character. -
Add character to window: For each character
s[r]
, increment its count in the Counter:cnt[c] += 1
-
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). -
Update maximum length: After ensuring no duplicates exist in the current window
[l, r]
, calculate the window size asr - l + 1
and update the answer:ans = max(ans, r - l + 1)
-
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 EvaluatorExample 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
- Remove s[0]='p':
- 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
- Remove s[2]='w':
- 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
Which of the following is a good use case for backtracking?
Recommended Readings
https assets algo monster cover_photos stack svg Sliding Window Maximum Monotonic Stack We have an array and a sliding window defined by a start index and an end index The sliding window moves from left of the array to right There are always k elements in the window The window
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!