LeetCode ยท #3

Longest Substring Without Repeating Characters Solution

Given a string s, find the length of the longest substring without repeating characters.

01
Section One ยท Problem

Problem Statement

๐Ÿท๏ธ

Difficulty

Medium

๐Ÿ”—

LeetCode

Problem #3

๐Ÿ—๏ธ

Pattern

Sliding Window + Hash Set

Given a string s, find the length of the longest substring without repeating characters. A substring is a contiguous sequence of characters within the string โ€” unlike a subsequence, it cannot skip characters.

Example 1
Input: s = "abcabcbb" Output: 3 // "abc" is the longest substring without repeating characters
Example 2
Input: s = "bbbbb" Output: 1 // "b" โ€” every character is the same, so max length is 1
Example 3
Input: s = "pwwkew" Output: 3 // "wke" โ€” note that "pwke" is a subsequence, not a substring
Constraints
โ€ข 0 โ‰ค s.length โ‰ค 5 ร— 10โด โ€ข s consists of English letters, digits, symbols, and spaces โ†‘ ASCII characters โ€” HashSet or int[128] both work
02
Section Two ยท Approach 1

Brute Force โ€” O(nยณ)

For every possible starting index, expand rightward checking for duplicates. Use a HashSet to verify each substring has no repeats. Track the maximum valid length.

Why it works

Exhaustively considers every substring. For each start, we extend until a duplicate is found, recording the length. Guaranteed to find the longest.

Why we can do better
Problem: When we find a duplicate at position j, we restart from i+1 and rebuild the set from scratch. This throws away all the work from the previous window. A sliding window reuses the previous state โ€” when a duplicate is found, we shrink the left side instead of starting over.
Java โ€” Brute Force
class Solution { public int lengthOfLongestSubstring(String s) { int max = 0; for (int i = 0; i < s.length(); i++) { HashSet<Character> seen = new HashSet<>(); for (int j = i; j < s.length(); j++) { if (!seen.add(s.charAt(j))) break; max = Math.max(max, j - i + 1); } } return max; } }
Python โ€” Brute Force
class Solution: def lengthOfLongestSubstring(self, s: str) -> int: max_len = 0 for i in range(len(s)): seen = set() for j in range(i, len(s)): if s[j] in seen: break seen.add(s[j]) max_len = max(max_len, j - i + 1) return max_len
MetricValue
TimeO(nยฒ) best case, O(nยณ) worst with set rebuild
SpaceO(min(n, m)) โ€” where m = character set size
03
Section Three ยท Approach 2

Sliding Window + HashSet โ€” O(n)

Maintain a window [left, right] where all characters are unique. Expand right to include a new character. If it's already in the window, shrink from left until the duplicate is removed. Track the maximum window size.

๐Ÿ’ก Mental model: Imagine a caterpillar crawling along a string. The head (right pointer) stretches forward, absorbing one character at a time. If the head encounters a character the body already covers, the tail (left pointer) contracts forward, releasing characters until the duplicate is gone. The longest stretch the caterpillar achieves is your answer.
Algorithm โ€” Expanding/contracting window
  • Step 1: Initialize left = 0, a HashSet, and maxLen = 0.
  • Step 2: For each right from 0 to nโˆ’1:
  • Step 3: While s[right] is in the set โ†’ remove s[left] from set, increment left.
  • Step 4: Add s[right] to the set.
  • Step 5: Update maxLen = max(maxLen, right - left + 1).
๐ŸŽฏ When to recognize this pattern:
  • Any problem asking for the longest/shortest subarray or substring satisfying a condition โ€” think sliding window.
  • The signals are: (1) contiguous elements, (2) a condition that breaks/restores as the window moves, (3) both pointers only move forward.
  • This pattern appears in LC 3, LC 76 (Minimum Window Substring), LC 424 (Longest Repeating Character Replacement), LC 567 (Permutation in String), and LC 209 (Minimum Size Subarray Sum).
HashMap optimization:
  • Instead of shrinking one character at a time, store each character's last seen index in a HashMap.
  • When a duplicate is found, jump left directly to map.get(c) + 1 โ€” skipping intermediate removals.
  • This is still O(n) but with a smaller constant factor.
04
Section Four ยท Trace

Visual Walkthrough

Trace through s = "abcabcbb":

