How to Explain Longest Substring Without Repeating Characters in a Coding Interview (Step-by-Step)

How to Explain Longest Substring Without Repeating Characters in a Coding Interview (Step-by-Step)

This problem is the canonical introduction to the sliding window pattern. The brute-force answer is obvious; the O(n) insight is not. The key is understanding why you never need to shrink the window one character at a time — and how a HashMap lets you jump the left pointer directly to the right position.

In this post, you’ll learn exactly how to walk an interviewer through LeetCode 3: the sliding window setup, the HashMap character-index trick, the left-pointer jump optimisation, and a clean complexity argument.


Problem Statement

LeetCode 3 Medium

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

Note: A substring is a contiguous sequence of characters — not a subsequence.

Examples

Example 1
Input: s = “abcabcbb”
Output: 3
Explanation: “abc” is the longest substring without repeating characters.
Example 2
Input: s = “bbbbb”
Output: 1
Explanation: “b” is the longest such substring.
Example 3
Input: s = “pwwkew”
Output: 3
Explanation: “wke” is the longest — note “pwke” is a subsequence, not a substring.

Constraints

  • 0 <= s.length <= 5 * 104
  • s consists of English letters, digits, symbols, and spaces

What Is the Problem?

“I need to find the length of the longest contiguous chunk of the string where every character appears exactly once. I’m not rearranging anything — the characters must come from a consecutive run within the original string. And I need to call out that ‘substring’ means contiguous, not subsequence.”

Explicitly distinguishing substring from subsequence is a small but powerful signal — it tells the interviewer you’ve read the constraint carefully and won’t accidentally solve the wrong problem.


Step 1: Compare Approaches Before Jumping to Code

✗ Too Slow — O(n³)
Brute Force
Generate every substring, check each for duplicates with a set. O(n²) substrings × O(n) check = O(n³). Fails for large inputs.
~ Better — O(n)
Sliding Window + Set
Expand right; when a duplicate is found, remove from the left one character at a time. O(n) but left pointer can move up to n steps per character.
✓ Optimal — O(n)
Sliding Window + HashMap
Store each character’s last-seen index. On a duplicate, jump the left pointer directly past the previous occurrence — no one-at-a-time shrinking.
“Both the set and HashMap approaches are O(n) in terms of big-O, but the HashMap is the cleaner, more elegant solution — it avoids shrinking the window character by character and makes the logic much easier to reason about in an interview.”

The Key Insight — Store Index, Not Just Presence

💡 Core Insight

Maintain a sliding window defined by two pointers: left and right. As right expands through the string, store each character’s most recent index in a HashMap.

When s[right] is already in the map and its stored index is within the current window (i.e., >= left), jump left directly to stored_index + 1. This skips over the duplicate in one step — no loop needed.

After adjusting left, update the map with the new index of s[right] and update the maximum window length.

“The critical guard is checking that the stored index is within the current window before jumping. If the previous occurrence of a character is already to the left of our window, it’s no longer a conflict — we should ignore it and not move left backwards.”

Step 2: Explain the Algorithm in Plain English

1 Initialise the window and HashMap

Set left = 0, max_len = 0, and an empty HashMap char_index that maps each character to its most recently seen index.

2 Expand right through the string

For each index right, look up s[right] in the map. If it exists and its stored index is >= left, a duplicate is inside the window — jump left to char_index[s[right]] + 1.

3 Update the map and max length

Always update char_index[s[right]] = right (overwrite the old index). Then update max_len = max(max_len, right - left + 1).

4 Return the result

After right reaches the end of the string, return max_len. Every character was visited exactly once — total work is O(n).


Sliding Window Visualised — s = “abcabcbb”

Tracing through the string step by step. The window is always the range [left, right]. The HashMap stores each character’s last-seen index.

right=0 → ‘a’ not in map
L
a
b
c
a
b
c
b
b
map: {a:0}  |  window=”a”  |  max=1
right=1→’b’, right=2→’c’ — no conflicts, window grows
L
R
a
b
c
a
b
c
b
b
map: {a:0, b:1, c:2}  |  window=”abc”  |  max=3
right=3 → ‘a’ found in map at index 0 (inside window) → jump left to 1
L
R
a
b
c
a
b
c
b
b
map: {a:3, b:1, c:2}  |  window=”bca”  |  max=3
right=4 → ‘b’ found at index 1 (inside window, left=1) → jump left to 2
L
R
a
b
c
a
b
c
b
b
map: {a:3, b:4, c:2}  |  window=”cab”  |  max=3
right=5 → ‘c’ found at index 2 (inside window, left=2) → jump left to 3
L
R
a
b
c
a
b
c
b
b
map: {a:3, b:4, c:5}  |  window=”abc”  |  max=3
right=6→’b’ (jump left to 5), right=7→’b’ (jump left to 7) — window shrinks to 1
L=R
a
b
c
a
b
c
b
b
window=”b”, length=1  |  final max=3 ✓
Left pointer
Right pointer
Current window
Conflict detected
Outside window
Final position

