Longest Substring Without Repeats
Use hash maps with sliding window to track character occurrences efficiently
Why Sliding Window? Understanding the Problem and the Pattern
Imagine you're scanning a security badge log at a building entrance. Your job is to find the longest uninterrupted streak of unique visitors β no one can enter twice during the streak. Do you re-read the entire log from scratch every time a duplicate appears? Of course not. You'd naturally slide your finger forward, dropping the earliest visitor and picking up the new one. That intuition is exactly what this lesson is about. Grab the free flashcards embedded below to lock in the vocabulary as you go, because the concepts here compound fast β and they appear in interviews at every major tech company.
The Problem: What Are We Actually Solving?
The Longest Substring Without Repeating Characters problem is one of LeetCode's most frequently asked medium-difficulty problems (Problem #3). The definition is deceptively simple:
Given a string
s, find the length of the longest substring that contains only unique characters.
Before writing a single line of code, let's be precise about terminology, because sloppy language leads to sloppy solutions.
- A substring is a contiguous sequence of characters within the original string.
"abc"is a substring of"abcdef", but"ace"is not β that would be a subsequence. - Unique characters means no character appears more than once within that window.
"abc"is valid;"abca"is not. - We want the length of the longest such window, not the substring itself.
A Few Quick Examples
| Input String | Longest Unique Substring | Answer |
|---|---|---|
"abcabcbb" |
"abc" |
3 |
"bbbbb" |
"b" |
1 |
"pwwkew" |
"wke" |
3 |
"" |
"" |
0 |
Notice that in "pwwkew", the answer is "wke" β not "pw" even though both have length 2, and not "kew" which also has length 3 but appears later. Any valid longest substring of maximum length is acceptable.
π‘ Mental Model: Think of your substring as a sliding spotlight on a stage. The spotlight can only illuminate characters that haven't appeared before. Your goal is to find the single widest stretch the spotlight can cover.
Why Brute Force Falls Apart
The most natural first instinct is the brute force approach: check every possible substring, determine whether it contains all unique characters, and track the maximum length found. Let's sketch that out:
## Brute Force Approach β O(nΒ²) or O(nΒ³)
def length_of_longest_substring_brute(s: str) -> int:
n = len(s)
max_len = 0
for i in range(n): # Start of substring
for j in range(i + 1, n + 1): # End of substring
substring = s[i:j]
# Converting to a set removes duplicates;
# if lengths match, all characters are unique
if len(set(substring)) == len(substring):
max_len = max(max_len, len(substring))
return max_len
This works correctly. For a small string like "abcabcbb" it produces the right answer of 3. So what's the problem?
The problem is scale. Let's count the operations:
- The outer loop runs
ntimes. - The inner loop runs up to
ntimes per iteration. - Creating a set from each substring and comparing lengths takes
O(k)time, wherekis the substring length.
In the worst case, this is O(nΒ³) time. Even the optimized version that tracks uniqueness on-the-fly as j advances is still O(nΒ²).
π€ Did you know? If n = 100,000 (a realistic string length in production), an O(nΒ²) algorithm performs 10 billion operations. At 10βΉ operations per second on modern hardware, that's 10 seconds β way beyond the typical 1β2 second LeetCode time limit and completely unacceptable in a real application.
β οΈ Common Mistake β Mistake 1: Assuming brute force is "good enough" for strings. Strings in real systems β URLs, DNA sequences, log files, document contents β routinely reach millions of characters. O(nΒ²) is a silent performance killer.
β Wrong thinking: "My brute force passed the small test cases, so the algorithm is fine." β Correct thinking: "I need an approach that scales linearly, O(n), with the input size."
The Sliding Window Insight
Here's the key question that unlocks everything: do we really need to re-examine characters we've already seen?
When our window s[i..j] encounters a duplicate at position j, we know:
- Every substring starting before
iand ending atjwill also contain that duplicate (since it contains everything our window contains, and more). - Every substring starting after the first occurrence of the duplicate and ending before
jwas already examined.
This means we can contract the window from the left to eliminate the duplicate, rather than starting over from scratch. This is the sliding window technique.
π― Key Principle: A sliding window maintains two pointers β a left pointer (left) and a right pointer (right) β that define the current window boundary. The right pointer expands the window by moving forward. The left pointer contracts the window when a constraint is violated. Together, each pointer moves at most n steps, giving us O(n) total time.
Here's the conceptual skeleton in pseudocode before we write real code:
left = 0
max_length = 0
character_set = empty set
for right from 0 to n-1:
while s[right] is already in character_set:
remove s[left] from character_set
move left one step right
add s[right] to character_set
max_length = max(max_length, right - left + 1)
return max_length
Notice how the structure mirrors our badge-log intuition: the right hand reaches forward grabbing new characters; the left hand releases old ones only when forced.
Why a Hash Set (or Hash Map) Is the Perfect Partner
The sliding window needs to answer one question extremely quickly: "Is this character already inside our window?" The data structure we choose to track window contents determines whether the answer takes O(1) or O(n) time.
- π Array/list: Checking membership is O(n) β we'd scan every element. Kills our O(n) goal.
- π§ Sorted structure (BST/heap): O(log n) lookup β better, but adds complexity and isn't needed.
- π― Hash Set: O(1) average-case lookup, insertion, and deletion. Perfect match.
- π§ Hash Map (dictionary): O(1) as well, but stores a value alongside each key β for example, the last seen index of each character. This allows smarter left-pointer jumps (covered in Section 2).
For the basic solution, a hash set is sufficient and elegant. For the optimized solution, a hash map that stores character indices allows the left pointer to jump directly past the duplicate rather than crawling character by character.
π‘ Pro Tip: In Python, set is backed by a hash table. In JavaScript, Set works identically. Both give O(1) has(), add(), and delete() β the three exact operations our algorithm needs.
Visual Walkthrough: Tracing "abcabcbb"
Let's cement the intuition by walking through the string "abcabcbb" step by step. We'll track left, right, the current window, our set contents, and the running maximum.
String: a b c a b c b b
Index: 0 1 2 3 4 5 6 7
Step-by-Step Trace
Step 1: right=0, char='a'
Set: {} β Add 'a' β Set: {a}
Window: [a] length=1 max=1
left=0 right=0
Step 2: right=1, char='b'
Set: {a} β Add 'b' β Set: {a,b}
Window: [a,b] length=2 max=2
left=0 right=1
Step 3: right=2, char='c'
Set: {a,b} β Add 'c' β Set: {a,b,c}
Window: [a,b,c] length=3 max=3
left=0 right=2
Step 4: right=3, char='a' β DUPLICATE FOUND
'a' is in {a,b,c}!
Remove s[left]='a', left moves to 1
Set: {b,c} β 'a' no longer in set
Add 'a' β Set: {b,c,a}
Window: [b,c,a] length=3 max=3
left=1 right=3
Step 5: right=4, char='b' β DUPLICATE FOUND
'b' is in {b,c,a}!
Remove s[left]='b', left moves to 2
Set: {c,a} β Add 'b' β Set: {c,a,b}
Window: [c,a,b] length=3 max=3
left=2 right=4
Step 6: right=5, char='c' β DUPLICATE FOUND
'c' is in {c,a,b}!
Remove s[left]='c', left moves to 3
Set: {a,b} β Add 'c' β Set: {a,b,c}
Window: [a,b,c] length=3 max=3
left=3 right=5
Step 7: right=6, char='b' β DUPLICATE FOUND
'b' is in {a,b,c}!
Remove s[left]='a', left=4
Set: {b,c} β 'b' still in set!
Remove s[left]='b', left=5
Set: {c} β Add 'b' β Set: {c,b}
Window: [c,b] length=2 max=3
left=5 right=6
Step 8: right=7, char='b' β DUPLICATE FOUND
'b' is in {c,b}!
Remove s[left]='c', left=6
Set: {b} β 'b' still in set!
Remove s[left]='b', left=7
Set: {} β Add 'b' β Set: {b}
Window: [b] length=1 max=3
left=7 right=7
Final Answer: 3
Ascii diagram of the window positions:
a b c a b c b b
[= = =] max=3 (steps 1-3)
[= = =] max=3 (step 4)
[= = =] max=3 (step 5)
[= = =] max=3 (step 6)
[= =] max=3 (step 7)
[=] max=3 (step 8)
π§ Mnemonic: Think "Expand Right, Contract Left" β the right pointer is always hungry, always moving forward. The left pointer is the bouncer, kicking out characters only when rules are broken.
Why This Matters Beyond LeetCode
The sliding window pattern isn't just an interview trick β it solves a whole family of real-world problems:
- π Network packet inspection: Finding the longest burst of unique request types before a repeat indicates a potential anomaly.
- π Text analysis: Identifying spans of diverse vocabulary in natural language processing.
- π§ DNA sequencing: Locating the longest unique nucleotide sequence in a genome string.
- π― Rate limiting: Tracking distinct events within a moving time window.
Every time you see a problem asking for a property of a contiguous subrange that must satisfy some constraint, your first thought should be: can a sliding window solve this?
π Quick Reference Card:
| π Brute Force | π― Sliding Window | |
|---|---|---|
| β± Time Complexity | O(nΒ²) or O(nΒ³) | O(n) |
| π§ Space Complexity | O(n) for substrings | O(min(n, charset)) |
| π§ Key Structure | None / set per substring | One shared hash set/map |
| π Re-examines chars? | Yes, repeatedly | Never |
| π― Scales to 10βΆ chars? | β No | β Yes |
Now that you understand why the sliding window works and what it's doing at each step, Section 2 will take you inside the actual implementation β handling edge cases, writing clean code in Python and JavaScript, and understanding exactly why the time complexity is O(n) even though the left pointer seems to loop inside the right pointer's loop.
Implementing the Solution: Code, Edge Cases, and Complexity
With the intuition for sliding window firmly in place, it's time to translate that mental model into working code. The implementation is elegant precisely because the algorithm mirrors the logical steps so closely: maintain a window, expand it greedily, and contract it the instant a duplicate appears. Let's build this up line by line.
The Core Data Structure: Hash Map as a Character Registry
The heart of the efficient solution is a hash map (a Python dict or JavaScript Map) that stores each character alongside the most recent index where it was seen. This is the key insight that separates an O(n) solution from slower approaches β instead of scanning the window to find a duplicate, you look it up in O(1) constant time.
character β most recent index
'a' β 0
'b' β 1
'c' β 2
When the right pointer encounters a character already in the map, you instantly know exactly where the duplicate lives and can jump the left pointer past it.
Python Implementation
Here is a clean, production-quality Python solution with inline comments explaining every decision:
def length_of_longest_substring(s: str) -> int:
# Maps each character to its most recently seen index
char_index = {}
max_len = 0
left = 0 # Left boundary of the sliding window
for right in range(len(s)):
char = s[right]
# If the character is in the map AND its last index is inside
# the current window, jump the left pointer forward
if char in char_index and char_index[char] >= left:
left = char_index[char] + 1
# Always update the map with the latest index for this character
char_index[char] = right
# Update the maximum window length seen so far
max_len = max(max_len, right - left + 1)
return max_len
Let's trace through s = "abcabc" to see every step:
right=0 char='a' window=[0,0] char_index={'a':0} max_len=1
right=1 char='b' window=[0,1] char_index={'a':0,'b':1} max_len=2
right=2 char='c' window=[0,2] char_index={...,'c':2} max_len=3
right=3 char='a' DUPLICATE! last seen at 0, left jumps to 1
window=[1,3] char_index={'a':3,...} max_len=3
right=4 char='b' DUPLICATE! last seen at 1, left jumps to 2
window=[2,4] char_index={...,'b':4} max_len=3
right=5 char='c' DUPLICATE! last seen at 2, left jumps to 3
window=[3,5] char_index={...,'c':5} max_len=3
Result: 3 β ("abc")
JavaScript Implementation
The logic is identical in JavaScript, using a Map object for the character registry:
function lengthOfLongestSubstring(s) {
const charIndex = new Map(); // character β most recent index
let maxLen = 0;
let left = 0;
for (let right = 0; right < s.length; right++) {
const char = s[right];
// Only move left forward if the duplicate is inside the current window
if (charIndex.has(char) && charIndex.get(char) >= left) {
left = charIndex.get(char) + 1;
}
charIndex.set(char, right); // Record the latest index
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}
π‘ Pro Tip: In JavaScript, a plain object {} would also work for the map, but Map is semantically cleaner and avoids potential collisions with prototype properties when characters happen to match built-in property names.
The Critical Pointer-Jump Logic
The single most important line in the entire solution deserves its own discussion:
left = char_index[char] + 1
But wait β why not just write left = char_index[char] + 1 unconditionally? Why do we also need the >= left guard?
β οΈ Common Mistake: Forgetting the >= left guard causes the left pointer to move backwards, which breaks the window entirely.
Consider the string "abba":
After processing 'a','b','b':
char_index = {'a':0, 'b':2}
left = 2 (jumped past the first 'b')
Now right=3, char='a'
char_index['a'] = 0 β this index is BEHIND the current left!
β Wrong: left = 0 + 1 = 1 (moves left BACKWARDS to 1)
β
Correct: 0 < left (2), so we DON'T move left. Window stays valid at [2,3]
The max(left, last_seen_index + 1) pattern (equivalent to the >= left guard) is what keeps the window monotonically expanding β left can only stay still or move forward, never backward.
π― Key Principle: The left pointer is a ratchet. It clicks forward but never reverses. Any duplicate whose last-known position is already to the left of the current window is irrelevant β it's no longer inside the window and poses no threat.
Window: [ 2 ... 3 ]
String: a b b a
Index: 0 1 2 3
'a' last seen at index 0 β outside the window.
No action needed. Window is already duplicate-free.
Handling Edge Cases
Robust code anticipates unusual inputs. Let's walk through three edge cases you should always test.
Empty String
If s = "", the for loop never executes. max_len starts at 0 and is returned immediately. This is correct behavior β the longest substring of an empty string has length zero. Both implementations handle this gracefully without any special-case logic.
Single Character String
For s = "z", the loop runs once: right=0, no duplicate exists, max_len = max(0, 0 - 0 + 1) = 1. Returned correctly.
All Identical Characters
For s = "aaaa", this is where the pointer-jump logic gets the most exercise:
right=0 'a' not in map window=[0,0] char_index={'a':0} max_len=1
right=1 'a' in map, index 0 >= left(0) β left = 1
window=[1,1] char_index={'a':1} max_len=1
right=2 'a' in map, index 1 >= left(1) β left = 2
window=[2,2] char_index={'a':2} max_len=1
right=3 'a' in map, index 2 >= left(2) β left = 3
window=[3,3] char_index={'a':3} max_len=1
Result: 1 β
The window is perpetually one character wide, and max_len correctly stays at 1.
Time and Space Complexity Analysis
Time Complexity: O(n)
The right pointer visits each character exactly once, advancing from index 0 to n-1 in a single pass. The left pointer also only moves forward β it never backtracks β so across the entire execution of the algorithm, left makes at most n total moves combined. Every hash map operation (lookup, insert, update) is O(1). Therefore, the total work done is proportional to n, giving us O(n) time complexity.
π§ Mnemonic: One pass, one pointer pair, one hash map β everything is linear.
Total iterations of right pointer: n
Total moves of left pointer: β€ n (amortized)
Hash map operations per step: O(1)
βββββββββββββββββββββββββββββββββββββββββ
Overall time: O(n)
Space Complexity: O(min(n, m))
The hash map stores at most one entry per unique character currently in the window. The space complexity is therefore bounded by two quantities:
- π n β the length of the string (you can't have more unique characters than total characters)
- π m β the size of the character set (e.g., 26 for lowercase English, 128 for ASCII, 65,536 for Unicode)
So we write O(min(n, m)). In practice, for problems constrained to lowercase ASCII letters, the map holds at most 26 entries β effectively O(1) constant space. For general Unicode strings, the space scales with the input.
π Quick Reference Card:
| π Metric | π Value | π¬ Reasoning |
|---|---|---|
| β±οΈ Time Complexity | O(n) | Single pass, O(1) operations per step |
| ποΈ Space Complexity | O(min(n, m)) | Hash map bounded by string length or alphabet size |
| π Pointer Moves | O(n) total | Left pointer is monotonically non-decreasing |
| π Lookup Cost | O(1) | Hash map provides constant-time access |
π€ Did you know? If the problem guarantees only lowercase English letters, you can replace the hash map with a fixed-size integer array of length 26 (using char - 'a' as the index). This gives the same O(1) lookup with zero hash collision overhead and slightly better cache performance β a common micro-optimization seen in competitive programming.
## Space-optimized variant for lowercase letters only
def length_of_longest_substring_ascii(s: str) -> int:
last_seen = [-1] * 26 # index -1 means "not yet seen"
max_len = 0
left = 0
for right, char in enumerate(s):
idx = ord(char) - ord('a')
if last_seen[idx] >= left:
left = last_seen[idx] + 1
last_seen[idx] = right
max_len = max(max_len, right - left + 1)
return max_len
This version is constrained to lowercase letters but illustrates how the underlying algorithm is unchanged β only the registry data structure differs.
With a solid implementation in hand and the complexity analysis fully understood, you're equipped to recognize this pattern and adapt it under interview pressure. The next section will sharpen your instincts further by examining the most common mistakes coders make on this exact problem and exploring the variations interviewers love to throw as follow-ups.
Common Pitfalls, Variations, and LeetCode Best Practices
You've learned the theory and implemented the solution β now it's time to bulletproof your understanding. Even developers who grasp the sliding window concept intuitively still submit wrong answers on their first attempt. This section dissects the most common failure modes, shows you how to extend your solution to a family of related problems, and gives you a concrete checklist to carry into any future sliding window challenge.
The Three Pitfalls That Cause Wrong Answers
These are not hypothetical β they are the exact bugs that appear most frequently in LeetCode discussion threads and whiteboard interviews. Understanding why they happen is just as important as knowing how to fix them.
Mistake 1: Forgetting max() When Jumping the Left Pointer β οΈ
β οΈ Common Mistake: This is arguably the sneakiest bug in the entire problem. When you store the last-seen index of each character in a hash map, you use that index to jump left forward past the duplicate. But what happens when the duplicate you find was already behind your current window?
Consider the input 'abba':
Index: 0 1 2 3
Char: a b b a
Here is what happens step by step with a broken implementation that skips max():
## β BROKEN β missing max(), causes left to move backwards
def length_of_longest_substring_broken(s):
char_map = {}
left = 0
result = 0
for right in range(len(s)):
if s[right] in char_map:
left = char_map[s[right]] + 1 # Bug: no max() guard!
char_map[s[right]] = right
result = max(result, right - left + 1)
return result
## Trace on 'abba':
## right=0 (a): char_map={a:0}, left=0, window='a', result=1
## right=1 (b): char_map={a:0,b:1}, left=0, window='ab', result=2
## right=2 (b): 'b' found at index 1 β left = 1+1 = 2, window='b', result=2
## right=3 (a): 'a' found at index 0 β left = 0+1 = 1 β MOVES BACKWARDS!
## Window now includes index 1 ('b'), which was already processed!
## result = max(2, 3 - 1 + 1) = 3 β WRONG ANSWER
The fix is a single word: max().
## β
CORRECT β max() prevents left from ever moving backwards
def length_of_longest_substring(s):
char_map = {}
left = 0
result = 0
for right in range(len(s)):
if s[right] in char_map:
left = max(left, char_map[s[right]] + 1) # β
Guard here
char_map[s[right]] = right
result = max(result, right - left + 1)
return result
## Correct trace on 'abba':
## right=3 (a): 'a' found at index 0 β left = max(2, 0+1) = max(2,1) = 2
## Window is s[2:4] = 'ba', result = max(2, 3-2+1) = 2 β CORRECT
π§ Mnemonic: Think of left as a ratchet β it can only click forward, never back. max() is the ratchet mechanism.
β Wrong thinking: "The character is in my map, so its stored index must be inside my current window."
β
Correct thinking: "The character's stored index might be stale (behind left), so I must check before jumping."
Mistake 2: Using a Set Instead of a Map β Accidentally Reverting to O(nΒ²) β οΈ
β οΈ Common Mistake: A set can tell you whether a character is in your window. A map can tell you where it is. These are not the same thing, and conflating them destroys your time complexity.
When developers use a set, they must shrink the window one character at a time by repeatedly moving left forward until the duplicate is evicted. This is linear work per step, which compounds to quadratic overall.
## β SLOW β O(nΒ²) due to inner while loop
def length_of_longest_substring_set(s):
seen = set()
left = 0
result = 0
for right in range(len(s)):
# Must shrink one step at a time β can't jump directly
while s[right] in seen:
seen.remove(s[left]) # O(n) inner loop in worst case
left += 1
seen.add(s[right])
result = max(result, right - left + 1)
return result
## On input 'abcabcabc...' (n chars), the while loop fires O(n) times
## for each right pointer position β total work is O(nΒ²)
The map-based approach eliminates the inner loop entirely by teleporting left directly to the correct position:
Set approach (slow): Map approach (fast):
left βββΊ βββΊ βββΊ right left βββββββββββββΊ right
one step at a time one direct jump
O(k) work per step O(1) work per step
π‘ Pro Tip: If you see a while loop inside your for loop in a sliding window solution, ask yourself: "Can I replace this with a direct index lookup?" For this problem, the answer is always yes.
Mistake 3: Off-by-One Errors in Window Length β οΈ
β οΈ Common Mistake: The window spanning from index left to index right (inclusive) has length right - left + 1, not right - left. This distinction matters every single time you update your result.
Indices: 0 1 2 3 4
Chars: a b c d e
β β
left right
right - left = 4 - 0 = 4 β counts the GAPS between elements
right - left + 1 = 4 - 0 + 1 = 5 β counts the ELEMENTS themselves
π― Key Principle: Whenever you count elements in an inclusive range [left, right], the formula is always right - left + 1. This applies in arrays, strings, and any index-based structure.
π§ Mnemonic: "Elements, not edges." Subtraction counts edges between fence posts; add one to count the fence posts themselves.
Common Follow-Up Variation: At Most K Distinct Characters
Once you've mastered this problem, interviewers frequently escalate with: "Now solve it for at most K distinct characters." This is LeetCode 340 (Longest Substring with At Most K Distinct Characters), and your existing template handles it with minimal modification.
The core idea shifts: instead of allowing zero repeated characters, you now allow up to K distinct characters in the window at any time. The map's new role is to count how many times each character appears in the current window, and you shrink from the left whenever the number of distinct keys exceeds K.
## β
Longest Substring with At Most K Distinct Characters
def length_of_longest_substring_k_distinct(s: str, k: int) -> int:
char_count = {} # Maps character β frequency in current window
left = 0
result = 0
for right in range(len(s)):
# Expand: add the new character to the window
char_count[s[right]] = char_count.get(s[right], 0) + 1
# Contract: shrink until we have at most k distinct chars
while len(char_count) > k:
char_count[s[left]] -= 1
if char_count[s[left]] == 0:
del char_count[s[left]] # Remove fully evicted characters
left += 1
# Record the best valid window seen so far
result = max(result, right - left + 1)
return result
## Example: s = 'eceba', k = 2
## Window expands to 'ece' (2 distinct: e, c) β length 3
## 'b' makes 3 distinct β shrink until only 2 remain β 'eb'
## Final answer: 3
Notice the structural parallel to the original solution:
Original problem: K-Distinct variation:
ββββββββββββββββββββββββββββββββββββββββββββββββββ
Constraint: 0 repeats Constraint: β€ K distinct
Data structure: index map Data structure: freq map
Shrink condition: duplicate Shrink condition: len > k
Pointer movement: direct Pointer movement: one step
π‘ Mental Model: Think of your sliding window as a budget. The original problem gives you a budget of zero duplicates. The K-distinct problem gives you a budget of K character types. The template is identical; only the budget constraint changes.
π€ Did you know? The K-distinct variation itself is a stepping stone to even harder problems like Minimum Window Substring (LeetCode 76) and Fruit Into Baskets (LeetCode 904 β which is literally K=2 distinct). Once you own the template, you own the entire problem family.
Key Takeaway Checklist
Before you submit any sliding window solution, run through this mental checklist:
π― Recognize the expand-then-contract shape. If a problem asks for the longest/shortest subarray or substring satisfying some constraint, sliding window is your first instinct. The window grows from the right and only shrinks from the left.
π§ Always pair two pointers with a fast-lookup structure. Two pointers alone tell you where your window is. A hash map or frequency counter tells you what is inside it. You need both halves to achieve O(n) time.
π Validate against edge cases before submitting:
- π§ Empty string
""β expect0 - π Single character
"a"β expect1 - π§ All same characters
"aaaa"β expect1 - π― All unique characters
"abcd"β expectn - π The
'abba'trap β expect2, not3
Summary
You began this lesson not knowing why brute force fails. You now understand that the sliding window pattern eliminates redundant recalculation by maintaining a live, valid window that only ever moves forward β achieving O(n) time that a nested-loop approach can never match.
π Quick Reference Card:
| π§ Topic | β Wrong Approach | β Correct Approach |
|---|---|---|
| π Left pointer update | left = map[char] + 1 |
left = max(left, map[char] + 1) |
| π Lookup structure | Set (no index info) | Hash map (stores last index) |
| π§ Window length | right - left |
right - left + 1 |
| π― Shrink trigger | While loop step-by-step | Direct jump via stored index |
| π§ K-distinct extension | New algorithm | Same template, new constraint |
β οΈ The single most impactful habit you can build is testing your solution on 'abba' before submitting. This four-character string exposes the max() bug, validates your window length formula, and confirms your left pointer never regresses β all at once.
Practical next steps:
- π Solve LeetCode 340 (Longest Substring with At Most K Distinct Characters) using the extended template above β no new concepts required.
- π§ Attempt LeetCode 76 (Minimum Window Substring) to see the same two-pointer + frequency map pattern applied to a minimization problem instead of maximization.
- π― Revisit LeetCode 3 (this problem) and time yourself implementing it from memory in under five minutes β that's the signal that the pattern is truly internalized.
The sliding window is not a trick β it is a transferable mental model. Every time you see "contiguous subarray" or "substring" paired with an optimization target, you now have a reliable first tool to reach for.