String Algorithms - Complete Guide

String algorithms are fundamental for text processing, pattern matching, and manipulation. This guide covers essential string algorithms and common problems.

String Basics #

Character Operations #

const str = 'Hello';

// Character access
console.log(str[0]);           // 'H'
console.log(str.charAt(0));    // 'H'
console.log(str.charCodeAt(0)); // 72

// String is immutable
str[0] = 'h';  // No effect
console.log(str);  // 'Hello'

// Convert to array for manipulation
const arr = str.split('');
arr[0] = 'h';
console.log(arr.join(''));  // 'hello'

String Comparison #

function compareStrings(s1, s2) {
  if (s1 === s2) return 0;
  return s1 < s2 ? -1 : 1;
}

console.log(compareStrings('abc', 'abd'));  // -1
console.log(compareStrings('abc', 'abc'));  // 0
console.log(compareStrings('abd', 'abc'));  // 1

Pattern Matching #

Naive Pattern Matching #

function naiveSearch(text, pattern) {
  const n = text.length;
  const m = pattern.length;
  const positions = [];

  for (let i = 0; i <= n - m; i++) {
    let j;
    for (j = 0; j < m; j++) {
      if (text[i + j] !== pattern[j]) {
        break;
      }
    }
    if (j === m) {
      positions.push(i);
    }
  }

  return positions;
}

console.log(naiveSearch('ababcabcab', 'abc'));
// [2, 5]

KMP Algorithm #

Knuth-Morris-Pratt for efficient pattern matching.

