AlgoMaster Logo

Longest Substring with At Most Two Distinct Characters

mediumFrequencyUpdated September 21, 2026

Understanding the Problem

We need to find the longest contiguous portion of the string that uses no more than two different characters. The substring must be contiguous, meaning we can't skip characters. And "at most two distinct" means a substring with one distinct character (like "aaa") also counts.

For example, in "eceba", the substring "ece" works because it only uses 'e' and 'c'. We could also pick "eb" or "ba", but those are shorter. The moment we extend "ece" to "eceb", we have three distinct characters ('e', 'c', 'b'), which violates the constraint.

We are searching for a contiguous window with a constraint on its content: at most 2 distinct characters. A window that can grow on the right and shrink on the left fits a sliding window, which scans valid substrings without re-examining characters from scratch.

Key Constraints:

  • 1 <= s.length <= 10^5 → With n up to 100,000, an O(n^2) scan would reach up to 10 billion operations, which is too slow. We need a linear or near-linear solution.
  • s consists of English letters → Up to 52 possible characters, though any valid window holds at most 2 distinct ones, so the auxiliary structure tracking them stays tiny.

Approach 1: Brute Force

Intuition

Check every possible substring and find the longest one with at most two distinct characters. For each starting index, extend the substring character by character, tracking which distinct characters appear. The moment a third distinct character shows up, stop and move to the next starting index.

For each starting position, this measures how far the window can extend before it exceeds two distinct characters, then keeps the longest such length.

Algorithm

  1. Initialize a variable maxLen to 0 to track the longest valid substring found.
  2. For each starting index i from 0 to n-1:
    • Create a set to track distinct characters in the current substring.
    • For each ending index j from i to n-1:
      • Add s[j] to the set.
      • If the set size exceeds 2, break out of the inner loop.
      • Otherwise, update maxLen with j - i + 1 if it's larger.
  3. Return maxLen.

Visualization and Code

Loading animation...

For each starting index, the inner loop re-scans the string from scratch, discarding the work done for the previous starting index. The next approach keeps a single window and only advances its left end when the constraint breaks, removing the repeated scanning.

Approach 2: Sliding Window with Hash Map

Intuition

A sliding window maintains two pointers, left and right, that define the current window. We advance right to include more characters, and when the window holds more than 2 distinct characters, we advance left until it is valid again.

To know how many distinct characters the window holds, we keep a hash map from each character to its count inside the window. Adding a character increments its count; removing one (by advancing left) decrements it. When a count reaches zero, we drop that character from the map, so the number of keys equals the number of distinct characters.

Each character enters the window once (when right passes it) and leaves at most once (when left passes it), so both pointers together traverse the string in O(n).

Algorithm

  1. Initialize left = 0, maxLen = 0, and an empty hash map charCount.
  2. Move right from 0 to n-1:
    • Add s[right] to the map (increment its count).
    • While the map has more than 2 keys (meaning more than 2 distinct characters):
      • Decrement the count of s[left] in the map.
      • If that count becomes 0, remove s[left] from the map.
      • Increment left.
    • Update maxLen with right - left + 1 if it's larger.
  3. Return maxLen.

Visualization and Code

Loading animation...

The hash map approach is O(n) but shrinks the window one character at a time. The next approach tracks each character's last occurrence instead of its frequency, which lets left jump past an evicted character in a single step.

Approach 3: Sliding Window with Last Occurrence Tracking

Intuition

Instead of tracking the frequency of each character, track the index of its last occurrence within the window. When a third distinct character appears, find which of the two existing characters has the earliest last occurrence and move left directly past that position.

This replaces the inner while loop with a single jump. The character with the earliest last occurrence is the one whose run ends first inside the window, so every position up to and including its last occurrence belongs to that character and must be dropped to remove it. Jumping left to that index plus one removes exactly that character and keeps the other two.

Both approaches are O(n). This one avoids stepping through the discarded positions one at a time, though it does a small scan over the map's entries on each eviction.

Algorithm

  1. Initialize left = 0, maxLen = 0, and an empty hash map lastSeen (maps character to its most recent index).
  2. Move right from 0 to n-1:
    • Set lastSeen[s[right]] = right.
    • If lastSeen has more than 2 keys:
      • Find the character with the smallest last-seen index.
      • Set left to that index + 1.
      • Remove that character from lastSeen.
    • Update maxLen with right - left + 1 if it's larger.
  3. Return maxLen.

Visualization and Code

Loading animation...