🧠
Sliding Window with Ordered Dictionary (Maintaining Last Seen Index)
💡 This approach uses an ordered dictionary (or linked hash map) to track the last occurrence of each character, allowing quick removal of the leftmost character when distinct count exceeds k.
Intuition
Keep track of characters and their last indices in insertion order. When distinct characters exceed k, remove the leftmost character by its last seen index to shrink the window.
Algorithm
- Initialize left pointer and max_length to 0.
- Use an ordered dictionary to store characters and their last indices.
- Expand right pointer, update character's last index in dictionary.
- If dictionary size exceeds k, remove the leftmost character and move left pointer to one past its last index.
- Update max_length with current window size.
💡 The ordered dictionary helps efficiently identify and remove the oldest character to maintain the constraint.
from collections import OrderedDict
def length_of_longest_substring_k_distinct(s: str, k: int) -> int:
if k == 0 or not s:
return 0
left = 0
max_length = 0
last_seen = OrderedDict()
for right, char in enumerate(s):
if char in last_seen:
del last_seen[char]
last_seen[char] = right
if len(last_seen) > k:
_, idx = last_seen.popitem(last=False)
left = idx + 1
max_length = max(max_length, right - left + 1)
return max_length
# Driver code
if __name__ == '__main__':
print(length_of_longest_substring_k_distinct('eceba', 2)) # Expected: 3
Line Notes
from collections import OrderedDictImport OrderedDict to maintain insertion order of keys
last_seen = OrderedDict()Stores characters and their last seen indices in order
if char in last_seen: del last_seen[char]Remove char to update its position in order
last_seen[char] = rightInsert char with current index as last seen
if len(last_seen) > kIf distinct chars exceed k, remove oldest
_, idx = last_seen.popitem(last=False)Pop the first inserted character and get its index
left = idx + 1Move left pointer to one past removed character's index
max_length = max(max_length, right - left + 1)Update max length
return max_lengthReturn longest valid substring length
import java.util.*;
public class Solution {
public static int lengthOfLongestSubstringKDistinct(String s, int k) {
if (k == 0 || s.length() == 0) return 0;
LinkedHashMap<Character, Integer> lastSeen = new LinkedHashMap<>();
int left = 0, maxLength = 0;
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
if (lastSeen.containsKey(c)) {
lastSeen.remove(c);
}
lastSeen.put(c, right);
if (lastSeen.size() > k) {
Map.Entry<Character, Integer> firstEntry = lastSeen.entrySet().iterator().next();
int idx = firstEntry.getValue();
lastSeen.remove(firstEntry.getKey());
left = idx + 1;
}
maxLength = Math.max(maxLength, right - left + 1);
}
return maxLength;
}
public static void main(String[] args) {
System.out.println(lengthOfLongestSubstringKDistinct("eceba", 2)); // Expected: 3
}
}
Line Notes
LinkedHashMap<Character, Integer> lastSeen = new LinkedHashMap<>();Maintain insertion order of characters and their last indices
if (lastSeen.containsKey(c)) lastSeen.remove(c);Remove character to update its position
lastSeen.put(c, right);Insert character with current index
if (lastSeen.size() > k)If distinct chars exceed k, remove oldest
Map.Entry<Character, Integer> firstEntry = lastSeen.entrySet().iterator().next();Get oldest character entry
lastSeen.remove(firstEntry.getKey());Remove oldest character
left = idx + 1;Move left pointer past removed character
maxLength = Math.max(maxLength, right - left + 1);Update max length
return maxLength;Return longest valid substring length
#include <iostream>
#include <string>
#include <list>
#include <unordered_map>
using namespace std;
int lengthOfLongestSubstringKDistinct(string s, int k) {
if (k == 0 || s.empty()) return 0;
list<pair<char, int>> lastSeenList;
unordered_map<char, list<pair<char, int>>::iterator> lastSeenMap;
int left = 0, maxLength = 0;
for (int right = 0; right < (int)s.size(); right++) {
char c = s[right];
if (lastSeenMap.find(c) != lastSeenMap.end()) {
lastSeenList.erase(lastSeenMap[c]);
}
lastSeenList.push_back({c, right});
lastSeenMap[c] = prev(lastSeenList.end());
if ((int)lastSeenList.size() > k) {
auto front = lastSeenList.front();
lastSeenMap.erase(front.first);
left = front.second + 1;
lastSeenList.pop_front();
}
maxLength = max(maxLength, right - left + 1);
}
return maxLength;
}
int main() {
cout << lengthOfLongestSubstringKDistinct("eceba", 2) << endl; // Expected: 3
return 0;
}
Line Notes
list<pair<char, int>> lastSeenList;Doubly linked list to maintain order of characters and indices
unordered_map<char, list<pair<char, int>>::iterator> lastSeenMap;Map to quickly find and remove characters from list
if (lastSeenMap.find(c) != lastSeenMap.end()) lastSeenList.erase(lastSeenMap[c]);Remove character to update its position
lastSeenList.push_back({c, right});Add character with current index at end
lastSeenMap[c] = prev(lastSeenList.end());Update map with iterator to new position
if ((int)lastSeenList.size() > k)If distinct chars exceed k, remove oldest
auto front = lastSeenList.front();Get oldest character and index
lastSeenMap.erase(front.first);Remove from map
left = front.second + 1;Move left pointer past removed character
lastSeenList.pop_front();Remove oldest character from list
maxLength = max(maxLength, right - left + 1);Update max length
return maxLength;Return longest valid substring length
function lengthOfLongestSubstringKDistinct(s, k) {
if (k === 0 || s.length === 0) return 0;
const lastSeen = new Map();
let left = 0, maxLength = 0;
for (let right = 0; right < s.length; right++) {
const c = s[right];
if (lastSeen.has(c)) {
lastSeen.delete(c);
}
lastSeen.set(c, right);
if (lastSeen.size > k) {
const firstKey = lastSeen.keys().next().value;
const idx = lastSeen.get(firstKey);
lastSeen.delete(firstKey);
left = idx + 1;
}
maxLength = Math.max(maxLength, right - left + 1);
}
return maxLength;
}
// Test
console.log(lengthOfLongestSubstringKDistinct('eceba', 2)); // Expected: 3
Line Notes
if (k === 0 || s.length === 0) return 0;Handle edge cases early
const lastSeen = new Map();Map to maintain insertion order and last indices
if (lastSeen.has(c)) lastSeen.delete(c);Remove character to update its position
lastSeen.set(c, right);Insert character with current index
if (lastSeen.size > k)If distinct chars exceed k, remove oldest
const firstKey = lastSeen.keys().next().value;Get oldest character key
const idx = lastSeen.get(firstKey);Get last index of oldest character
lastSeen.delete(firstKey);Remove oldest character
left = idx + 1;Move left pointer past removed character
maxLength = Math.max(maxLength, right - left + 1);Update max length
return maxLength;Return longest valid substring length
Each character is processed once, and the ordered dictionary/map operations are O(1) average. The window adjusts dynamically.
💡 This approach is similar in efficiency to the frequency map method but uses order to quickly evict the oldest character.
Interview Verdict: Accepted
This approach is optimal and useful when you want to quickly identify and remove the oldest character in the window.