function computeLPS(pattern) {
  const m = pattern.length;
  const lps = new Array(m).fill(0);
  let len = 0;
  let i = 1;

  while (i < m) {
    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 n = text.length;
  const m = pattern.length;
  const lps = computeLPS(pattern);
  const positions = [];

  let i = 0;  // text index
  let j = 0;  // pattern index

  while (i < n) {
    if (text[i] === pattern[j]) {
      i++;
      j++;
    }

    if (j === m) {
      positions.push(i - j);
      j = lps[j - 1];
    } else if (i < n && text[i] !== pattern[j]) {
      if (j !== 0) {
        j = lps[j - 1];
      } else {
        i++;
      }
    }
  }

  return positions;
}

console.log(kmpSearch('ababcabcab', 'abc'));
// [2, 5]

Rabin-Karp Algorithm #

Rolling hash for pattern matching.

function rabinKarp(text, pattern) {
  const n = text.length;
  const m = pattern.length;
  const d = 256;  // Number of characters
  const q = 101;  // Prime number
  const positions = [];

  let p = 0;  // Pattern hash
  let t = 0;  // Text hash
  let h = 1;

  // h = d^(m-1) % q
  for (let i = 0; i < m - 1; i++) {
    h = (h * d) % q;
  }

  // Calculate initial hash values
  for (let i = 0; i < m; i++) {
    p = (d * p + pattern.charCodeAt(i)) % q;
    t = (d * t + text.charCodeAt(i)) % q;
  }

  // Slide pattern over text
  for (let i = 0; i <= n - m; i++) {
    if (p === t) {
      // Hash match - verify actual characters
      let match = true;
      for (let j = 0; j < m; j++) {
        if (text[i + j] !== pattern[j]) {
          match = false;
          break;
        }
      }
      if (match) {
        positions.push(i);
      }
    }

    // Calculate hash for next window
    if (i < n - m) {
      t = (d * (t - text.charCodeAt(i) * h) + text.charCodeAt(i + m)) % q;
      if (t < 0) {
        t += q;
      }
    }
  }

  return positions;
}

console.log(rabinKarp('ababcabcab', 'abc'));
// [2, 5]

Substring Problems #

Longest Substring Without Repeating Characters #

function lengthOfLongestSubstring(s) {
  const seen = new Map();
  let maxLen = 0;
  let start = 0;

  for (let end = 0; end < s.length; end++) {
    const char = s[end];

    if (seen.has(char) && seen.get(char) >= start) {
      start = seen.get(char) + 1;
    }

    seen.set(char, end);
    maxLen = Math.max(maxLen, end - start + 1);
  }

  return maxLen;
}

console.log(lengthOfLongestSubstring('abcabcbb'));  // 3 ('abc')
console.log(lengthOfLongestSubstring('bbbbb'));     // 1 ('b')
console.log(lengthOfLongestSubstring('pwwkew'));    // 3 ('wke')

Longest Palindromic Substring #

function longestPalindrome(s) {
  if (s.length < 2) return s;

  let start = 0;
  let maxLen = 0;

  function expandAroundCenter(left, right) {
    while (left >= 0 && right < s.length && s[left] === s[right]) {
      left--;
      right++;
    }
    return right - left - 1;
  }

  for (let i = 0; i < s.length; i++) {
    // Odd length palindrome
    const len1 = expandAroundCenter(i, i);
    // Even length palindrome
    const len2 = expandAroundCenter(i, i + 1);

    const len = Math.max(len1, len2);

    if (len > maxLen) {
      maxLen = len;
      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'

Minimum Window Substring #

function minWindow(s, t) {
  const need = new Map();
  const window = new Map();

  for (const char of t) {
    need.set(char, (need.get(char) || 0) + 1);
  }

  let left = 0;
  let right = 0;
  let valid = 0;
  let start = 0;
  let len = Infinity;

  while (right < s.length) {
    const char = s[right];
    right++;

    if (need.has(char)) {
      window.set(char, (window.get(char) || 0) + 1);
      if (window.get(char) === need.get(char)) {
        valid++;
      }
    }

    while (valid === need.size) {
      if (right - left < len) {
        start = left;
        len = right - left;
      }

      const leftChar = s[left];
      left++;

      if (need.has(leftChar)) {
        if (window.get(leftChar) === need.get(leftChar)) {
          valid--;
        }
        window.set(leftChar, window.get(leftChar) - 1);
      }
    }
  }

  return len === Infinity ? '' : s.substring(start, start + len);
}

console.log(minWindow('ADOBECODEBANC', 'ABC'));  // 'BANC'

Anagram Problems #

Valid Anagram #

function isAnagram(s, t) {
  if (s.length !== t.length) return false;

  const count = new Map();

  for (const char of s) {
    count.set(char, (count.get(char) || 0) + 1);
  }

  for (const char of t) {
    if (!count.has(char)) return false;
    count.set(char, count.get(char) - 1);
    if (count.get(char) < 0) return false;
  }

  return true;
}

console.log(isAnagram('anagram', 'nagaram'));  // true
console.log(isAnagram('rat', 'car'));          // false

Group Anagrams #

function groupAnagrams(strs) {
  const map = new Map();

  for (const str of strs) {
    const key = str.split('').sort().join('');

    if (!map.has(key)) {
      map.set(key, []);
    }
    map.get(key).push(str);
  }

  return Array.from(map.values());
}

console.log(groupAnagrams(['eat', 'tea', 'tan', 'ate', 'nat', 'bat']));
// [['eat', 'tea', 'ate'], ['tan', 'nat'], ['bat']]

String Transformation #

Longest Common Subsequence #

function longestCommonSubsequence(text1, text2) {
  const m = text1.length;
  const n = text2.length;
  const dp = Array(m + 1).fill(0).map(() => Array(n + 1).fill(0));

  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      if (text1[i - 1] === text2[j - 1]) {
        dp[i][j] = dp[i - 1][j - 1] + 1;
      } else {
        dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
      }
    }
  }

  return dp[m][n];
}

console.log(longestCommonSubsequence('abcde', 'ace'));  // 3 ('ace')

Edit Distance #

function minDistance(word1, word2) {
  const m = word1.length;
  const n = word2.length;
  const dp = Array(m + 1).fill(0).map(() => Array(n + 1).fill(0));

  // Initialize first row and column
  for (let i = 0; i <= m; i++) dp[i][0] = i;
  for (let j = 0; j <= n; j++) dp[0][j] = j;

  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      if (word1[i - 1] === word2[j - 1]) {
        dp[i][j] = dp[i - 1][j - 1];
      } else {
        dp[i][j] = Math.min(
          dp[i - 1][j] + 1,     // Delete
          dp[i][j - 1] + 1,     // Insert
          dp[i - 1][j - 1] + 1  // Replace
        );
      }
    }
  }

  return dp[m][n];
}

console.log(minDistance('horse', 'ros'));  // 3
console.log(minDistance('intention', 'execution'));  // 5

Interleaving String #

function isInterleave(s1, s2, s3) {
  const m = s1.length;
  const n = s2.length;

  if (m + n !== s3.length) return false;

  const dp = Array(m + 1).fill(false).map(() => Array(n + 1).fill(false));
  dp[0][0] = true;

  // First column
  for (let i = 1; i <= m; i++) {
    dp[i][0] = dp[i - 1][0] && s1[i - 1] === s3[i - 1];
  }

  // First row
  for (let j = 1; j <= n; j++) {
    dp[0][j] = dp[0][j - 1] && s2[j - 1] === s3[j - 1];
  }

  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      dp[i][j] = (dp[i - 1][j] && s1[i - 1] === s3[i + j - 1]) ||
                 (dp[i][j - 1] && s2[j - 1] === s3[i + j - 1]);
    }
  }

  return dp[m][n];
}

