Longest Substring Without Repeating Characters
Concept
This is LeetCode #3. It is arguably one of the most frequently asked String questions in software engineering interviews.
Problem: Given a string s, find the length of the longest substring without repeating characters.
s = "abcabcbb" -> Output: 3 (The substring is “abc”).
s = "pwwkew" -> Output: 3 (The substring is “wke”).
Because it asks for the “longest substring” under a specific constraint, this is a textbook Dynamic Sliding Window problem.
Mental Model
String: "abcabcbb"
Leftpointer at 0.Rightpointer at 0. Hash Set is empty.Rightlooks at ‘a’. Not in set. Add ‘a’. Window:[a]. Max Length: 1.Rightlooks at ‘b’. Not in set. Add ‘b’. Window:[a, b]. Max Length: 2.Rightlooks at ‘c’. Not in set. Add ‘c’. Window:[a, b, c]. Max Length: 3.- The Collision:
Rightmoves to ‘a’. Wait, ‘a’ is already in the set! - We must shrink the window from the left until the duplicate ‘a’ is removed.
Leftpointer is currently at ‘a’. Remove it from the set. MoveLeftforward.- The duplicate is gone! The window is now
[b, c].Rightcan now safely add the new ‘a’ to the set. Window is[b, c, a].
Implementation (The Set Approach)
// Time Complexity: O(N) (Left and Right pointers only move forward)
// Space Complexity: O(K) (where K is the size of the character set, e.g., 26)
function lengthOfLongestSubstring(s: string): number {
const charSet = new Set<string>();
let left = 0;
let maxLength = 0;
// Expand the window
for (let right = 0; right < s.length; right++) {
// If we hit a duplicate, shrink from the left until it's gone
while (charSet.has(s[right])) {
charSet.delete(s[left]);
left++;
}
// The window is now clean. Add the new character.
charSet.add(s[right]);
// Update the maximum length
maxLength = Math.max(maxLength, right - left + 1);
}
return maxLength;
}
The Optimization (The Hash Map Approach)
The Set approach works perfectly, but there is a slight inefficiency.
If the string is "abxyzba", when the Right pointer hits the second ‘a’, the Left pointer is sitting at the first ‘a’.
The Left pointer has to slowly chug along: remove ‘a’, remove ‘b’, remove ‘x’, remove ‘y’, remove ‘z’… just to clean out the old ‘a’.
We can optimize this by using a Hash Map that stores the character and its exact index.
If we hit a duplicate, we don’t need to inch the Left pointer forward. We can instantly teleport the Left pointer to previous_index + 1.
function lengthOfLongestSubstringOptimized(s: string): number {
// Map stores { character : the LAST SEEN index of that character }
const charMap = new Map<string, number>();
let left = 0;
let maxLength = 0;
for (let right = 0; right < s.length; right++) {
const char = s[right];
// If we've seen this character BEFORE, and its old position
// is actually inside our current window...
if (charMap.has(char) && charMap.get(char)! >= left) {
// Teleport the Left pointer instantly past the old duplicate
left = charMap.get(char)! + 1;
}
charMap.set(char, right); // Update the map with the new index
maxLength = Math.max(maxLength, right - left + 1);
}
return maxLength;
}
Interview Questions
Q: In the Optimized Hash Map approach, why is the condition charMap.get(char) >= left strictly necessary? Can’t we just teleport the Left pointer if the character exists in the map?
A: Imagine the string is "abba".
Rightis at ‘b’ (index 2). Map has ‘b’ at index 1. We teleportLeftto index 2. Window is"b".Rightmoves to ‘a’ (index 3). Map has ‘a’ at index 0.
If we blindly teleportedLefttoMap.get('a') + 1, we would teleportLeftbackwards to index 1! Our window would instantly include the duplicate ‘b’s again.
The checkcharMap.get(char) >= leftensures that we only care about duplicates that are physically currently inside our active sliding window. If the duplicate ‘a’ is sitting at index 0, and ourLeftpointer is already at index 2, the old ‘a’ is safely outside the window and cannot hurt us.