340. Longest Substring with At Most K Distinct Characters
Problem Description
You are given a string s and an integer k. Your task is to find the length of the longest substring within s that contains at most k distinct characters.
A substring is a contiguous sequence of characters within the string. For example, if s = "eceba" and k = 2, you need to find the longest substring that has no more than 2 different characters.
Let's break down what this means:
- You need to examine all possible substrings of the input string
s - Count the number of distinct/unique characters in each substring
- Keep only those substrings where the distinct character count is at most
k(less than or equal tok) - Return the length of the longest valid substring
For instance:
- If
s = "eceba"andk = 2, the substring"ece"has 2 distinct characters ('e' and 'c'), making it valid with length 3 - If
s = "aa"andk = 1, the entire string has only 1 distinct character ('a'), so the answer would be 2
The solution uses a sliding window technique with a hash table to efficiently track the distinct characters in the current window. The window expands by moving the right boundary and contracts from the left when the distinct character count exceeds k. The final answer is calculated as the length of the string minus the final position of the left boundary, which gives us the maximum valid window size encountered.
How We Pick the Algorithm
Why Sliding Window?
This problem maps to Sliding Window through a short path in the full flowchart.
A counter tracks distinct characters and the left pointer slides right until the window holds at most k distinct letters.
Open in FlowchartIntuition
When we need to find the longest substring with a certain property, the sliding window technique often comes to mind. The key insight here is that we want to maintain a window that always satisfies our constraint (at most k distinct characters).
Think about it this way: if we have a valid window with at most k distinct characters, we want to expand it as much as possible to maximize its length. We keep expanding the right boundary by including more characters. However, once we include a character that causes us to exceed k distinct characters, we need to shrink from the left.
The clever part of this solution is recognizing that we don't need to track the maximum length explicitly. Instead, we can maintain a window that grows to the maximum possible size and then slides along the string while maintaining that size (or growing larger if possible).
Here's the key observation: once we find a valid window of a certain size, we never need to consider windows smaller than that size again. So when we need to shrink the window due to having too many distinct characters, we only shrink it by exactly one position from the left. This ensures our window either maintains its current size or grows larger.
By the end of the iteration, the window size (which is len(s) - l where l is the left boundary) represents the maximum valid window we've encountered. The hash table cnt helps us efficiently track how many distinct characters are in our current window, and we remove characters from it when their count drops to zero as we slide the left boundary forward.
This approach is optimal because we visit each character at most twice (once when expanding right, once when shrinking from left), giving us O(n) time complexity.
Solution Approach
Watch the window grow and slide across the string — step through it below, then read the mechanics.
The solution uses a sliding window technique combined with a hash table to efficiently solve this problem. Let's walk through the implementation step by step:
Data Structures Used:
- A hash table
cnt(Counter in Python) to track the frequency of each character in the current window - A variable
lto mark the left boundary of the sliding window
Algorithm Steps:
-
Initialize the window: Start with
l = 0as the left boundary and an empty Countercntto track character frequencies. -
Expand the window: Iterate through each character
cin the strings(this implicitly represents the right boundary of our window):for c in s: cnt[c] += 1Add each character to our frequency counter.
-
Check constraint and shrink if needed: After adding a character, check if we've exceeded
kdistinct characters:if len(cnt) > k: cnt[s[l]] -= 1 if cnt[s[l]] == 0: del cnt[s[l]] l += 1- If
len(cnt) > k, we have too many distinct characters - Decrease the count of the character at position
l(left boundary) - If that character's count becomes 0, remove it from the hash table entirely
- Move the left boundary one position to the right
- If
-
Calculate the result: After processing all characters, the maximum window size is:
return len(s) - lThis works because the window maintains the maximum valid size it has seen. The final window spans from position
lto the end of the string.
Why this works:
- The window never shrinks below the maximum valid size found so far
- When we encounter too many distinct characters, we only shrink by exactly one position
- This ensures we're always maintaining or growing toward the maximum possible window size
- The time complexity is
O(n)since each character is visited at most twice - The space complexity is
O(min(n, σ)), whereσis the alphabet size: the window is allowed to slide while invalid, so the hash table can hold more thank + 1distinct characters (fors = "aaabc"andk = 1the final window"abc"holds 3)
Example Walkthrough
Let's walk through the solution with s = "eceba" and k = 2:
Initial State:
l = 0(left boundary)cnt = {}(empty counter)
Step 1: Process 'e' at index 0
- Add 'e' to counter:
cnt = {'e': 1} - Check:
len(cnt) = 1 ≤ 2✓ (no shrinking needed) - Current window: "e" (from index 0 to 0)
Step 2: Process 'c' at index 1
- Add 'c' to counter:
cnt = {'e': 1, 'c': 1} - Check:
len(cnt) = 2 ≤ 2✓ (no shrinking needed) - Current window: "ec" (from index 0 to 1)
Step 3: Process 'e' at index 2
- Add 'e' to counter:
cnt = {'e': 2, 'c': 1} - Check:
len(cnt) = 2 ≤ 2✓ (no shrinking needed) - Current window: "ece" (from index 0 to 2)
Step 4: Process 'b' at index 3
- Add 'b' to counter:
cnt = {'e': 2, 'c': 1, 'b': 1} - Check:
len(cnt) = 3 > 2✗ (need to shrink!) - Shrink from left:
- Decrease count of
s[0]which is 'e':cnt = {'e': 1, 'c': 1, 'b': 1} - Move
lfrom 0 to 1
- Decrease count of
- Current window: "ceb" (from index 1 to 3)
Step 5: Process 'a' at index 4
- Add 'a' to counter:
cnt = {'e': 1, 'c': 1, 'b': 1, 'a': 1} - Check:
len(cnt) = 4 > 2✗ (need to shrink!) - Shrink from left:
- Decrease count of
s[1]which is 'c':cnt = {'e': 1, 'c': 0, 'b': 1, 'a': 1} - Since
cnt['c'] = 0, delete 'c':cnt = {'e': 1, 'b': 1, 'a': 1} - Move
lfrom 1 to 2
- Decrease count of
- Current window: "eba" (from index 2 to 4)
- The window still has 3 distinct characters, but the code does not shrink again. It only slides: the window keeps length 3, the largest length seen so far, and waits for a later valid window that is longer.
Result Calculation:
- Final
l = 2 - Result =
len(s) - l = 5 - 2 = 3✓
The algorithm found that the longest substring with at most 2 distinct characters has length 3, which corresponds to "ece" (one of the valid substrings).
Solution Implementation
1from collections import Counter
2
3class Solution:
4 def lengthOfLongestSubstringKDistinct(self, s: str, k: int) -> int:
5 # Initialize left pointer for sliding window
6 left = 0
7
8 # Counter to track character frequencies in current window
9 char_count = Counter()
10
11 # Iterate through string with implicit right pointer
12 for char in s:
13 # Add current character to window
14 char_count[char] += 1
15
16 # If window has more than k distinct characters, shrink from left
17 if len(char_count) > k:
18 # Decrease count of leftmost character
19 char_count[s[left]] -= 1
20
21 # Remove character from counter if count reaches 0
22 if char_count[s[left]] == 0:
23 del char_count[s[left]]
24
25 # Move left pointer forward
26 left += 1
27
28 # Return length of longest valid substring (from left to end)
29 return len(s) - left
301class Solution {
2 public int lengthOfLongestSubstringKDistinct(String s, int k) {
3 // Map to store character frequencies in the current window
4 Map<Character, Integer> charFrequency = new HashMap<>();
5
6 // Left pointer of the sliding window
7 int leftPointer = 0;
8
9 // Convert string to char array for easier access
10 char[] characters = s.toCharArray();
11
12 // Iterate through each character with right pointer (implicitly)
13 for (char currentChar : characters) {
14 // Add current character to the window and increment its frequency
15 charFrequency.merge(currentChar, 1, Integer::sum);
16
17 // If we have more than k distinct characters, shrink the window from left
18 if (charFrequency.size() > k) {
19 // Decrement the frequency of the character at left pointer
20 char leftChar = characters[leftPointer];
21 if (charFrequency.merge(leftChar, -1, Integer::sum) == 0) {
22 // Remove the character from map if its frequency becomes 0
23 charFrequency.remove(leftChar);
24 }
25 // Move left pointer to shrink the window
26 leftPointer++;
27 }
28 }
29
30 // The maximum window size is the total length minus the left pointer position
31 // This works because we only move left pointer when necessary to maintain at most k distinct chars
32 return characters.length - leftPointer;
33 }
34}
351class Solution {
2public:
3 int lengthOfLongestSubstringKDistinct(string s, int k) {
4 // Map to store character frequencies in the current window
5 unordered_map<char, int> charCount;
6
7 // Left pointer of the sliding window
8 int left = 0;
9
10 // Iterate through each character in the string (right pointer)
11 for (char& currentChar : s) {
12 // Add current character to the window
13 ++charCount[currentChar];
14
15 // If we have more than k distinct characters, shrink the window from left
16 if (charCount.size() > k) {
17 // Decrease count of the leftmost character
18 if (--charCount[s[left]] == 0) {
19 // Remove character from map if its count becomes 0
20 charCount.erase(s[left]);
21 }
22 // Move left pointer to the right
23 ++left;
24 }
25 }
26
27 // The longest substring length is the remaining window size
28 return s.size() - left;
29 }
30};
311/**
2 * Finds the length of the longest substring that contains at most k distinct characters
3 * @param s - The input string to search within
4 * @param k - The maximum number of distinct characters allowed
5 * @returns The length of the longest valid substring
6 */
7function lengthOfLongestSubstringKDistinct(s: string, k: number): number {
8 // Map to store character frequencies in the current window
9 const charFrequencyMap: Map<string, number> = new Map();
10
11 // Left pointer of the sliding window
12 let leftPointer: number = 0;
13
14 // Iterate through each character using the right pointer (implicit)
15 for (const currentChar of s) {
16 // Add current character to the window and update its frequency
17 charFrequencyMap.set(currentChar, (charFrequencyMap.get(currentChar) ?? 0) + 1);
18
19 // If we have more than k distinct characters, shrink the window from left
20 if (charFrequencyMap.size > k) {
21 // Get the character at the left pointer
22 const leftChar: string = s[leftPointer];
23
24 // Decrease frequency of the leftmost character
25 charFrequencyMap.set(leftChar, charFrequencyMap.get(leftChar)! - 1);
26
27 // Remove character from map if its frequency becomes 0
28 if (charFrequencyMap.get(leftChar) === 0) {
29 charFrequencyMap.delete(leftChar);
30 }
31
32 // Move left pointer to shrink the window
33 leftPointer++;
34 }
35 }
36
37 // Return the length of the valid window (total length - left pointer position)
38 return s.length - leftPointer;
39}
40Time and Space Complexity
Time Complexity: O(n), where n is the length of the string s.
The algorithm uses a single pass through the string with the right pointer (implicit in the for loop). The left pointer l moves at most n times throughout the entire execution. Each character is visited at most twice - once when it enters the window and once when it exits. All operations inside the loop (adding to counter, checking counter size, deleting from counter) take O(1) time when using a hash map. Therefore, the overall time complexity is O(n).
Space Complexity: O(min(n, σ)), where σ is the size of the character set.
The space is used by the Counter. Because the window only slides by one position when it becomes invalid, it can stay invalid for several steps and hold more than k + 1 distinct characters. For s = "aaabc" and k = 1, the final window "abc" holds 3 distinct characters, which is k + 2. The counter can never hold more distinct characters than the window has positions or than the alphabet has letters, which gives the O(min(n, σ)) bound. For lowercase English letters this is constant.
Common Pitfalls
Pitfall 1: Misunderstanding the Window Size Calculation
The Problem:
The given solution returns len(s) - left without ever storing a maximum. Readers who know the standard template often assume this is a bug, because the standard version tracks the maximum explicitly:
# Standard template: also correct, with one extra variable
max_length = 0
for right, char in enumerate(s):
char_count[char] += 1
while len(char_count) > k:
# shrink window
...
max_length = max(max_length, right - left + 1) # Extra tracking
Why This Happens:
The standard template is correct, but the two styles cannot be mixed. The explicit max_length is required whenever the window shrinks with a while loop, and it becomes unnecessary only with the slide-by-one if used here.
The Solution:
The given approach maintains an invariant: the window never shrinks below the maximum size seen so far. When we violate the constraint (too many distinct characters), we only move the left pointer by one position, effectively sliding the entire window forward rather than shrinking it. This means len(s) - left at the end gives us the maximum window size automatically.
Pitfall 2: Incorrectly Handling Character Removal from Counter
The Problem: Forgetting to remove characters with zero count from the counter:
# Incorrect - leaves zero-count entries in counter
if len(char_count) > k:
char_count[s[left]] -= 1
# Missing: del char_count[s[left]] when count is 0
left += 1
Why This Happens:
The counter will still consider characters with count 0 as "distinct" when checking len(char_count), leading to incorrect distinct character counts.
The Solution: Always remove entries with zero count to maintain accurate distinct character tracking:
if char_count[s[left]] == 0: del char_count[s[left]]
Pitfall 3: Combining a While Loop With return len(s) - left
The Problem:
Replacing the if with a while loop but keeping the final return len(s) - left:
# Wrong when combined with return len(s) - left
while len(char_count) > k:
char_count[s[left]] -= 1
if char_count[s[left]] == 0:
del char_count[s[left]]
left += 1
Why This Matters:
The while loop shrinks the window until it is valid again, so at the end the window is just the last valid window, not the longest one. For s = "eceba" and k = 2, the loop ends with left = 3, and len(s) - left returns 2 instead of 3.
The Solution:
Either keep the single if (the window then never shrinks, so its final length is the answer), or use the while loop and track max_length = max(max_length, right - left + 1) after each step.
Ready to land your dream job?
Unlock your dream job with a 5-minute quiz for a personalized study roadmap!
Get My RoadmapGiven a sorted array of integers and an integer called target, find the element that
equals to the target and return its index. Select the correct code that fills the
___ in the given code snippet.
1def binary_search(arr, target):
2 left, right = 0, len(arr) - 1
3 while left ___ right:
4 mid = (left + right) // 2
5 if arr[mid] == target:
6 return mid
7 if arr[mid] < target:
8 ___ = mid + 1
9 else:
10 ___ = mid - 1
11 return -1
121public static int binarySearch(int[] arr, int target) {
2 int left = 0;
3 int right = arr.length - 1;
4
5 while (left ___ right) {
6 int mid = left + (right - left) / 2;
7 if (arr[mid] == target) return mid;
8 if (arr[mid] < target) {
9 ___ = mid + 1;
10 } else {
11 ___ = mid - 1;
12 }
13 }
14 return -1;
15}
161function binarySearch(arr, target) {
2 let left = 0;
3 let right = arr.length - 1;
4
5 while (left ___ right) {
6 let mid = left + Math.trunc((right - left) / 2);
7 if (arr[mid] == target) return mid;
8 if (arr[mid] < target) {
9 ___ = mid + 1;
10 } else {
11 ___ = mid - 1;
12 }
13 }
14 return -1;
15}
16Recommended Readings
https assets algo monster cover_photos stack svg Sliding Window Maximum Monotonic Stack We have an array and a sliding window defined by a start index and an end index The sliding window moves from left of the array to right There are always k elements in the window The window
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion If you prefer videos here's a video that explains recursion in a fun and easy way Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask him
Want a Structured Path to Master System Design Too? Don’t Miss This!