Sliding window, two pointers, frequency maps, and pattern matching on character sequences.
Strings are arrays of characters. Most string problems map directly to array techniques. The most common patterns are sliding window for substring problems, frequency maps (character count arrays of size 26) for anagram detection, stack-based parsing, and expand-around-center for palindromes.
Most sliding window solutions: O(n)
O(1) with a fixed-size frequency array, O(n) otherwise
Learn the core patterns in this topic. Each block explains when to use the pattern, the intuition behind it, and a compact code example.
Use this when the problem depends on how many times each character appears rather than where it appears
For lowercase English letters, a fixed array of size 26 is often faster and simpler than a hash map. For general Unicode or mixed-case input, a hash map is safer. This pattern is common in anagrams, permutation checks, and counting distinct characters.
Problem Valid Anagram. Count every character in the first string, subtract counts using the second string, and verify every frequency returns to zero.
function isAnagram(s, t) {
if (s.length !== t.length) return false;
const freq = {};
for (const ch of s) freq[ch] = (freq[ch] || 0) + 1;
for (const ch of t) {
if (!freq[ch]) return false;
freq[ch]--;
}
return true;
}Use this when the question asks about a substring and the current answer depends only on the characters inside a moving range
Expand the right pointer to include new characters, and shrink from the left when the window becomes invalid. This avoids rebuilding character counts for every substring from scratch. It is one of the most important patterns for strings.
Problem Longest Substring Without Repeating Characters. Expand the window to the right, and whenever a duplicate appears, move the left pointer until the duplicate is removed.
function lengthOfLongestSubstring(s) {
const seen = new Map();
let left = 0, maxLen = 0;
for (let right = 0; right < s.length; right++) {
const ch = s[right];
if (seen.has(ch) && seen.get(ch) >= left) left = seen.get(ch) + 1;
seen.set(ch, right);
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}Use this when the string contains nested structure, balanced symbols, or encoded expressions where later characters depend on unfinished earlier characters
A stack naturally stores incomplete context. This is the standard tool for valid parentheses, decoding strings, and expression parsing.
Problem Valid Parentheses. Push opening brackets onto the stack. On a closing bracket, check whether it matches the most recent opening bracket.
function validParentheses(s) {
const stack = [];
const pairs = { ')': '(', ']': '[', '}': '{' };
for (const ch of s) {
if (ch === '(' || ch === '[' || ch === '{') stack.push(ch);
else if (stack.pop() !== pairs[ch]) return false;
}
return stack.length === 0;
}Use this when the problem asks for palindromic substrings
Every palindrome is centered either at one character for odd length or between two characters for even length. Instead of checking all substrings, expand only from valid centers. This reduces the brute-force O(n³) approach to O(n²) with very clean logic.
Problem Longest Palindromic Substring. Try each index as an odd center and each gap as an even center, and keep the longest palindrome found.
function longestPalindrome(s) {
if (s.length < 2) return s;
let best = s[0];
const expand = (l, r) => {
while (l >= 0 && r < s.length && s[l] === s[r]) {
l--; r++;
}
return s.slice(l + 1, r);
};
for (let i = 0; i < s.length; i++) {
const odd = expand(i, i);
const even = expand(i, i + 1);
if (odd.length > best.length) best = odd;
if (even.length > best.length) best = even;
}
return best;
}Use this when you need to compare characters from both ends, especially for palindrome-style checks or when skipping non-important characters
The idea is to move inward while maintaining an invariant about the substring between the pointers. This pattern is simple but appears often.
Problem Valid Palindrome. Ignore non-alphanumeric characters, compare lowercase characters from both ends, and stop early on any mismatch.
function groupAnagrams(strs) {
const groups = new Map();
for (const word of strs) {
const key = word.split('').sort().join('');
if (!groups.has(key)) groups.set(key, []);
groups.get(key).push(word);
}
return Array.from(groups.values());
}Use this when two strings should be treated as equivalent after reordering or normalization
Convert every string into a canonical key, then group or compare based on that key. For anagrams, sorting characters is the simplest canonicalization. This pattern turns grouping problems into map aggregation problems.
Problem Group Anagrams. Sort each word, use the sorted string as the key, and collect all words sharing that key.
function isPalindrome(s) {
let left = 0, right = s.length - 1;
while (left < right) {
while (left < right && !/[a-z0-9]/i.test(s[left])) left++;
while (left < right && !/[a-z0-9]/i.test(s[right])) right--;
if (s[left].toLowerCase() !== s[right].toLowerCase()) return false;
left++; right--;
}
return true;
}Strings in most languages are immutable sequences of characters
Indexing is O(1) but concatenation in a loop is O(n²) — always use an array or StringBuilder to build strings. Key operations to know: split, join, indexOf, slice, charCodeAt, and converting between char and int. Off-by-one errors and empty-string edge cases are the most common bugs.
Problem Reverse Words in a String. Split on whitespace, filter empty tokens, reverse the list, and join with a single space.
function reverseWords(s) {
return s.trim().split(/s+/).reverse().join(' ');
}Hashing a string means converting it to a fixed-size representation that enables fast equality checks
In interviews this shows up as: using a frequency count array of size 26 as a hash key, rolling hash for substring matching, or comparing sorted characters as a canonical key. The key insight is that equal hashes imply equal strings only when the hash function is carefully designed.
Problem Check if two strings are permutations. A count array of 26 characters is effectively a hash. Two strings are permutations if and only if their count arrays are identical.
function isPermutation(a, b) {
if (a.length !== b.length) return false;
const count = new Array(26).fill(0);
for (let i = 0; i < a.length; i++) {
count[a.charCodeAt(i) - 97]++;
count[b.charCodeAt(i) - 97]--;
}
return count.every(n => n === 0);
}Lexicographic order is dictionary order: compare character by character from left to right and the first mismatch determines the order
This is used in sorting strings, finding the next permutation, checking if a string comes before another, and problems that ask for the smallest or largest valid string. Always remember: shorter strings that are a prefix of longer ones come first.
Problem Largest Number. To arrange numbers so their concatenation is largest, use a custom comparator: compare string(a)+string(b) vs string(b)+string(a).
function largestNumber(nums) {
const strs = nums.map(String);
strs.sort((a, b) => (b + a).localeCompare(a + b));
return strs[0] === '0' ? '0' : strs.join('');
}