Interview Importance: π΄ Critical β This technique appears in 35% of frontend coding interviews for substring, subarray, and data stream problems. Essential for optimizing O(nΒ²) brute force solutions to O(n) linear time.
The Sliding Window Technique is an algorithmic pattern that maintains a "window" (contiguous sequence) over an array or string. The window slides through the data structure by adding elements on one end and removing from the other, allowing efficient computation without recalculating everything from scratch.
Visual Representation:
String: "abcabcbb"
Window of size 3 sliding right:
Step 1: [a b c] a b c b b -> "abc"
Step 2: a [b c a] b c b b -> "bca"
Step 3: a b [c a b] c b b -> "cab"
Step 4: a b c [a b c] b b -> "abc"
Dynamic window (expands/contracts):
Step 1: [a b c] -> unique chars: 3
Step 2: [a b c a] -> duplicate! shrink from left
Step 3: [b c a] -> unique chars: 3
Real-World Analogy:
Think of it like a camera viewfinder moving across a landscape. Instead of taking a new photo for every possible view (expensive), you slide the viewfinder smoothly, only adjusting what enters and exits the frame. This is exactly how video streaming buffers work in frontend applications!
| Problem Type | Without Sliding Window | With Sliding Window | Benefit |
|---|---|---|---|
| Longest substring without repeats | O(nΒ³) check all substrings | O(n) single pass | 1000x faster for 1000 chars |
| Max sum of k elements | O(n*k) recalculate sums | O(n) maintain sum | Linear scaling |
| Count anagrams | O(n*m) compare each window | O(n) frequency map | Optimal for pattern matching |
| Find all substrings of length k | O(n*k) create substrings | O(n) slide indices | 50% memory saved |
| Valid window with conditions | O(nΒ²) nested checks | O(n) expand/contract | Real-time processing |
Performance Benefits:
// Find maximum sum of any subarray of size k const maxSumSubarray = (arr, k) => { if (!arr || arr.length === 0) return null; // Edge case: null or empty array if (k <= 0) return null; // Edge case: invalid window size if (arr.length < k) return null; // Edge case: array smaller than window // Calculate sum of first window let windowSum = 0; for (let i = 0; i < k; i++) { windowSum += arr[i]; } let maxSum = windowSum; // Slide window: add next element, remove first element for (let i = k; i < arr.length; i++) { windowSum = windowSum + arr[i] - arr[i - k]; // Slide in O(1) maxSum = Math.max(maxSum, windowSum); } return maxSum; };
Input: arr = [2, 1, 5, 1, 3, 2], k = 3
Step 1: Initialize first window of size 3
---------------------------------------------------------
arr = [2, 1, 5, 1, 3, 2]
[-----] <-- window
windowSum = 2 + 1 + 5 = 8
maxSum = 8
Step 2: Slide window right (remove arr[0], add arr[3])
---------------------------------------------------------
arr = [2, 1, 5, 1, 3, 2]
[-----] <-- window
Remove: arr[0] = 2
Add: arr[3] = 1
windowSum = 8 - 2 + 1 = 7
maxSum = Math.max(8, 7) = 8
Step 3: Slide window right (remove arr[1], add arr[4])
---------------------------------------------------------
arr = [2, 1, 5, 1, 3, 2]
[-----] <-- window
Remove: arr[1] = 1
Add: arr[4] = 3
windowSum = 7 - 1 + 3 = 9
maxSum = Math.max(8, 9) = 9 β New maximum!
Step 4: Slide window right (remove arr[2], add arr[5])
---------------------------------------------------------
arr = [2, 1, 5, 1, 3, 2]
[-----] <-- window
Remove: arr[2] = 5
Add: arr[5] = 2
windowSum = 9 - 5 + 2 = 6
maxSum = Math.max(9, 6) = 9
Result: 9 (subarray [5, 1, 3])
Time Complexity: O(n) - single pass through array
Space Complexity: O(1) - only storing sum and max
// β BAD: Brute force recalculates entire sum each time for (let i = 0; i <= arr.length - k; i++) { let sum = 0; for (let j = i; j < i + k; j++) { // Nested loop! sum += arr[j]; } maxSum = Math.max(maxSum, sum); } // Time: O(n*k) - recalculating overlapping work // β GOOD: Sliding window reuses previous calculation windowSum = windowSum + arr[i] - arr[i - k]; // O(1) update! // Time: O(n) - each element visited once
Key Insight: When the window slides, only 2 elements change (one enters, one exits). We don't need to recalculate everything in between.
// Edge case: What if k > arr.length? if (arr.length < k) return null; // Must check first! // Edge case: What if k is 0 or negative? if (k <= 0) return null; // Invalid window size // Edge case: What if array is empty? if (!arr || arr.length === 0) return null;
These checks prevent:
/** * Find the length of longest substring without repeating characters * Example: "abcabcbb" -> "abc" (length 3) * * @param {string} s - Input string * @returns {number} Length of longest unique substring */ const lengthOfLongestSubstring = (s) => { // Input validation if (!s || s.length === 0) return 0; if (s.length === 1) return 1; const charSet = new Set(); // Track characters in current window let left = 0; // Left boundary of window let maxLength = 0; // Result // Right pointer expands window for (let right = 0; right < s.length; right++) { const char = s[right]; // Shrink window from left while duplicate exists while (charSet.has(char)) { charSet.delete(s[left]); // Remove leftmost character left++; // Move left boundary } // Add current character and update max length charSet.add(char); maxLength = Math.max(maxLength, right - left + 1); } return maxLength; };
Input: s = "abcabcbb"
Step 1: Start with empty window
---------------------------------------------------------
s = "abcabcbb"
^
left=0, right=0
charSet = {}
Add 'a': charSet = {a}
maxLength = 1
Step 2: Expand window (right moves)
---------------------------------------------------------
s = "abcabcbb"
^ ^
left=0, right=1
charSet = {a}
Add 'b': charSet = {a, b}
maxLength = max(1, 2) = 2
Step 3: Continue expanding
---------------------------------------------------------
s = "abcabcbb"
^ ^
left=0, right=2
charSet = {a, b}
Add 'c': charSet = {a, b, c}
maxLength = max(2, 3) = 3 β "abc"
Step 4: Duplicate found! Shrink from left
---------------------------------------------------------
s = "abcabcbb"
^ ^
left=0, right=3
charSet = {a, b, c}
char='a' already exists!
While loop iteration 1:
Remove s[0]='a': charSet = {b, c}
left = 1
Now 'a' not in set, exit while loop
Add 'a': charSet = {b, c, a}
maxLength = max(3, 3) = 3
Step 5: Another duplicate 'b'
---------------------------------------------------------
s = "abcabcbb"
^ ^
left=1, right=4
charSet = {b, c, a}
char='b' already exists!
While loop:
Remove s[1]='b': charSet = {c, a}
left = 2
Add 'b': charSet = {c, a, b}
maxLength = max(3, 3) = 3
Step 6: Another duplicate 'c'
---------------------------------------------------------
s = "abcabcbb"
^ ^
left=2, right=5
charSet = {c, a, b}
char='c' already exists!
While loop:
Remove s[2]='c': charSet = {a, b}
left = 3
Add 'c': charSet = {a, b, c}
maxLength = max(3, 3) = 3
Step 7-8: Process remaining 'b's (shrinking continues)
---------------------------------------------------------
Final maxLength = 3
Result: 3 (substring "abc" or "bca" or "cab")
/** * Count occurrences of anagrams of pattern in text * Example: countAnagrams("cbaebabacd", "abc") -> 2 * Windows "cba" and "bac" are anagrams of "abc" */ const countAnagrams = (text, pattern) => { if (!text || !pattern || text.length < pattern.length) return 0; const k = pattern.length; const patternFreq = new Map(); const windowFreq = new Map(); // Build frequency map for pattern for (const char of pattern) { patternFreq.set(char, (patternFreq.get(char) || 0) + 1); } let count = 0; let matches = 0; // Tracks how many characters match pattern frequency // Process first window for (let i = 0; i < k; i++) { const char = text[i]; windowFreq.set(char, (windowFreq.get(char) || 0) + 1); if (patternFreq.has(char) && windowFreq.get(char) === patternFreq.get(char)) { matches++; } } if (matches === patternFreq.size) count++; // Slide window for (let i = k; i < text.length; i++) { // Add new character (right side) const newChar = text[i]; windowFreq.set(newChar, (windowFreq.get(newChar) || 0) + 1); if (patternFreq.has(newChar)) { if (windowFreq.get(newChar) === patternFreq.get(newChar)) { matches++; } else if (windowFreq.get(newChar) === patternFreq.get(newChar) + 1) { matches--; // Exceeded required count } } // Remove old character (left side) const oldChar = text[i - k]; if (patternFreq.has(oldChar)) { if (windowFreq.get(oldChar) === patternFreq.get(oldChar)) { matches--; } else if (windowFreq.get(oldChar) === patternFreq.get(oldChar) + 1) { matches++; } } windowFreq.set(oldChar, windowFreq.get(oldChar) - 1); if (windowFreq.get(oldChar) === 0) { windowFreq.delete(oldChar); } // Check if current window is an anagram if (matches === patternFreq.size) count++; } return count; };
/** * Real-time search suggestions with substring matching * Used in: Search bars, command palettes, dropdown filters */ const getSearchSuggestions = (query, maxResults = 5) => { const suggestions = []; const queryLower = query.toLowerCase(); const k = query.length; // Simulate database of items const items = [ "JavaScript Developer", "Java Backend Engineer", "Senior JavaScript", "Python Developer" ]; for (const item of items) { const itemLower = item.toLowerCase(); // Slide window through item to find query for (let i = 0; i <= itemLower.length - k; i++) { const window = itemLower.slice(i, i + k); if (window === queryLower) { suggestions.push(item); break; // Found match, no need to continue } } if (suggestions.length >= maxResults) break; } return suggestions; }; // Usage console.log(getSearchSuggestions("java")); // -> ["JavaScript Developer", "Java Backend Engineer", "Senior JavaScript"]
import { useState, useEffect, useRef } from 'react'; /** * Custom hook using sliding window concept for search optimization * Maintains a buffer of recent searches to avoid redundant API calls */ const useSearchWithWindow = (searchFn, windowSize = 10) => { const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); const cacheRef = useRef(new Map()); // FIFO cache with sliding window (max size) useEffect(() => { if (!query) { setResults([]); return; } // Check cache first (sliding window of recent searches) if (cacheRef.current.has(query)) { setResults(cacheRef.current.get(query)); return; } const timeoutId = setTimeout(async () => { setLoading(true); try { const data = await searchFn(query); setResults(data); // Maintain sliding window of cache if (cacheRef.current.size >= windowSize) { // Remove oldest entry (FIFO) const firstKey = cacheRef.current.keys().next().value; cacheRef.current.delete(firstKey); } cacheRef.current.set(query, data); } catch (error) { console.error('Search error:', error); setResults([]); } finally { setLoading(false); } }, 300); // Debounce return () => clearTimeout(timeoutId); }, [query, searchFn, windowSize]); return { query, setQuery, results, loading }; }; // Usage in component const SearchComponent = () => { const searchAPI = async (q) => { const res = await fetch(`/api/search?q=${q}`); return res.json(); }; const { query, setQuery, results, loading } = useSearchWithWindow(searchAPI); return ( <div> <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search..." /> {loading && <p>Loading...</p>} <ul> {results.map(item => <li key={item.id}>{item.name}</li>)} </ul> </div> ); };
/** * Monitor real-time metrics with sliding window * Use case: Performance monitoring, analytics dashboards */ class MetricsMonitor { constructor(windowSizeMs = 60000) { // 1 minute window this.window = []; this.windowSize = windowSizeMs; } addMetric(value, timestamp = Date.now()) { this.window.push({ value, timestamp }); // Slide window: remove old metrics const cutoff = timestamp - this.windowSize; while (this.window.length > 0 && this.window[0].timestamp < cutoff) { this.window.shift(); } } getAverage() { if (this.window.length === 0) return 0; const sum = this.window.reduce((acc, metric) => acc + metric.value, 0); return sum / this.window.length; } getMax() { if (this.window.length === 0) return null; return Math.max(...this.window.map(m => m.value)); } getPercentile(p) { if (this.window.length === 0) return null; const sorted = [...this.window].sort((a, b) => a.value - b.value); const index = Math.ceil((p / 100) * sorted.length) - 1; return sorted[index].value; } } // Usage: Track API response times const monitor = new MetricsMonitor(60000); // 1 minute sliding window // Simulate API calls setInterval(() => { const responseTime = Math.random() * 1000; // 0-1000ms monitor.addMetric(responseTime); console.log({ avg: monitor.getAverage().toFixed(2), max: monitor.getMax().toFixed(2), p95: monitor.getPercentile(95).toFixed(2) }); }, 1000);
| Aspect | Sliding Window | Two Pointers | Hash Map Only | Brute Force |
|---|---|---|---|---|
| Time Complexity | O(n) | O(n) | O(n) | O(nΒ²) or O(nΒ³) |
| Space Complexity | O(k) for window | O(1) typically | O(n) worst case | O(1) |
| Use Case | Contiguous sequences | Sorted arrays, pairs | Frequency counting | Small inputs only |
| Window Type | Fixed or variable | N/A | N/A | All possible |
| Best For | Substrings, subarrays | Finding pairs, palindromes | Character counting | Testing, validation |
| Frontend Example | Search autocomplete | Merge sorted lists | Detect duplicates | Simple validation |
// Use Sliding Window when: // - Looking for contiguous sequence (substring/subarray) // - Need to optimize from O(nΒ²) to O(n) // - Window size is fixed or changes predictably β "Find longest substring with at most k distinct characters" β "Maximum sum of k consecutive elements" β "Find all anagrams of a pattern" // Use Two Pointers when: // - Array is sorted // - Looking for pairs meeting a condition // - Need to process from both ends β "Find two numbers that sum to target in sorted array" β "Remove duplicates from sorted array" β "Check if string is palindrome" // Use Hash Map when: // - Order doesn't matter // - Need O(1) lookups // - Counting frequencies β "Find first non-repeating character" β "Check if two strings are anagrams" β "Group items by category"
Q1: What's the difference between fixed and variable size sliding windows?
A: Fixed-size windows maintain constant length (e.g., "max sum of k elements"), while variable-size windows expand/contract based on conditions (e.g., "longest substring without repeating characters"). Fixed windows slide by 1 position each time; variable windows adjust both boundaries independently.
// Fixed: Window size = k, always same length for (let i = k; i < arr.length; i++) { windowSum = windowSum + arr[i] - arr[i - k]; } // Variable: Window grows/shrinks based on condition while (right < arr.length) { // Expand add(arr[right]); right++; // Shrink when condition violated while (conditionViolated()) { remove(arr[left]); left++; } }
Q2: How do you handle edge cases in sliding window problems?
A: Always validate:
// Comprehensive edge case handling const slidingWindowTemplate = (arr, k) => { // Edge case 1: Invalid input if (!arr || arr.length === 0) return null; // Edge case 2: Window too large if (k > arr.length) return null; // Edge case 3: Invalid window size if (k <= 0) return null; // Edge case 4: Single element if (arr.length === 1) return arr[0]; // Main logic here };
Q3: When would sliding window be less efficient than brute force?
A: Sliding window is less efficient when:
// BAD: Sliding window overhead not worth it for size 3 const arr = [1, 2, 3]; // Just calculate directly: Math.max(...arr) // BAD: Non-contiguous (need all pairs) "Find two numbers that sum to X" // Use hash map instead // BAD: Expensive window maintenance "Median of each window" // Need to re-sort each time
Q4: How do you optimize space complexity in sliding window problems?
A: Use these strategies:
// β BAD: Creating substrings (O(n*k) space) for (let i = 0; i <= s.length - k; i++) { const window = s.substring(i, i + k); // Creates new string! process(window); } // β GOOD: Use indices (O(1) space) for (let i = 0; i <= s.length - k; i++) { process(s, i, i + k); // Pass indices only } // β GOOD: Reuse Set (O(k) space max) const charSet = new Set(); // Single set for entire algorithm
Q5: Explain how to debug a sliding window solution that's giving wrong answers.
A: Follow this debugging checklist:
// Step 1: Add logging at key points console.log('Window:', { left, right, windowContent }); console.log('State:', { sum, max, frequency }); // Step 2: Check boundary conditions console.log('Entering window?', right < arr.length); console.log('Exiting window?', conditionViolated); // Step 3: Verify window updates const before = windowSum; windowSum = windowSum + arr[right] - arr[left]; console.log(`Updated: ${before} + ${arr[right]} - ${arr[left]} = ${windowSum}`); // Step 4: Dry run manually with small input const testCase = [1, 2, 3]; // Simple case // Trace each step by hand // Step 5: Check off-by-one errors // - Is it right - left + 1 or just right - left? // - Should it be < or <=? // - Is window initialized correctly?
Q6: How would you implement a sliding window for a 2D array?
A: Use two nested sliding windows (row and column):
/** * Find maximum sum of kΓk submatrix */ const maxSumSubmatrix = (matrix, k) => { if (!matrix || matrix.length < k || matrix[0].length < k) return null; const rows = matrix.length; const cols = matrix[0].length; let maxSum = -Infinity; // Slide vertically for (let i = 0; i <= rows - k; i++) { // Column sums for current k rows const colSums = new Array(cols).fill(0); // Calculate initial column sums for (let row = i; row < i + k; row++) { for (let col = 0; col < cols; col++) { colSums[col] += matrix[row][col]; } } // Slide horizontally through column sums let windowSum = 0; for (let j = 0; j < k; j++) { windowSum += colSums[j]; } maxSum = Math.max(maxSum, windowSum); for (let j = k; j < cols; j++) { windowSum = windowSum + colSums[j] - colSums[j - k]; maxSum = Math.max(maxSum, windowSum); } } return maxSum; };
// β BAD: Wrong window size calculation const length = right - left; // Missing + 1! // If left=0, right=2, this gives length=2, but should be 3 // β GOOD: Correct window size const length = right - left + 1; // Inclusive of both ends
Why it breaks: Array indices are 0-based. If left=0 and right=2, the window includes elements at indices 0, 1, and 2 (3 elements total), not 2.
// β BAD: Doesn't handle duplicates in variable window const longestUnique = (s) => { const charSet = new Set(); let left = 0, maxLen = 0; for (let right = 0; right < s.length; right++) { if (charSet.has(s[right])) { charSet.delete(s[left]); // Only removes one char! left++; } charSet.add(s[right]); maxLen = Math.max(maxLen, right - left + 1); } return maxLen; }; // Input: "abba" // After seeing second 'b', we only delete 'a', but 'b' still in set! // Result: Wrong answer // β GOOD: Use while loop to remove all until duplicate gone const longestUnique = (s) => { const charSet = new Set(); let left = 0, maxLen = 0; for (let right = 0; right < s.length; right++) { while (charSet.has(s[right])) { // While, not if! charSet.delete(s[left]); left++; } charSet.add(s[right]); maxLen = Math.max(maxLen, right - left + 1); } return maxLen; };
Why it breaks: A single delete might not remove the duplicate. You need to keep deleting from the left until the duplicate is gone.
// β BAD: Forgetting to update frequency when removing element const countAnagrams = (s, pattern) => { const freq = new Map(); // ... build pattern freq ... for (let i = k; i < s.length; i++) { // Add new character const newChar = s[i]; freq.set(newChar, (freq.get(newChar) || 0) + 1); // Remove old character - FORGOT TO UPDATE! // This causes frequency map to grow forever // Check anagram if (isAnagram(freq, patternFreq)) count++; } }; // β GOOD: Always maintain window state correctly const countAnagrams = (s, pattern) => { const freq = new Map(); // ... build pattern freq ... for (let i = k; i < s.length; i++) { // Add new character const newChar = s[i]; freq.set(newChar, (freq.get(newChar) || 0) + 1); // Remove old character (critical!) const oldChar = s[i - k]; freq.set(oldChar, freq.get(oldChar) - 1); if (freq.get(oldChar) === 0) { freq.delete(oldChar); // Clean up zero counts } // Check anagram if (isAnagram(freq, patternFreq)) count++; } };
Why it breaks: Sliding window requires both adding new elements AND removing old ones. Forgetting either operation corrupts the window state.
// β BAD: Creating substring copies (slow and memory-intensive) const allSubstrings = (s, k) => { const result = []; for (let i = 0; i <= s.length - k; i++) { const window = s.substring(i, i + k); // O(k) time and space each iteration! result.push(window); } return result; }; // Time: O(n*k), Space: O(n*k) // β GOOD: Use indices only const allSubstrings = (s, k) => { const result = []; for (let i = 0; i <= s.length - k; i++) { result.push({ start: i, end: i + k }); // O(1) time and space } return result; }; // Time: O(n), Space: O(n) // If you must return strings, do it lazily: const getString = (indices) => s.substring(indices.start, indices.end);
Why it breaks: Creating substrings for every window position multiplies time complexity by window size (k) and wastes memory. Use indices instead and only create strings when absolutely necessary.
| Operation | Time Complexity | Space Complexity | Explanation |
|---|---|---|---|
| Fixed window (size k) | O(n) | O(1) or O(k) | Single pass; constant space or window storage |
| Variable window (expand/contract) | O(n) | O(k) | Each element enters and exits once; window state |
| With hash map | O(n) | O(k) | Hash operations O(1); k unique elements max |
| With sorting in window | O(nklog k) | O(k) | Sorting each window position; not optimal |
| 2D sliding window | O(n*m) | O(kΒ²) | Iterate all positions; kΓk window storage |
| Brute force comparison | O(nΒ²) or O(nΒ³) | O(1) | Nested loops; no optimization |
// Even though there are nested structures, analysis shows: for (let right = 0; right < n; right++) { // Executes n times while (left < right && condition) { // Total across all iterations: n times left++; // Each element moved by left at most once } } // Total operations: // - right pointer moves n times // - left pointer moves n times (across all while loops combined) // - Total: 2n = O(n)
Key insight: Although the while loop is nested, the left pointer moves at most n times in total across the entire algorithm, not n times per iteration. This is what makes sliding window O(n) instead of O(nΒ²).
| Category | Key Takeaway |
|---|---|
| Definition | Algorithm pattern that maintains a contiguous window sliding through data |
| Time Optimization | Reduces O(nΒ²) or O(nΒ³) to O(n) by avoiding recalculation |
| Space Optimization | O(1) to O(k) by using indices and reusing data structures |
| Fixed Window | Constant size k, slide by adding one and removing one element |
| Variable Window | Dynamic size, expand/contract based on conditions using two pointers |
| Frontend Use Cases | Search autocomplete, real-time monitoring, data stream processing |
| Key Insight | Each element enters and exits the window exactly once |
Test your understanding with 3 quick questions