console.log(isInterleave('aabcc', 'dbbca', 'aadbbcbcac'));  // true

String Reversal #

Reverse String #

function reverseString(s) {
  let left = 0;
  let right = s.length - 1;

  while (left < right) {
    [s[left], s[right]] = [s[right], s[left]];
    left++;
    right--;
  }

  return s;
}

const arr = ['h', 'e', 'l', 'l', 'o'];
reverseString(arr);
console.log(arr);  // ['o', 'l', 'l', 'e', 'h']

Reverse Words #

function reverseWords(s) {
  return s.trim()
    .split(/\s+/)
    .reverse()
    .join(' ');
}

console.log(reverseWords('  hello   world  '));  // 'world hello'

Reverse Words III #

Reverse each word individually.

function reverseWordsIII(s) {
  return s.split(' ')
    .map(word => word.split('').reverse().join(''))
    .join(' ');
}

console.log(reverseWordsIII("Let's take LeetCode contest"));
// "s'teL ekat edoCteeL tsetnoc"

String Encoding #

Encode and Decode Strings #

class Codec {
  encode(strs) {
    return strs.map(s => `${s.length}#${s}`).join('');
  }

  decode(s) {
    const result = [];
    let i = 0;

    while (i < s.length) {
      // Find the delimiter
      let j = i;
      while (s[j] !== '#') j++;

      const length = parseInt(s.substring(i, j));
      const str = s.substring(j + 1, j + 1 + length);
      result.push(str);

      i = j + 1 + length;
    }

    return result;
  }
}

const codec = new Codec();
const encoded = codec.encode(['hello', 'world']);
console.log(encoded);  // '5#hello5#world'
console.log(codec.decode(encoded));  // ['hello', 'world']

String Compression #

function compress(chars) {
  let write = 0;
  let read = 0;

  while (read < chars.length) {
    const char = chars[read];
    let count = 0;

    while (read < chars.length && chars[read] === char) {
      read++;
      count++;
    }

    chars[write++] = char;

    if (count > 1) {
      for (const digit of String(count)) {
        chars[write++] = digit;
      }
    }
  }

  return write;
}

const chars = ['a', 'a', 'b', 'b', 'c', 'c', 'c'];
const len = compress(chars);
console.log(chars.slice(0, len));  // ['a', '2', 'b', '2', 'c', '3']

Palindrome Problems #

Valid Palindrome #

function isPalindrome(s) {
  s = s.toLowerCase().replace(/[^a-z0-9]/g, '');

  let left = 0;
  let right = s.length - 1;

  while (left < right) {
    if (s[left] !== s[right]) {
      return false;
    }
    left++;
    right--;
  }

  return true;
}

console.log(isPalindrome('A man, a plan, a canal: Panama'));  // true
console.log(isPalindrome('race a car'));  // false

Longest Palindromic Subsequence #

function longestPalindromeSubseq(s) {
  const n = s.length;
  const dp = Array(n).fill(0).map(() => Array(n).fill(0));

  // Every single character is a palindrome
  for (let i = 0; i < n; i++) {
    dp[i][i] = 1;
  }

  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];
}

console.log(longestPalindromeSubseq('bbbab'));  // 4 ('bbbb')

Time Complexity #

AlgorithmTime ComplexitySpace Complexity
Naive SearchO(n*m)O(1)
KMPO(n + m)O(m)
Rabin-KarpO(n + m) avg, O(n*m) worstO(1)
Longest SubstringO(n)O(min(n, k))
LCSO(n*m)O(n*m)
Edit DistanceO(n*m)O(n*m)

n = text length, m = pattern length, k = character set size

Best Practices #

  1. Use appropriate algorithm - KMP for pattern matching, sliding window for substrings
  2. Consider character set - ASCII vs Unicode affects space
  3. Hash for anagrams - Sorted string or character count
  4. Two pointers - Efficient for palindromes and reversal
  5. Dynamic programming - LCS, edit distance need DP
  6. Sliding window - Substring problems often use this
  7. Handle edge cases - Empty strings, single characters
  8. Space optimization - DP can often use O(n) instead of O(n²)

String algorithms are essential for text processing. Master these patterns to solve a wide range of string problems efficiently.