Working with String Matching
1. Understanding String Matching Algorithms
| Algorithm | Description | Time Complexity | Space Complexity |
|---|---|---|---|
| Brute Force | Compares every possible substring of text with pattern | O(n*m) |
O(1) |
| KMP (Knuth-Morris-Pratt) | Uses failure function to avoid redundant comparisons | O(n+m) |
O(m) |
| Rabin-Karp | Uses hash function to find pattern in text | O(n+m) average |
O(1) |
| Boyer-Moore | Skips characters based on mismatched character rules | O(n*m) worst case, O(n/m) average |
O(k) where k is alphabet size |
Example: KMP Algorithm Implementation
function buildFailureFunction(pattern) {
const failure = new Array(pattern.length).fill(0);
let j = 0;
for (let i = 1; i < pattern.length; i++) {
while (j > 0 && pattern[i] !== pattern[j]) {
j = failure[j - 1];
}
if (pattern[i] === pattern[j]) {
j++;
}
failure[i] = j;
}
return failure;
}
function kmpSearch(text, pattern) {
const failure = buildFailureFunction(pattern);
let j = 0;
for (let i = 0; i < text.length; i++) {
while (j > 0 && text[i] !== pattern[j]) {
j = failure[j - 1];
}
if (text[i] === pattern[j]) {
j++;
}
if (j === pattern.length) {
return i - pattern.length + 1;
}
}
return -1;
}
Note: The KMP algorithm is particularly useful when the pattern has overlapping subpatterns, allowing it to skip unnecessary comparisons.
2. Validating Palindromes
| Property | Description | Example |
|---|---|---|
| isPalindrome | Checks if a string is a palindrome | "racecar" |
| isPalindromeIgnoreCase | Checks if a string is a palindrome, ignoring case | "RaceCar" |
| isPalindromeWithSpaces | Checks if a string is a palindrome, ignoring spaces and punctuation | "A man, a plan, a canal: Panama" |
Example: Basic Palindrome Check
function isPalindrome(str) {
const cleanStr = str.toLowerCase();
return cleanStr === cleanStr.split('').reverse().join('');
}
console.log(isPalindrome("racecar")); // true
console.log(isPalindrome("hello")); // false
Example: Case-Insensitive Palindrome Check
function isPalindromeIgnoreCase(str) {
const cleanStr = str.toLowerCase();
return cleanStr === cleanStr.split('').reverse().join('');
}
console.log(isPalindromeIgnoreCase("RaceCar")); // true
console.log(isPalindromeIgnoreCase("Madam")); // true
Example: Palindrome with Spaces and Punctuation
function isPalindromeWithSpaces(str) {
const cleanStr = str.replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
return cleanStr === cleanStr.split('').reverse().join('');
}
console.log(isPalindromeWithSpaces("A man, a plan, a canal: Panama")); // true
console.log(isPalindromeWithSpaces("race a car")); // false
Note: The time complexity of basic palindrome checking is O(n), where n is the length of the string. Space complexity is also O(n) due to creating a reversed copy.
3. Checking for Anagrams
| Method | Description | Time Complexity | Space Complexity |
|---|---|---|---|
| Sorting Approach | Sort characters of both strings and compare. | O(n log n) |
O(n) |
| Character Frequency Count | Count character frequencies in both strings and compare. | O(n) |
O(k) where k is the number of unique characters |
| Built-in Functions (JavaScript) | Use sort() and join() to compare sorted strings. |
O(n log n) |
O(n) |
Example: Sorting Approach
function areAnagrams(str1, str2) {
// Normalize strings (lowercase, remove spaces)
const cleanStr1 = str1.toLowerCase().replace(/\s/g, '');
const cleanStr2 = str2.toLowerCase().replace(/\s/g, '');
// Sort characters and compare
const sorted1 = cleanStr1.split('').sort().join('');
const sorted2 = cleanStr2.split('').sort().join('');
return sorted1 === sorted2;
}
// Example usage
console.log(areAnagrams("listen", "silent")); // true
console.log(areAnagrams("hello", "bello")); // false
Example: Character Frequency Count
function areAnagrams(str1, str2) {
// Normalize strings (lowercase, remove spaces)
const cleanStr1 = str1.toLowerCase().replace(/\s/g, '');
const cleanStr2 = str2.toLowerCase().replace(/\s/g, '');
// Early exit if lengths differ
if (cleanStr1.length !== cleanStr2.length) return false;
// Count character frequencies
const freqMap = {};
for (let char of cleanStr1) {
freqMap[char] = (freqMap[char] || 0) + 1;
}
// Subtract frequencies from second string
for (let char of cleanStr2) {
if (!freqMap[char]) return false;
freqMap[char]--;
}
// Ensure all counts are zero
return Object.values(freqMap).every(count => count === 0);
}
// Example usage
console.log(areAnagrams("anagram", "nagaram")); // true
console.log(areAnagrams("rat", "car")); // false
Note: Anagrams must have the same length and same character frequencies, but not necessarily the same order.
4. Grouping Anagrams Together
An anagram is a word or phrase formed by rearranging the letters of another, using all the original letters exactly once. Grouping anagrams together is a common problem in string matching and algorithm design.
| Approach | Description | Time Complexity | Space Complexity |
|---|---|---|---|
| Sorting | Sort characters of each string and use the sorted string as a key in a hash map. | O(n * m log m) |
O(n * m) |
| Character Counting | Count character frequencies for each string and use the count as a key in a hash map. | O(n * m) |
O(n * m) |
Example: Using Sorting to Group Anagrams
function groupAnagrams(strs) {
const map = new Map();
for (const str of strs) {
const sortedStr = str.split('').sort().join('');
if (!map.has(sortedStr)) {
map.set(sortedStr, []);
}
map.get(sortedStr).push(str);
}
return Array.from(map.values());
}
// Example usage
const input = ["eat", "tea", "tan", "ate", "nat", "bat"];
console.log(groupAnagrams(input));
// Output: [["eat", "tea", "ate"], ["tan", "nat"], ["bat"]]
Example: Using Character Counting to Group Anagrams
function getCharCount(str) {
const count = new Array(26).fill(0);
for (const char of str) {
count[char.charCodeAt(0) - 'a'.charCodeAt(0)]++;
}
return count.join(',');
}
function groupAnagrams(strs) {
const map = new Map();
for (const str of strs) {
const key = getCharCount(str);
if (!map.has(key)) {
map.set(key, []);
}
map.get(key).push(str);
}
return Array.from(map.values());
}
// Example usage
const input = ["eat", "tea", "tan", "ate", "nat", "bat"];
console.log(groupAnagrams(input));
// Output: [["eat", "tea", "ate"], ["tan", "nat"], ["bat"]]
Note: The character counting approach assumes all strings consist of lowercase letters only. For a more general solution, sorting or using a map to count character frequencies is preferred.
5. Implementing String Compression
| Method | Description | Time Complexity | Space Complexity |
|---|---|---|---|
| Run-Length Encoding (RLE) | Encodes consecutive repeated characters with count and character | O(n) |
O(n) |
| LZ77 (Lempel-Ziv 1977) | Uses sliding window to find repeated sequences | O(n^2) worst case |
O(n) |
| LZ78 (Lempel-Ziv 1978) | Builds dictionary of phrases dynamically | O(n) |
O(d) where d is dictionary size |
Example: Run-Length Encoding Implementation
function runLengthEncode(str) {
if (!str) return "";
let result = "";
let count = 1;
let currentChar = str[0];
for (let i = 1; i <= str.length; i++) {
if (str[i] === currentChar) {
count++;
} else {
result += currentChar + count;
currentChar = str[i];
count = 1;
}
}
return result;
}
// Example usage:
console.log(runLengthEncode("aaabbc")); // "a3b2c1"
Example: LZ77 Compression
function lz77Compress(text, windowSize = 10) {
const output = [];
let i = 0;
while (i < text.length) {
let matchLength = 0;
let matchOffset = 0;
// Find longest match in sliding window
for (let j = Math.max(0, i - windowSize); j < i; j++) {
let len = 0;
while (i + len < text.length &&
text[j + len] === text[i + len]) {
len++;
}
if (len > matchLength) {
matchLength = len;
matchOffset = i - j;
}
}
if (matchLength > 0) {
output.push([matchOffset, matchLength, text[i + matchLength]]);
i += matchLength + 1;
} else {
output.push([0, 0, text[i]]);
i++;
}
}
return output;
}
// Example usage:
console.log(lz77Compress("ababab")); // [[0,0,"a"], [2,3,"b"]]
Note: Run-Length Encoding is most effective for strings with long sequences of repeated characters. LZ77 and LZ78 are more suitable for general-purpose compression.
6. Finding Longest Palindromic Substring
| Approach | Description | Time Complexity | Space Complexity |
|---|---|---|---|
| Expand Around Centers | For each possible center (both odd and even length palindromes), expand outward while characters match. | O(n²) |
O(1) |
| Manacher's Algorithm | Preprocesses string to handle even-length palindromes uniformly and uses previously computed values for optimization. | O(n) |
O(n) |
Example: Expand Around Centers
function longestPalindrome(s) {
let start = 0;
let maxLen = 1;
function expandAroundCenter(left, right) {
while (left >= 0 && right < s.length && s[left] === s[right]) {
if (right - left + 1 > maxLen) {
maxLen = right - left + 1;
start = left;
}
left--;
right++;
}
}
for (let i = 0; i < s.length; i++) {
expandAroundCenter(i, i); // Odd-length
expandAroundCenter(i, i + 1); // Even-length
}
return s.substring(start, start + maxLen);
}
console.log(longestPalindrome("babad")); // "bab" or "aba"
Example: Manacher's Algorithm
function manachers(s) {
// Preprocess string to handle even-length palindromes
let processed = '#' + s.split('').join('#') + '#';
let P = new Array(processed.length).fill(0);
let center = 0;
let right = 0;
for (let i = 0; i < processed.length; i++) {
let mirror = 2 * center - i;
if (i < right) {
P[i] = Math.min(right - i, P[mirror]);
}
try {
while (processed[i + P[i] + 1] === processed[i - P[i] - 1]) {
P[i]++;
}
} catch (e) {}
if (i + P[i] > right) {
center = i;
right = i + P[i];
}
}
let maxLen = Math.max(...P);
let centerIndex = P.indexOf(maxLen);
let start = (centerIndex - maxLen) / 2;
return s.substring(start, start + maxLen);
}
console.log(manachers("babad")); // "bab" or "aba"
Note: Manacher's algorithm is optimal for finding the longest palindromic substring in linear time, but it requires preprocessing and is more complex to understand than the expand-around-centers approach.
7. Finding Longest Palindromic Subsequence
| Property | Description | Example |
|---|---|---|
| Problem | Finds the longest subsequence that is a palindrome in a given string. | Input: "bbbab" → Output: 4 |
| Approach | Dynamic Programming (DP) - Build a 2D table to store lengths of palindromic subsequences. | |
| Time Complexity | O(n²) | |
| Space Complexity | O(n²) |
Example: Implementation using Dynamic Programming
function longestPalindromeSubseq(s) {
const n = s.length;
const dp = Array(n).fill().map(() => Array(n).fill(0));
// Every single character is a palindrome of length 1
for (let i = 0; i < n; i++) {
dp[i][i] = 1;
}
// Fill the table for substrings of length 2 to n
for (let len = 2; len <= n; len++) {
for (let i = 0; i <= n - len; i++) {
const j = i + len - 1;
if (s[i] === s[j]) {
dp[i][j] = dp[i + 1][j - 1] + 2;
} else {
dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
}
}
}
return dp[0][n - 1];
}
// Example usage
console.log(longestPalindromeSubseq("bbbab")); // Output: 4
Note: The longest palindromic subsequence is not necessarily a substring. It's a sequence of characters that maintains relative order but does not need to be contiguous.
8. Implementing KMP Algorithm
| Component | Description | Details |
|---|---|---|
| Pattern | String to search for | "ABABCABAA" |
| Text | String to search in | "ABABDABACDABABCABAA" |
| LPS Array | Longest Proper Prefix which is also Suffix | [0, 0, 1, 2, 0, 1, 2, 3, 4, 0, 1] |
Example: KMP Algorithm Implementation in JavaScript
function buildLPSArray(pattern) {
const lps = new Array(pattern.length).fill(0);
let len = 0;
let i = 1;
while (i < pattern.length) {
if (pattern[i] === pattern[len]) {
len++;
lps[i] = len;
i++;
} else {
if (len !== 0) {
len = lps[len - 1];
} else {
lps[i] = 0;
i++;
}
}
}
return lps;
}
function KMPSearch(text, pattern) {
const lps = buildLPSArray(pattern);
let i = 0;
let j = 0;
while (i < text.length) {
if (pattern[j] === text[i]) {
i++;
j++;
}
if (j === pattern.length) {
console.log("Pattern found at index " + (i - j));
j = lps[j - 1];
} else if (i < text.length && pattern[j] !== text[i]) {
if (j !== 0) {
j = lps[j - 1];
} else {
i++;
}
}
}
}
// Example usage:
const text = "ABABDABACDABABCABAA";
const pattern = "ABABCABAA";
KMPSearch(text, pattern);
Note: The KMP algorithm preprocesses the pattern to build an LPS (Longest Proper Prefix which is also Suffix) array, enabling efficient pattern matching without backtracking in the text.
9. Implementing Rabin-Karp Algorithm
The Rabin-Karp algorithm is a string-searching algorithm that uses hashing to find any one of a set of pattern strings in a text. It is particularly useful when searching for multiple patterns simultaneously.
| Feature | Description | Details |
|---|---|---|
| Time Complexity | Average Case | O(n + m) where n is the length of text and m is the length of pattern |
| Space Complexity | Space Required | O(1) for basic implementation |
| Hash Function | Used for Pattern Matching | Rolling hash function to compute hash values efficiently |
Example: Basic Implementation of Rabin-Karp
function rabinKarp(text, pattern) {
const n = text.length;
const m = pattern.length;
const d = 256; // Number of characters in the alphabet
const q = 101; // A prime number for modulo operation
let h = 1;
for (let i = 0; i < m - 1; i++) {
h = (h * d) % q;
}
let p = 0, t = 0;
for (let i = 0; i < m; i++) {
p = (d * p + pattern.charCodeAt(i)) % q;
t = (d * t + text.charCodeAt(i)) % q;
}
for (let i = 0; i <= n - m; i++) {
if (p === t) {
let j;
for (j = 0; j < m; j++) {
if (text[i + j] !== pattern[j]) break;
}
if (j === m) {
return i; // Pattern found at index i
}
}
if (i < n - m) {
t = (d * (t - text.charCodeAt(i) * h) + text.charCodeAt(i + m)) % q;
if (t < 0) t += q;
}
}
return -1; // Pattern not found
}
// Example usage:
const text = "ABABCABABA";
const pattern = "ABABA";
console.log(rabinKarp(text, pattern)); // Output: 5
Note: The Rabin-Karp algorithm uses a rolling hash to efficiently compute the hash of each substring. It is efficient for multiple pattern searches but can have worst-case performance O(nm) if hash collisions occur frequently.
10. Finding Longest Repeating Substring
| Approach | Description | Time Complexity | Space Complexity |
|---|---|---|---|
| Brute Force | Check all possible substrings for repetition using nested loops. | O(n^3) |
O(1) |
| Suffix Array | Build a suffix array and find the longest common prefix (LCP) between adjacent suffixes. | O(n log n) |
O(n) |
| Rolling Hash (Rabin-Karp) | Use hash functions to detect repeated substrings efficiently with sliding window. | O(n^2) average |
O(n) |
Example: Brute Force Approach
function longestRepeatingSubstring(s) {
let result = '';
for (let i = 0; i < s.length; i++) {
for (let j = i + 1; j < s.length; j++) {
let substr = s.substring(i, j);
if (s.indexOf(substr, j) !== -1 && substr.length > result.length) {
result = substr;
}
}
}
return result;
}
console.log(longestRepeatingSubstring("banana")); // "ana"
Example: Rolling Hash Approach
function longestRepeatingSubstring(s) {
let n = s.length;
let left = 1, right = n - 1;
let result = '';
while (left <= right) {
let mid = Math.floor((left + right) / 2);
let hashSet = new Set();
let hash = 0;
let base = 256;
let power = 1;
for (let i = 0; i < mid; i++) {
power = (power * base) % 1000000007;
}
for (let i = 0; i < mid; i++) {
hash = (hash * base + s.charCodeAt(i)) % 1000000007;
}
hashSet.add(hash);
let found = false;
for (let i = mid; i < n; i++) {
hash = (hash * base - s.charCodeAt(i - mid) * power + s.charCodeAt(i)) % 1000000007;
if (hashSet.has(hash)) {
found = true;
result = s.substring(i - mid + 1, i + 1);
break;
}
hashSet.add(hash);
}
if (found) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return result;
}
console.log(longestRepeatingSubstring("abcdefabc")); // "abc"
Note: The rolling hash approach uses binary search over the length of the substring and a hash set to efficiently check for repeating patterns.
11. Finding Distinct Subsequences
| Property | Description | Example |
|---|---|---|
| distinctSubsequences | Counts the number of distinct subsequences of string T in string S | distinctSubsequences("rabbbit", "rabbit") = 3 |
Example: Counting Distinct Subsequences
function distinctSubsequences(s, t) {
const m = s.length;
const n = t.length;
const dp = Array(m + 1).fill().map(() => Array(n + 1).fill(0));
for (let i = 0; i <= m; i++) {
dp[i][0] = 1;
}
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
dp[i][j] = dp[i - 1][j];
if (s[i - 1] === t[j - 1]) {
dp[i][j] += dp[i - 1][j - 1];
}
}
}
return dp[m][n];
}
// Example usage:
console.log(distinctSubsequences("rabbbit", "rabbit")); // Output: 3
Note: The algorithm uses dynamic programming where
dp[i][j] represents the number of distinct subsequences of the first j characters of t in the first i characters of s.
12. Implementing Trie for Prefix Matching
Example: Basic Trie Implementation
class TrieNode {
constructor() {
this.children = {};
this.isEndOfWord = false;
}
}
class Trie {
constructor() {
this.root = new TrieNode();
}
insert(word) {
let node = this.root;
for (let char of word) {
if (!node.children[char]) {
node.children[char] = new TrieNode();
}
node = node.children[char];
}
node.isEndOfWord = true;
}
search(word) {
let node = this.root;
for (let char of word) {
if (!node.children[char]) return false;
node = node.children[char];
}
return node.isEndOfWord;
}
startsWith(prefix) {
let node = this.root;
for (let char of prefix) {
if (!node.children[char]) return false;
node = node.children[char];
}
return true;
}
}
// Usage
const trie = new Trie();
trie.insert("apple");
console.log(trie.search("apple")); // true
console.log(trie.startsWith("app")); // true
| Method | Description | Time Complexity | Space Complexity |
|---|---|---|---|
| insert(word) | Inserts a word into the trie | O(m) | O(m × N) |
| search(word) | Checks if a word exists in the trie | O(m) | O(1) |
| startsWith(prefix) | Checks if any word starts with the given prefix | O(m) | O(1) |
Note: m is the length of the word/prefix, and N is the size of the alphabet.