String Matching and Pattern Search
1. Longest Substring Without Repeating Characters
| Technique | Strategy | Data Structure | Time Complexity |
|---|---|---|---|
| Sliding Window | Expand right, shrink left when duplicate found | HashSet or HashMap for tracking | O(n) |
| HashMap Optimization | Store last seen index, jump left pointer | Map: char → last index | O(n) |
| Array Index | Use array for ASCII characters (256 size) | lastSeen[128] or [256] | O(n) |
Example: Longest Substring Without Repeating Characters
function lengthOfLongestSubstring(s) {
const seen = new Map();
let left = 0;
let maxLen = 0;
for (let right = 0; right < s.length; right++) {
const char = s[right];
// If char seen and within current window
if (seen.has(char) && seen.get(char) >= left) {
// Move left pointer past the previous occurrence
left = seen.get(char) + 1;
}
// Update last seen position
seen.set(char, right);
// Update max length
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}
console.log(lengthOfLongestSubstring("abcabcbb")); // 3 ("abc")
console.log(lengthOfLongestSubstring("bbbbb")); // 1 ("b")
console.log(lengthOfLongestSubstring("pwwkew")); // 3 ("wke")
// Trace for "abcabcbb":
// right=0, char='a': seen={a:0}, left=0, len=1
// right=1, char='b': seen={a:0,b:1}, left=0, len=2
// right=2, char='c': seen={a:0,b:1,c:2}, left=0, len=3
// right=3, char='a': seen at 0, left=1, len=3
// right=4, char='b': seen at 1, left=2, len=3
// ...
Example: Alternative Using Set
function lengthOfLongestSubstringSet(s) {
const window = new Set();
let left = 0;
let maxLen = 0;
for (let right = 0; right < s.length; right++) {
// Shrink window while duplicate exists
while (window.has(s[right])) {
window.delete(s[left]);
left++;
}
window.add(s[right]);
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}
// Using array for ASCII (faster for small character sets)
function lengthOfLongestSubstringArray(s) {
const lastSeen = new Array(128).fill(-1);
let left = 0;
let maxLen = 0;
for (let right = 0; right < s.length; right++) {
const charCode = s.charCodeAt(right);
// If char seen within current window
if (lastSeen[charCode] >= left) {
left = lastSeen[charCode] + 1;
}
lastSeen[charCode] = right;
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}
Note: The HashMap optimization jumps
left directly to index + 1 of duplicate character, avoiding the need to remove characters one-by-one. This maintains O(n) with only one pass.
2. Longest Palindromic Substring (Manacher's Algorithm)
| Algorithm | Approach | Key Idea | Complexity |
|---|---|---|---|
| Expand Around Center | Try each position as center, expand outward | Handle odd/even length separately | O(n²) |
| Dynamic Programming | dp[i][j] = is s[i..j] palindrome | Build from length 1 to n | O(n²) |
| Manacher's Algorithm | Use previously computed palindromes | Mirror property + special characters | O(n) |
Example: Expand Around Center
function longestPalindrome(s) {
if (!s || s.length < 1) return "";
let start = 0, maxLen = 0;
function expandAroundCenter(left, right) {
while (left >= 0 && right < s.length && s[left] === s[right]) {
left--;
right++;
}
// Return length of palindrome
return right - left - 1;
}
for (let i = 0; i < s.length; i++) {
// Odd length palindrome (single center)
const len1 = expandAroundCenter(i, i);
// Even length palindrome (two centers)
const len2 = expandAroundCenter(i, i + 1);
const len = Math.max(len1, len2);
if (len > maxLen) {
maxLen = len;
// Calculate start position
start = i - Math.floor((len - 1) / 2);
}
}
return s.substring(start, start + maxLen);
}
console.log(longestPalindrome("babad")); // "bab" or "aba"
console.log(longestPalindrome("cbbd")); // "bb"
// Why expand around center works:
// "racecar": center at 'e' → expands to full string
// "abba": center between 'b' and 'b' → expands to full string
Example: Manacher's Algorithm (Linear Time)
function longestPalindromeManacher(s) {
if (!s) return "";
// Transform string: "abc" → "^#a#b#c#$"
// ^ and $ are sentinels to avoid boundary checks
let T = "^";
for (const char of s) {
T += "#" + char;
}
T += "#$";
const n = T.length;
const P = new Array(n).fill(0); // P[i] = radius of palindrome centered at i
let center = 0, right = 0;
for (let i = 1; i < n - 1; i++) {
// Mirror of i with respect to center
const mirror = 2 * center - i;
// If i is within right boundary, use mirror's value
if (i < right) {
P[i] = Math.min(right - i, P[mirror]);
}
// Try to expand palindrome centered at i
while (T[i + P[i] + 1] === T[i - P[i] - 1]) {
P[i]++;
}
// If palindrome centered at i extends past right, update center
if (i + P[i] > right) {
center = i;
right = i + P[i];
}
}
// Find the longest palindrome
let maxLen = 0;
let centerIndex = 0;
for (let i = 1; i < n - 1; i++) {
if (P[i] > maxLen) {
maxLen = P[i];
centerIndex = i;
}
}
// Extract the longest palindrome from original string
const start = Math.floor((centerIndex - maxLen) / 2);
return s.substring(start, start + maxLen);
}
console.log(longestPalindromeManacher("babad")); // "bab" or "aba"
// Manacher's key insight:
// Use symmetry - palindrome's left half mirrors right half
// If position i is inside palindrome centered at c,
// then P[i] is at least min(P[mirror], right - i)
Note: Manacher's algorithm uses mirror property of palindromes and adds special characters (#) to handle both odd and even length palindromes uniformly. Achieves O(n) by avoiding redundant comparisons.
3. Minimum Window Substring (Sliding Window)
| Technique | Strategy | Tracking | Complexity |
|---|---|---|---|
| Two Pointers | Expand right until valid, shrink left while valid | Count required vs found characters | O(n + m) |
| Character Frequency | Map to track required and current counts | Compare frequencies for validity | O(n) |
| Formed Counter | Count how many unique chars meet requirement | formed === required → valid window | O(n) |
Example: Minimum Window Substring
function minWindow(s, t) {
if (!s || !t || s.length < t.length) return "";
// Count characters in t
const tCount = new Map();
for (const char of t) {
tCount.set(char, (tCount.get(char) || 0) + 1);
}
const required = tCount.size; // Unique characters needed
let formed = 0; // Unique characters in current window with desired frequency
const windowCount = new Map();
let left = 0;
let minLen = Infinity;
let minStart = 0;
for (let right = 0; right < s.length; right++) {
const char = s[right];
windowCount.set(char, (windowCount.get(char) || 0) + 1);
// Check if this character's frequency matches requirement
if (tCount.has(char) && windowCount.get(char) === tCount.get(char)) {
formed++;
}
// Try to shrink window while it's valid
while (formed === required && left <= right) {
// Update result if this window is smaller
if (right - left + 1 < minLen) {
minLen = right - left + 1;
minStart = left;
}
// Remove leftmost character
const leftChar = s[left];
windowCount.set(leftChar, windowCount.get(leftChar) - 1);
if (tCount.has(leftChar) && windowCount.get(leftChar) < tCount.get(leftChar)) {
formed--;
}
left++;
}
}
return minLen === Infinity ? "" : s.substring(minStart, minStart + minLen);
}
console.log(minWindow("ADOBECODEBANC", "ABC")); // "BANC"
console.log(minWindow("a", "a")); // "a"
console.log(minWindow("a", "aa")); // ""
// Trace for "ADOBECODEBANC", "ABC":
// Need: {A:1, B:1, C:1}
// Window expands to "ADOBEC" - valid, try shrink
// Window "DOBEC" - invalid (no A)
// Continue expanding to "ODEBANC" - valid
// Final answer: "BANC"
Example: Simplified Version with Character Array
function minWindowSimplified(s, t) {
const tFreq = {};
for (const char of t) {
tFreq[char] = (tFreq[char] || 0) + 1;
}
let required = Object.keys(tFreq).length;
let formed = 0;
const windowFreq = {};
let left = 0;
let result = "";
let minLen = Infinity;
for (let right = 0; right < s.length; right++) {
const char = s[right];
windowFreq[char] = (windowFreq[char] || 0) + 1;
if (tFreq[char] && windowFreq[char] === tFreq[char]) {
formed++;
}
while (formed === required) {
// Update result
const windowSize = right - left + 1;
if (windowSize < minLen) {
minLen = windowSize;
result = s.substring(left, right + 1);
}
// Shrink
const leftChar = s[left];
if (tFreq[leftChar] && windowFreq[leftChar] === tFreq[leftChar]) {
formed--;
}
windowFreq[leftChar]--;
left++;
}
}
return result;
}
// Variation: Find all start indices
function findSubstring(s, words) {
if (!s || !words || words.length === 0) return [];
const wordLen = words[0].length;
const totalLen = wordLen * words.length;
const wordCount = {};
for (const word of words) {
wordCount[word] = (wordCount[word] || 0) + 1;
}
const result = [];
for (let i = 0; i <= s.length - totalLen; i++) {
const seen = {};
let j = 0;
while (j < words.length) {
const wordStart = i + j * wordLen;
const word = s.substring(wordStart, wordStart + wordLen);
if (!wordCount[word]) break;
seen[word] = (seen[word] || 0) + 1;
if (seen[word] > wordCount[word]) break;
j++;
}
if (j === words.length) {
result.push(i);
}
}
return result;
}
Window Validity Criteria
- Formed Counter: Track unique chars meeting requirement
- Valid Window: formed === required
- Expand: Add right char, check if formed increases
- Shrink: Remove left char, check if formed decreases
- Update Result: Only when window is valid and smaller
Note: Key Pattern
- Use two maps: required frequencies and window frequencies
- Formed tracks chars with exact frequency match
- Expand until valid, then shrink while maintaining validity
- O(n) time: each character visited at most twice (right + left)
4. Group Anagrams Pattern
| Approach | Key Generation | Time | Space |
|---|---|---|---|
| Sorted String Key | Sort characters: "eat" → "aet" | O(n * k log k) |
O(n * k) |
| Character Count Key | Frequency array: [1,0,0,1,1...] for "eat" | O(n * k) |
O(n * k) |
| Prime Number Hash | Product of primes for each char | O(n * k) |
O(n * k) |
Example: Group Anagrams Using Sorted Key
function groupAnagrams(strs) {
const groups = new Map();
for (const str of strs) {
// Sort string to create key
const key = str.split('').sort().join('');
if (!groups.has(key)) {
groups.set(key, []);
}
groups.get(key).push(str);
}
return Array.from(groups.values());
}
console.log(groupAnagrams(["eat","tea","tan","ate","nat","bat"]));
// [["eat","tea","ate"], ["tan","nat"], ["bat"]]
// Why it works:
// "eat" → sort → "aet"
// "tea" → sort → "aet" (same key!)
// "ate" → sort → "aet" (same key!)
// All anagrams produce identical sorted string
Example: Using Character Count as Key
function groupAnagramsCharCount(strs) {
const groups = new Map();
for (const str of strs) {
// Create frequency array as key
const count = new Array(26).fill(0);
for (const char of str) {
count[char.charCodeAt(0) - 'a'.charCodeAt(0)]++;
}
// Convert array to string key
const key = count.join('#');
if (!groups.has(key)) {
groups.set(key, []);
}
groups.get(key).push(str);
}
return Array.from(groups.values());
}
// Example with detailed trace:
// "eat" → [1,0,0,0,1,0,...,0,1] → "1#0#0#0#1#...#1"
// "tea" → [1,0,0,0,1,0,...,0,1] → "1#0#0#0#1#...#1" (same!)
// Alternative: Use object as map
function groupAnagramsObject(strs) {
const groups = {};
for (const str of strs) {
const count = new Array(26).fill(0);
for (const char of str) {
count[char.charCodeAt(0) - 97]++;
}
const key = count.toString();
if (!groups[key]) {
groups[key] = [];
}
groups[key].push(str);
}
return Object.values(groups);
}
Example: Related Problems - Valid Anagram
function isAnagram(s, t) {
if (s.length !== t.length) return false;
const count = new Array(26).fill(0);
for (let i = 0; i < s.length; i++) {
count[s.charCodeAt(i) - 97]++;
count[t.charCodeAt(i) - 97]--;
}
return count.every(c => c === 0);
}
// Find all anagrams of p in s
function findAnagrams(s, p) {
const result = [];
const pCount = new Array(26).fill(0);
const windowCount = new Array(26).fill(0);
// Count characters in p
for (const char of p) {
pCount[char.charCodeAt(0) - 97]++;
}
for (let i = 0; i < s.length; i++) {
// Add new character to window
windowCount[s.charCodeAt(i) - 97]++;
// Remove old character if window too large
if (i >= p.length) {
windowCount[s.charCodeAt(i - p.length) - 97]--;
}
// Check if current window is anagram
if (i >= p.length - 1 && arraysEqual(pCount, windowCount)) {
result.push(i - p.length + 1);
}
}
return result;
}
function arraysEqual(arr1, arr2) {
for (let i = 0; i < arr1.length; i++) {
if (arr1[i] !== arr2[i]) return false;
}
return true;
}
console.log(findAnagrams("cbaebabacd", "abc")); // [0, 6]
5. String Permutation in String Check
| Problem Type | Technique | Key Insight | Complexity |
|---|---|---|---|
| Check if Permutation | Compare character frequencies | Same frequency → permutation possible | O(n) |
| Permutation in String | Sliding window of fixed size | Window matches pattern frequency | O(n) |
| Generate All Permutations | Backtracking or iterative | Swap elements recursively | O(n!) |
Example: Check Permutation in String
function checkInclusion(s1, s2) {
if (s1.length > s2.length) return false;
const s1Count = new Array(26).fill(0);
const windowCount = new Array(26).fill(0);
// Count characters in s1
for (const char of s1) {
s1Count[char.charCodeAt(0) - 97]++;
}
// Sliding window
for (let i = 0; i < s2.length; i++) {
// Add right character
windowCount[s2.charCodeAt(i) - 97]++;
// Remove left character if window too large
if (i >= s1.length) {
windowCount[s2.charCodeAt(i - s1.length) - 97]--;
}
// Check if window matches s1
if (i >= s1.length - 1 && matches(s1Count, windowCount)) {
return true;
}
}
return false;
}
function matches(arr1, arr2) {
for (let i = 0; i < 26; i++) {
if (arr1[i] !== arr2[i]) return false;
}
return true;
}
console.log(checkInclusion("ab", "eidbaooo")); // true ("ba" is permutation)
console.log(checkInclusion("ab", "eidboaoo")); // false
// Optimized: Use difference counter
function checkInclusionOptimized(s1, s2) {
if (s1.length > s2.length) return false;
const count = new Array(26).fill(0);
// Initialize: count s1 chars as positive, first window as negative
for (let i = 0; i < s1.length; i++) {
count[s1.charCodeAt(i) - 97]++;
count[s2.charCodeAt(i) - 97]--;
}
let diff = 0;
for (const c of count) {
if (c !== 0) diff++;
}
if (diff === 0) return true;
// Slide window
for (let i = s1.length; i < s2.length; i++) {
const rightIdx = s2.charCodeAt(i) - 97;
const leftIdx = s2.charCodeAt(i - s1.length) - 97;
// Add right
if (count[rightIdx] === 0) diff++;
count[rightIdx]--;
if (count[rightIdx] === 0) diff--;
// Remove left
if (count[leftIdx] === 0) diff++;
count[leftIdx]++;
if (count[leftIdx] === 0) diff--;
if (diff === 0) return true;
}
return false;
}
Example: Generate All Permutations
function permute(nums) {
const result = [];
function backtrack(path, used) {
if (path.length === nums.length) {
result.push([...path]);
return;
}
for (let i = 0; i < nums.length; i++) {
if (used[i]) continue;
path.push(nums[i]);
used[i] = true;
backtrack(path, used);
path.pop();
used[i] = false;
}
}
backtrack([], new Array(nums.length).fill(false));
return result;
}
console.log(permute([1, 2, 3]));
// [[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]]
// String permutations
function permuteString(s) {
const result = [];
const chars = s.split('');
function swap(i, j) {
[chars[i], chars[j]] = [chars[j], chars[i]];
}
function permute(start) {
if (start === chars.length) {
result.push(chars.join(''));
return;
}
for (let i = start; i < chars.length; i++) {
swap(start, i);
permute(start + 1);
swap(start, i); // backtrack
}
}
permute(0);
return result;
}
console.log(permuteString("abc"));
// ["abc", "acb", "bac", "bca", "cab", "cba"]
6. Longest Repeating Character Replacement Problem
| Concept | Strategy | Formula | Complexity |
|---|---|---|---|
| Sliding Window | Expand right, shrink if replacements > k | windowSize - maxFreq <= k |
O(n) |
| Max Frequency | Track most frequent character in window | Others can be replaced to match max | O(26n) = O(n) |
| Valid Window | Window valid if replacements ≤ k | len - maxCount ≤ k | Check per iteration |
Example: Longest Repeating Character Replacement
function characterReplacement(s, k) {
const count = new Array(26).fill(0);
let left = 0;
let maxCount = 0; // Most frequent char in current window
let maxLen = 0;
for (let right = 0; right < s.length; right++) {
const rightIdx = s.charCodeAt(right) - 65; // 'A' = 65
count[rightIdx]++;
// Update max frequency in current window
maxCount = Math.max(maxCount, count[rightIdx]);
// Window size - max frequency = characters to replace
// If more than k, shrink window
while (right - left + 1 - maxCount > k) {
const leftIdx = s.charCodeAt(left) - 65;
count[leftIdx]--;
left++;
// Note: We don't update maxCount here (optimization)
}
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}
console.log(characterReplacement("ABAB", 2)); // 4 (replace both B → "AAAA")
console.log(characterReplacement("AABABBA", 1)); // 4 (replace 1 B → "AABAA")
// Trace for "ABAB", k=2:
// Window "A": len=1, maxCount=1, replacements=0, valid
// Window "AB": len=2, maxCount=1, replacements=1, valid
// Window "ABA": len=3, maxCount=2, replacements=1, valid
// Window "ABAB": len=4, maxCount=2, replacements=2, valid ✓
// Result: 4 (can replace 2 B's to get "AAAA")
Example: Alternative with Explicit Max Count Update
function characterReplacementExplicit(s, k) {
const count = {};
let left = 0;
let maxLen = 0;
for (let right = 0; right < s.length; right++) {
count[s[right]] = (count[s[right]] || 0) + 1;
// Get current max frequency
const maxCount = Math.max(...Object.values(count));
// Shrink if invalid
while (right - left + 1 - maxCount > k) {
count[s[left]]--;
if (count[s[left]] === 0) {
delete count[s[left]];
}
left++;
}
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}
// Related: Longest Substring with At Most K Distinct Characters
function lengthOfLongestSubstringKDistinct(s, k) {
if (k === 0) return 0;
const count = new Map();
let left = 0;
let maxLen = 0;
for (let right = 0; right < s.length; right++) {
count.set(s[right], (count.get(s[right]) || 0) + 1);
// Shrink if more than k distinct characters
while (count.size > k) {
count.set(s[left], count.get(s[left]) - 1);
if (count.get(s[left]) === 0) {
count.delete(s[left]);
}
left++;
}
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}
console.log(lengthOfLongestSubstringKDistinct("eceba", 2)); // 3 ("ece")
Note: Key insight: windowSize - maxFreq = characters to replace. If this exceeds k, window is invalid. We don't need to update maxCount when shrinking because we only care about finding a larger window, not maintaining exact maxCount.
Summary: String Matching Pattern
- Longest Substring No Repeat: Sliding window with HashMap - jump left to duplicate's next position - O(n)
- Longest Palindrome: Expand around center O(n²), or Manacher's algorithm O(n) with mirror property
- Minimum Window Substring: Two pointers with frequency maps - expand right until valid, shrink left while valid
- Group Anagrams: Use sorted string or character count array as key - anagrams map to same key
- Permutation Check: Fixed-size sliding window with frequency comparison - O(n) with 26-size array
- Character Replacement: Window valid when (size - maxFreq) ≤ k - track most frequent character
- Frequency Tracking: Use Map/Object for flexible chars, Array[26] for lowercase letters (faster)
- Sliding Window Template: Expand right, check validity, shrink left, update result
- Key Insight: Most string problems use sliding window or frequency maps - choose data structure based on character set size