33. Minimum Window Substring
🔗 LeetCode Problem: 76. Minimum Window Substring
📊 Difficulty: Hard
🏷️ Topics: Hash Table, String, Sliding Window
Problem Statement
Given two strings s and t of lengths m and n respectively, return the minimum window substring of s such that every character in t (including duplicates) is included in the window. If there is no such substring, return the empty string "".
The testcases will be generated such that the answer is unique.
Example 1:
Input: s = "ADOBECODEBANC", t = "ABC"
Output: "BANC"
Explanation: The minimum window substring "BANC" includes 'A', 'B', and 'C' from string t.
Example 2:
Input: s = "a", t = "a"
Output: "a"
Explanation: The entire string s is the minimum window.
Example 3:
Input: s = "a", t = "aa"
Output: ""
Explanation: Both 'a's from t must be included in the window.
Since the largest window of s only has one 'a', return empty string.
Constraints:
- m == s.length
- n == t.length
- 1 <= m, n <= 10^5
- s and t consist of uppercase and lowercase English letters.
Follow up: Could you find an algorithm that runs in O(m + n) time?
🎨 Visual Understanding
The Problem Visualized
s = "ADOBECODEBANC"
t = "ABC"
Find minimum substring containing all characters from t:
A D O B E C O D E B A N C
^ ^ ^ → "ADOBEC" contains A, B, C (length 6)
^ ^ ^ ^ → "ODEBANC" contains A, B, C (length 7)
^ ^ ^ → "BANC" contains A, B, C (length 4) ✓
Answer: "BANC" (shortest window)
Key Observation: Sliding Window
Use two pointers to create a sliding window:
1. Expand window (right pointer) until all characters found
2. Contract window (left pointer) while still valid
3. Track minimum window during contraction
4. Repeat until right reaches end
Example:
s = "ADOBECODEBANC", t = "ABC"
Step 1: Expand until valid
[A D O B E C] → Contains A, B, C ✓ (length 6)
Step 2: Contract while valid
[D O B E C] → Missing A ✗
Stop, record "ADOBEC"
Step 3: Continue expanding
[D O B E C O D E B A] → Contains A, B, C ✓
Step 4: Contract while valid
[B E C O D E B A] → Missing all
Continue...
[B A N C] → Contains A, B, C ✓ (length 4)
Why Sliding Window Works
Key insight:
- Right pointer expands to find valid window
- Left pointer contracts to minimize window
- We never need to backtrack
Time: O(m + n) where m = s.length, n = t.length
Each character visited at most twice (once by right, once by left)
🎯 Approach 1: Brute Force
The Most Natural Thinking 💭
Core Logic:
Check all possible substrings:
1. Generate all substrings of s
2. For each substring, check if it contains all characters from t
3. Track minimum valid substring
Why This Works: - Checks all possibilities - Guaranteed to find answer if exists - Simple to understand
Why This Fails Requirements: - Time complexity O(m² × n) or worse - Too many substrings to check - Checking each substring is expensive - Not acceptable for interviews
Implementation
public String minWindow(String s, String t) {
if (s.length() < t.length()) return "";
String minWindow = "";
int minLen = Integer.MAX_VALUE;
// Try all substrings
for (int i = 0; i < s.length(); i++) {
for (int j = i + 1; j <= s.length(); j++) {
String substring = s.substring(i, j);
// Check if this substring contains all chars from t
if (containsAllChars(substring, t)) {
if (substring.length() < minLen) {
minLen = substring.length();
minWindow = substring;
}
}
}
}
return minWindow;
}
private boolean containsAllChars(String s, String t) {
Map<Character, Integer> tCount = new HashMap<>();
for (char c : t.toCharArray()) {
tCount.put(c, tCount.getOrDefault(c, 0) + 1);
}
for (char c : s.toCharArray()) {
if (tCount.containsKey(c)) {
tCount.put(c, tCount.get(c) - 1);
if (tCount.get(c) == 0) {
tCount.remove(c);
}
}
}
return tCount.isEmpty();
}
⏰ Time: O(m² × n) - m² substrings, each checked in O(n)
💾 Space: O(n) - for character counting
✅ Approach 2: Sliding Window with HashMap (Optimal)
The Breakthrough Insight 💡
Core Logic:
Use sliding window with two pointers + frequency maps:
Data structures:
- tMap: frequency of each character in t (target)
- windowMap: frequency of characters in current window
- required: total unique characters needed from t
- formed: how many unique characters satisfied in window
Algorithm:
1. Build tMap from string t
2. Expand window (right++) until valid (formed == required)
3. Contract window (left++) while still valid
4. Track minimum window during contraction
5. Repeat until right reaches end
Visual Explanation:
s = "ADOBECODEBANC", t = "ABC"
tMap = {A:1, B:1, C:1}
required = 3 (need A, B, C)
Window: [A D O B E C]
windowMap = {A:1, D:1, O:1, B:1, E:1, C:1}
formed = 3 (have A, B, C) ✓
Contract left:
Window: [D O B E C]
windowMap = {D:1, O:1, B:1, E:1, C:1}
formed = 2 (missing A) ✗
Can't contract more, record window "ADOBEC"
Continue expanding...
Key Variables:
required: Number of unique characters in t that need to be present
formed: Number of unique characters in current window with desired frequency
Example: t = "AABC"
tMap = {A:2, B:1, C:1}
required = 3 (need A, B, C as unique chars)
Window has A:1, B:1, C:1 → formed = 2 (A not satisfied yet)
Window has A:2, B:1, C:1 → formed = 3 (all satisfied) ✓
Implementation
public String minWindow(String s, String t) {
if (s.length() == 0 || t.length() == 0) {
return "";
}
// Dictionary to keep count of all unique characters in t
Map<Character, Integer> tMap = new HashMap<>();
for (char c : t.toCharArray()) {
tMap.put(c, tMap.getOrDefault(c, 0) + 1);
}
int required = tMap.size(); // Unique characters in t
int left = 0, right = 0;
int formed = 0; // Unique characters in window with desired frequency
// Dictionary to keep count of characters in current window
Map<Character, Integer> windowMap = new HashMap<>();
// Result: [window length, left, right]
int[] result = {-1, 0, 0};
while (right < s.length()) {
// Expand window by adding character from right
char c = s.charAt(right);
windowMap.put(c, windowMap.getOrDefault(c, 0) + 1);
// Check if frequency of current character matches desired frequency in t
if (tMap.containsKey(c) &&
windowMap.get(c).intValue() == tMap.get(c).intValue()) {
formed++;
}
// Try to contract window until it's no longer valid
while (left <= right && formed == required) {
// Save smallest window
if (result[0] == -1 || right - left + 1 < result[0]) {
result[0] = right - left + 1;
result[1] = left;
result[2] = right;
}
// Character at left pointer is no longer part of window
char leftChar = s.charAt(left);
windowMap.put(leftChar, windowMap.get(leftChar) - 1);
// Check if this character is no longer satisfied
if (tMap.containsKey(leftChar) &&
windowMap.get(leftChar) < tMap.get(leftChar)) {
formed--;
}
// Move left pointer ahead for next iteration
left++;
}
// Expand window
right++;
}
return result[0] == -1 ? "" : s.substring(result[1], result[2] + 1);
}
⏰ Time: O(m + n) - each character visited at most twice
💾 Space: O(m + n) - for HashMaps
🔍 Step-by-Step Execution
Example: s = "ADOBECODEBANC", t = "ABC"
Initial State:
═══════════════════════════════════════════════════════════════════
s = "ADOBECODEBANC"
t = "ABC"
tMap = {A:1, B:1, C:1}
required = 3
left = 0, right = 0
formed = 0
minWindow = ""
Step 1: right=0, char='A'
═══════════════════════════════════════════════════════════════════
Window: [A]
windowMap = {A:1}
A is in tMap and count matches → formed = 1
Try to contract? formed(1) != required(3) → No
Move right++
Step 2: right=1, char='D'
═══════════════════════════════════════════════════════════════════
Window: [A D]
windowMap = {A:1, D:1}
D not in tMap → formed = 1
Move right++
Step 3: right=2, char='O'
═══════════════════════════════════════════════════════════════════
Window: [A D O]
windowMap = {A:1, D:1, O:1}
O not in tMap → formed = 1
Move right++
Step 4: right=3, char='B'
═══════════════════════════════════════════════════════════════════
Window: [A D O B]
windowMap = {A:1, D:1, O:1, B:1}
B is in tMap and count matches → formed = 2
Move right++
Step 5: right=4, char='E'
═══════════════════════════════════════════════════════════════════
Window: [A D O B E]
windowMap = {A:1, D:1, O:1, B:1, E:1}
E not in tMap → formed = 2
Move right++
Step 6: right=5, char='C'
═══════════════════════════════════════════════════════════════════
Window: [A D O B E C]
windowMap = {A:1, D:1, O:1, B:1, E:1, C:1}
C is in tMap and count matches → formed = 3 ✓
Now formed == required, try to contract:
Contract iteration 1: left=0
Current window "ADOBEC" length 6
Update minWindow = "ADOBEC"
Remove 'A': windowMap = {A:0, D:1, O:1, B:1, E:1, C:1}
A count drops below required → formed = 2
left = 1
Stop contracting (formed != required)
Move right++
Steps 7-9: right=6,7,8 (chars 'O','D','E')
═══════════════════════════════════════════════════════════════════
Window expands but formed stays at 2 (missing A)
windowMap updated but not valid yet
Step 10: right=9, char='B'
═══════════════════════════════════════════════════════════════════
Window: [D O B E C O D E B]
windowMap = {D:2, O:2, B:2, E:2, C:1}
B count now 2, but only need 1 → formed = 2
Move right++
Step 11: right=10, char='A'
═══════════════════════════════════════════════════════════════════
Window: [D O B E C O D E B A]
windowMap = {D:2, O:2, B:2, E:2, C:1, A:1}
A is in tMap and count matches → formed = 3 ✓
Try to contract:
Contract iteration 1: left=1
Current window "DOBECODEBA" length 10
Not better than "ADOBEC" (6)
Remove 'D': formed = 3 (still valid)
left = 2
Contract iteration 2: left=2
Remove 'O': formed = 3 (still valid)
left = 3
Contract iteration 3: left=3
Remove 'B': windowMap B count becomes 1
B still satisfied → formed = 3 (still valid)
left = 4
Contract iteration 4: left=4
Remove 'E': formed = 3 (still valid)
left = 5
Contract iteration 5: left=5
Remove 'C': windowMap C count becomes 0
C no longer satisfied → formed = 2
Stop contracting
Move right++
Step 12: right=11, char='N'
═══════════════════════════════════════════════════════════════════
Window: [O D E B A N]
N not in tMap → formed = 2
Move right++
Step 13: right=12, char='C'
═══════════════════════════════════════════════════════════════════
Window: [O D E B A N C]
windowMap gets C:1
C is in tMap and count matches → formed = 3 ✓
Try to contract:
Contract: Keep removing until we can't
Eventually get to: [B A N C]
Window "BANC" length 4 < 6
Update minWindow = "BANC"
═══════════════════════════════════════════════════════════════════
🎯 FINAL RESULT: "BANC"
═══════════════════════════════════════════════════════════════════
🔍 Edge Cases
Case 1: t longer than s
Input: s = "a", t = "aa"
Output: ""
Explanation: Cannot find two 'a's in s
Case 2: Exact match
Input: s = "a", t = "a"
Output: "a"
Explanation: Entire string is the answer
Case 3: t contains duplicates
Input: s = "ADOBECODEBANC", t = "AABC"
Output: "ADOBECODEBA"
Explanation: Need two A's
Case 4: Multiple valid windows
Input: s = "ABCABC", t = "ABC"
Output: "ABC"
Explanation: Two windows of same length, return first
Case 5: No valid window
Input: s = "a", t = "b"
Output: ""
Case 6: All of s needed
Input: s = "ABC", t = "ABC"
Output: "ABC"
Case 7: Extra characters in s
Input: s = "AXXXXBC", t = "ABC"
Output: "AXXXXBC"
Case 8: Window at end
Input: s = "XYZABC", t = "ABC"
Output: "ABC"
Common Mistakes
Mistake 1: Not handling character frequency correctly
❌ Wrong:
if (windowMap.containsKey(c)) {
formed++; // Always increment
}
✓ Right:
if (tMap.containsKey(c) &&
windowMap.get(c).equals(tMap.get(c))) {
formed++; // Only when frequency matches
}
Reason: Need exact frequency match, not just presence
Mistake 2: Wrong contraction condition
❌ Wrong:
while (left < right) { // Missing formed check
...
}
✓ Right:
while (left <= right && formed == required) {
...
}
Reason: Only contract when window is valid
Mistake 3: Not tracking minimum window correctly
❌ Wrong:
String minWindow = s.substring(left, right + 1);
// Directly assigning without comparison
✓ Right:
if (result[0] == -1 || right - left + 1 < result[0]) {
result[0] = right - left + 1;
result[1] = left;
result[2] = right;
}
Reason: Need to track minimum, not just current
Mistake 4: Forgetting to expand window
❌ Wrong:
while (right < s.length()) {
...
// Missing right++
}
✓ Right:
while (right < s.length()) {
...
right++;
}
Reason: Must move right pointer to expand
Mistake 5: Wrong formed decrement
❌ Wrong:
windowMap.put(leftChar, windowMap.get(leftChar) - 1);
if (windowMap.get(leftChar) == 0) {
formed--;
}
✓ Right:
windowMap.put(leftChar, windowMap.get(leftChar) - 1);
if (tMap.containsKey(leftChar) &&
windowMap.get(leftChar) < tMap.get(leftChar)) {
formed--;
}
Reason: Only decrement when frequency drops below required
Mistake 6: Integer comparison with ==
❌ Wrong:
if (windowMap.get(c) == tMap.get(c)) {
// May fail due to Integer caching
}
✓ Right:
if (windowMap.get(c).intValue() == tMap.get(c).intValue()) {
// Or use .equals()
}
Reason: Integer objects need .equals() or .intValue()
🎯 Key Takeaways
⚡ Algorithm Comparison
Approach 1: Brute Force
Time: O(m² × n)
Space: O(n)
Use: Only for understanding, too slow
Approach 2: Sliding Window (RECOMMENDED)
Time: O(m + n)
Space: O(m + n)
Use: Optimal solution, best for interviews
🔑 The Core Insight
Sliding Window Pattern:
1. Use two pointers (left, right) to form window
2. Expand window (right++) until valid
3. Contract window (left++) while still valid
4. Track optimum during contraction
5. Repeat until right reaches end
Key Variables:
- tMap: Target character frequencies
- windowMap: Current window character frequencies
- required: Number of unique characters needed
- formed: Number of unique characters satisfied
Valid Window:
formed == required
(All unique characters from t have desired frequency in window)
Time Complexity:
- Each character visited at most twice
- Once by right pointer (expand)
- Once by left pointer (contract)
- Total: O(m + n)
🎯 Pattern Recognition
Problem Type: Substring with Constraints
Core Pattern: Sliding Window + HashMap
When to Apply:
✓ Finding substring with specific properties
✓ Need to track character frequencies
✓ Can use two pointers (window)
✓ Want O(n) time complexity
Key Techniques:
→ Sliding window with two pointers
→ HashMap for character counting
→ Expand until valid
→ Contract while valid
→ Track optimum during contraction
Related Problems:
- Longest Substring Without Repeating Characters (LC 3)
- Longest Substring with At Most K Distinct Characters (LC 340)
- Find All Anagrams in String (LC 438)
- Substring with Concatenation of All Words (LC 30)
🧠 Interview Strategy
Step 1: "Need minimum substring containing all chars from t"
Step 2: "Use sliding window with two pointers"
Step 3: "Track character frequencies with HashMap"
Step 4: "Expand until valid, contract while valid"
Step 5: "O(m + n) time, each char visited twice max"
Key Points to Mention:
- Sliding window technique
- Two HashMaps for counting
- formed vs required concept
- Contract only when window is valid
- Track minimum during contraction
- O(m + n) time complexity
📝 Quick Revision Notes
🎯 Core Concept:
Use sliding window with two pointers and HashMaps for frequency counting. Expand until valid, contract while valid, track minimum.
⚡ Quick Implementation:
public String minWindow(String s, String t) {
int left = 0;
int right = 0;
// Place all characters of t in tMap
HashMap<Character, Integer> tMap = new HashMap<>();
for(char ch : t.toCharArray()) {
tMap.put(ch, tMap.getOrDefault(ch, 0) + 1);
}
int needed = tMap.size(); // actual number of unique chars in target t
int formed = 0; // actual number of unique chars in source s
// For results
int size = Integer.MAX_VALUE;
int start = 0;
String res = "";
// Place characters of the current window in a hashmap
HashMap<Character, Integer> windowMap = new HashMap<>();
while (right < s.length()) {
// Put the current char being examined in source map (windowMap)
// which is always character at index right
char ch = s.charAt(right);
windowMap.put(ch, windowMap.getOrDefault(ch, 0) + 1);
// check if the count of the current char (ch) in the windowMap
// matches the count of the same char in the tMap
if(tMap.containsKey(ch) && windowMap.get(ch).intValue() == tMap.get(ch).intValue()) { // Gotch2 - without intValue, numbers never become equal for very large numbers
formed++; // no of unique chars matched till now
}
// If no of chars match, save the result and now start shrinking
// the window by reducing the left till you find a difference in char count
while(needed == formed && left <= right) {
// First save the best result before proceeding
if (right - left + 1 < size) {
size = right - left + 1;
// res = s.substring(left, right + 1); // Gotch1 - gets Memory Limit Exception
start = left;
}
// Since window at left side is reduced, windowMap also should be updated.
windowMap.put(s.charAt(left), windowMap.get(s.charAt(left)) - 1);
// since there is a change in windowMap, check if still windowMap contains all chars of tMap
// Similar condition as above. But, check char removed which is at left
if(tMap.containsKey(s.charAt(left)) && windowMap.get(s.charAt(left)).intValue() < tMap.get(s.charAt(left)).intValue()) {
formed--;
}
left++;
}
right++;
}
return size == Integer.MAX_VALUE ? "" : s.substring(start, start + size);
}
🔑 Key Insights:
- Two HashMaps: tMap (target), windowMap (current window)
- Two counters: required (unique chars needed), formed (unique chars satisfied)
- Expand: right++, add to window until formed == required
- Contract: left++, remove from window while formed == required
- Track minimum: Update result during contraction phase
- Valid window: formed == required (all chars with correct frequency)
- Time: O(m + n) - each char visited at most twice
- Space: O(m + n) - for HashMaps
- Key: Only contract when valid, track min while contracting
🎪 Memory Aid:
"Expand till VALID, contract while VALID, track MIN!"
Think: "Two pointers slide, HashMaps count, formed == required"
Related Patterns
- Longest Substring Without Repeating Characters (LC 3)
- Find All Anagrams in a String (LC 438)
- Longest Substring with At Most K Distinct Characters (LC 340)
- Substring with Concatenation of All Words (LC 30)
Happy practicing! 🎯