Step 3: Talk Through the Code

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        char_index = {}   # Maps each character to its most recently seen index
        left    = 0
        max_len = 0

        for right in range(len(s)):
            char = s[right]

            # If char is in the window (stored index >= left), jump left past it
            if char in char_index and char_index[char] >= left:
                left = char_index[char] + 1

            # Always update the character's latest index
            char_index[char] = right

            # Update max window length
            max_len = max(max_len, right - left + 1)

        return max_len

Highlight the guard char_index[char] >= left — this is essential. Without it, a character seen before the current window would incorrectly push left backward (or not forward enough). The map retains old entries but we only act on them if they’re still inside the active window.


The Left-Jump Optimisation — Why It Matters

🚀 Why HashMap over HashSet?

With a HashSet, when a duplicate is found you must remove characters from the left one by one until the duplicate is gone — potentially O(n) work per character in the worst case (even though the amortised total is still O(n)). With a HashMap storing indices, you jump left directly in a single operation — conceptually cleaner and genuinely faster in practice.

“If the interviewer asks ‘could you do this with a set instead?’ — yes, and it’s also O(n) amortised. But the HashMap version is the one worth knowing: it’s cleaner to explain, harder to get wrong, and eliminates an inner while loop entirely.”

Step 4: Highlight Edge Cases

1 Empty string
“If s = "", the for loop never runs, and we return max_len = 0. Correct — the constraint allows length 0.”
2 All identical characters — “bbbbb”
“Every time right advances, it finds ‘b’ in the map with index right - 1, which is always >= left. So left jumps to right each time — the window stays size 1 throughout. Answer: 1.”
3 All unique characters — “abcdef”
“No character is ever found in the map while inside the window. The window expands with every step of right and never shrinks. The answer is the length of the entire string.”
4 Duplicate outside the current window — “tmmzuxt”
“When right reaches the final ‘t’, the map has ‘t’ stored at index 0 — but left has already moved past it. Since 0 < left, we don’t jump. Without the >= left guard this would incorrectly shrink the window.”
5 Single character
“One character, one iteration. The map stores it, max is set to 1, and we return 1. No special casing needed.”

Step 5: State Time and Space Complexity

DimensionComplexityReason
Time O(n) right visits each character exactly once; left only ever moves forward — total work is O(n)
Space O(min(n, m)) The HashMap stores at most one entry per unique character — bounded by both the string length n and the character set size m (e.g., 128 for ASCII)
“Time is O(n) — right only ever moves right, and left only ever moves right. Neither pointer ever backtracks, so every character is touched at most twice total. Space is O(min(n, m)) where m is the size of the character set. For ASCII it’s at most 128 entries — effectively O(1). For arbitrary Unicode it’s O(n) in the worst case.”

Interview Checklist for Longest Substring Without Repeating Characters

  • Restate — “longest contiguous substring with no duplicate characters” — stress contiguous vs subsequence
  • Mention brute force O(n³) → explain why it’s rejected
  • Propose sliding window — define left and right pointers clearly
  • Explain why HashMap (storing indices) beats HashSet (storing presence only)
  • State the key insight — jump left directly past the duplicate’s last known index
  • Explain the char_index[char] >= left guard — critical for stale entries
  • Always update the map entry for s[right] regardless of whether a jump occurred
  • Call out: empty string → 0, all-same → 1, all-unique → n
  • Call out: duplicate outside window — must not move left backward
  • State O(n) time and O(min(n, m)) space — mention the ASCII constant-space case

If you can solve this cleanly, try these next:

💡 The follow-up interviewers love: “What if the string contains Unicode characters?” The answer is the same algorithm — the HashMap still works. The space complexity changes from O(1) / O(128) to O(n) in the worst case, since the character set is unbounded. Knowing this follow-up cold is a strong signal.
Scroll to Top