Sliding Window โ€” expand right, contract left on duplicate
STRING s a b c a b c b b 0 1 2 3 4 5 6 7 r=0: add 'a' window=[a] set={a} len=1 r=1: add 'b' window=[a,b] set={a,b} len=2 r=2: add 'c' window=[a,b,c] set={a,b,c} len=3 โ† max! r=3: 'a' is in set! โ†’ shrink left remove s[0]='a', left=1 add 'a' window=[b,c,a] set={b,c,a} len=3 r=4: 'b' is in set! โ†’ shrink left remove s[1]='b', left=2 add 'b' window=[c,a,b] set={c,a,b} len=3 r=5: 'c' is in set! โ†’ shrink left remove s[2]='c', left=3 add 'c' window=[a,b,c] set={a,b,c} len=3 r=6: 'b' is in set! โ†’ shrink left until 'b' removed remove s[3]='a', left=4; remove s[4]='b', left=5 add 'b' window=[c,b] set={c,b} len=2 r=7: 'b' is in set! โ†’ shrink: remove s[5]='c', s[6]='b' add 'b' window=[b] set={b} len=1 Answer: 3 โ€” "abc" โœ“
Notice:
  • Both pointers only move forward. The left pointer advances at most n times total across all iterations.
  • Combined with the right pointer's n steps, total work is O(2n) = O(n).
  • Each character enters and leaves the set exactly once.
05
Section Five ยท Implementation

Code โ€” Java & Python

Java โ€” Sliding Window + HashSet
import java.util.HashSet; class Solution { public int lengthOfLongestSubstring(String s) { HashSet<Character> window = new HashSet<>(); int left = 0, maxLen = 0; for (int right = 0; right < s.length(); right++) { while (window.contains(s.charAt(right))) // shrink until no dup window.remove(s.charAt(left++)); window.add(s.charAt(right)); maxLen = Math.max(maxLen, right - left + 1); } return maxLen; } }
Python โ€” Sliding Window + set
class Solution: def lengthOfLongestSubstring(self, s: str) -> int: window = set() left = max_len = 0 for right in range(len(s)): while s[right] in window: # shrink until no dup window.remove(s[left]) left += 1 window.add(s[right]) max_len = max(max_len, right - left + 1) return max_len
Optimized โ€” HashMap (jump left pointer)
Java โ€” HashMap (skip directly to last occurrence + 1)
import java.util.HashMap; class Solution { public int lengthOfLongestSubstring(String s) { HashMap<Character, Integer> lastSeen = new HashMap<>(); int left = 0, maxLen = 0; for (int right = 0; right < s.length(); right++) { char c = s.charAt(right); if (lastSeen.containsKey(c) && lastSeen.get(c) >= left) left = lastSeen.get(c) + 1; // jump past the duplicate lastSeen.put(c, right); // update last seen index maxLen = Math.max(maxLen, right - left + 1); } return maxLen; } }
Python โ€” dict (skip directly)
class Solution: def lengthOfLongestSubstring(self, s: str) -> int: last_seen = {} left = max_len = 0 for right, c in enumerate(s): if c in last_seen and last_seen[c] >= left: left = last_seen[c] + 1 # jump past duplicate last_seen[c] = right max_len = max(max_len, right - left + 1) return max_len
06
Section Six ยท Analysis

Complexity Analysis

ApproachTimeSpaceTrade-off
Brute Force (all substrings)O(nยฒ) to O(nยณ)O(min(n,m))Check every substring; redundant work
Sliding Window + HashSetO(2n) = O(n)O(min(n,m))Each char enters/leaves set at most once
Sliding Window + HashMap โ† optimal O(n) O(min(n,m)) Jump left pointer; strictly one pass per char
HashSet vs HashMap:
  • Both are O(n) overall. The HashSet version does at most 2n operations (each char enters and leaves).
  • The HashMap version does exactly n operations โ€” it never removes characters, just jumps left past the duplicate's last position.
  • In practice, the difference is small; the HashMap version is a constant-factor improvement.
07
Section Seven ยท Edge Cases

Edge Cases & Pitfalls

CaseInputWhy It Matters
Empty string""Return 0 โ€” no characters, no substring.
Single character"a"Return 1 โ€” the character itself is the longest substring.
All unique"abcdef"Entire string is the answer. Window never shrinks. Length = n.
All same"aaaa"Window is always size 1. Left follows right โ€” every char is a dup of the previous.
Spaces and symbols"a b c"Spaces are valid characters. "a b c" has all 5 unique chars โ†’ length 5.
Duplicate at end"abca"The window shrinks at the last char. Max was 3 from "abc" โ€” not 4.
โš  Common Mistake (HashMap version): Forgetting the condition lastSeen.get(c) >= left. Without it, old entries from before the current window can incorrectly trigger a left-pointer jump. If a character was last seen at index 2 but left is already at 5, that old occurrence is outside the window โ€” ignore it. Always check that the last-seen index is within the current window bounds.

โ† Back to Arrays & Hashing problems