DSAString Algorithms

String Algorithms

String algorithms are the bread and butter of coding interviews. This page walks through eight classic problems — each teaching a reusable technique that appears again and again.

1. Reverse a String

The classic warm-up. The interview expects O(n) time, O(1) extra space using two pointers that swap from both ends and meet in the middle.

JS
// In-place two-pointer reversal (array input, LeetCode 344)
function reverseString(s) {
  let left = 0, right = s.length - 1;
  while (left < right) {
    [s[left], s[right]] = [s[right], s[left]];
    left++;
    right--;
  }
}

// If you receive a plain string (immutable), convert first
function reverseStr(s) {
  return s.split('').reverse().join('');
  // or manually:
  // const arr = s.split('');
  // let l = 0, r = arr.length - 1;
  // while (l < r) { [arr[l], arr[r]] = [arr[r], arr[l]]; l++; r--; }
  // return arr.join('');
}

console.log(reverseStr('hello'));   // 'olleh'
console.log(reverseStr('abcde'));   // 'edcba'
Note
Time: O(n). Space: O(1) for the in-place array version, O(n) if you must return a new string (unavoidable in JS since strings are immutable).
2. Valid Palindrome

A string is a palindrome if it reads the same forwards and backwards after ignoring non-alphanumeric characters and case differences (LeetCode 125). Two pointers let you check in a single pass without allocating a cleaned copy.

JS
function isPalindrome(s) {
  // helper: is the character alphanumeric?
  const isAlphaNum = ch => /[a-z0-9]/.test(ch.toLowerCase());

  let left = 0, right = s.length - 1;
  while (left < right) {
    // skip non-alphanumeric from both ends
    while (left < right && !isAlphaNum(s[left]))  left++;
    while (left < right && !isAlphaNum(s[right])) right--;

    if (s[left].toLowerCase() !== s[right].toLowerCase()) return false;
    left++;
    right--;
  }
  return true;
}

console.log(isPalindrome("A man, a plan, a canal: Panama")); // true
console.log(isPalindrome("race a car"));                     // false
console.log(isPalindrome(" "));                              // true
3. String Compression (Run-Length Encoding)

Given a string like "aaabbc", compress consecutive repeated characters to "a3b2c1". If the compressed string is not shorter, return the original.

JS
function compress(s) {
  if (!s.length) return s;

  const result = [];
  let i = 0;

  while (i < s.length) {
    const ch = s[i];
    let count = 0;
    // count consecutive occurrences
    while (i < s.length && s[i] === ch) {
      i++;
      count++;
    }
    result.push(ch);
    if (count > 1) result.push(count);
  }

  const compressed = result.join('');
  return compressed.length < s.length ? compressed : s;
}

console.log(compress('aaabbc'));   // 'a3b2c1' — wait, c1 is same length
// Actually: 'a3b2c1'.length === 6 === 'aaabbc'.length, return original
console.log(compress('aaaaabbb')); // 'a5b3' (shorter, return it)
console.log(compress('abc'));      // 'abc'  (a1b1c1 longer, return original)

// LeetCode 443 variant: compress in-place with two pointers
function compressInPlace(chars) {
  let write = 0, i = 0;
  while (i < chars.length) {
    const ch = chars[i];
    let count = 0;
    while (i < chars.length && chars[i] === ch) { i++; count++; }
    chars[write++] = ch;
    if (count > 1) {
      for (const digit of String(count)) {
        chars[write++] = digit;
      }
    }
  }
  return write; // new length
}
4. Longest Common Prefix

Find the longest string that is a prefix of all strings in the array (LeetCode 14). The vertical scan approach checks each column position across all strings at once.

JS
function longestCommonPrefix(strs) {
  if (!strs.length) return '';

  for (let col = 0; col < strs[0].length; col++) {
    const ch = strs[0][col];
    for (let row = 1; row < strs.length; row++) {
      // mismatch: current column doesn't match, OR string is too short
      if (col >= strs[row].length || strs[row][col] !== ch) {
        return strs[0].slice(0, col);
      }
    }
  }
  return strs[0]; // all characters matched
}

console.log(longestCommonPrefix(['flower','flow','flight'])); // 'fl'
console.log(longestCommonPrefix(['dog','racecar','car']));    // ''
console.log(longestCommonPrefix(['abc']));                    // 'abc'
Note
Time: O(S) where S is the total number of characters across all strings. Space: O(1) extra.
5. Isomorphic Strings

Two strings are isomorphic if the characters of one can be replaced consistently to produce the other (LeetCode 205). No two characters may map to the same character, but a character may map to itself. Use two maps — one for each direction — to enforce the bijection.

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

  const sToT = new Map(); // s char → t char
  const tToS = new Map(); // t char → s char

  for (let i = 0; i < s.length; i++) {
    const sc = s[i], tc = t[i];

    if (sToT.has(sc) && sToT.get(sc) !== tc) return false;
    if (tToS.has(tc) && tToS.get(tc) !== sc) return false;

    sToT.set(sc, tc);
    tToS.set(tc, sc);
  }
  return true;
}

console.log(isIsomorphic('egg', 'add'));  // true  (e→a, g→d)
console.log(isIsomorphic('foo', 'bar'));  // false (o maps to both a and r)
console.log(isIsomorphic('paper', 'title')); // true
6. Group Anagrams

Given an array of strings, group all anagrams together (LeetCode 49). Key insight: anagrams are equal when sorted. Use the sorted string as a Map key.

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

  for (const s of strs) {
    const key = s.split('').sort().join(''); // canonical form
    if (!map.has(key)) map.set(key, []);
    map.get(key).push(s);
  }

  return [...map.values()];
}

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

// O(n) alternative: encode frequency as key instead of sorting
function groupAnagramsLinear(strs) {
  const map = new Map();
  const a = 'a'.charCodeAt(0);

  for (const s of strs) {
    const freq = new Array(26).fill(0);
    for (const ch of s) freq[ch.charCodeAt(0) - a]++;
    const key = freq.join('#'); // e.g. "1#0#0...#1#..."
    if (!map.has(key)) map.set(key, []);
    map.get(key).push(s);
  }
  return [...map.values()];
}
Tip
The sort-based approach is O(n · k log k) where k is the max string length. The frequency-array approach is O(n · k) — better when strings are long and the alphabet is fixed.
7. String Rotation Check

Given two strings s1 and s2, check if s2 is a rotation of s1 (e.g. "abcde""cdeab").

The elegant trick: if you double s1 to get s1+s1, then every rotation of s1 appears as a contiguous substring inside s1+s1.

JS
function isRotation(s1, s2) {
  if (s1.length !== s2.length) return false;
  return (s1 + s1).includes(s2);
  // For production, replace .includes with KMP for O(n) guarantee
}

console.log(isRotation('abcde', 'cdeab')); // true
console.log(isRotation('abcde', 'abced')); // false
console.log(isRotation('abc',   'cab'));   // true

// Why it works:
// s1 = "abcde",  s1+s1 = "abcdeabcde"
// All rotations: abcde, bcdea, cdeab, deabc, eabcd — all substrings of s1+s1
8. Implement strStr (indexOf)

Find the first occurrence of needle in haystack (LeetCode 28). The naive approach is O(n·m). The built-in indexOf is fine for interviews unless the problem explicitly asks for a sub-quadratic implementation — then use KMP (see Pattern Matching page).

JS
// Naive O(n·m) — acceptable for most interviews
function strStr(haystack, needle) {
  if (needle.length === 0) return 0;
  const n = haystack.length, m = needle.length;

  for (let i = 0; i <= n - m; i++) {
    let j = 0;
    while (j < m && haystack[i + j] === needle[j]) j++;
    if (j === m) return i; // full match found
  }
  return -1;
}

console.log(strStr('hello', 'll'));    // 2
console.log(strStr('aaaaa', 'bba'));   // -1
console.log(strStr('', ''));           // 0

// One-liner using built-in (same semantics)
const strStrBuiltin = (h, n) => h.indexOf(n);
Complexity Summary

Algorithm

Time

Space

Key Technique

Reverse String

O(n)

O(1)

Two pointers swap

Valid Palindrome

O(n)

O(1)

Two pointers skip non-alnum

String Compression

O(n)

O(n)

Linear scan counting run

Longest Common Prefix

O(n·m)

O(1)

Vertical column scan

Isomorphic Strings

O(n)

O(1)*

Bidirectional Map

Group Anagrams

O(n·k log k)

O(n·k)

Sorted key Map

String Rotation

O(n)

O(n)

Doubling trick + contains

strStr (naive)

O(n·m)

O(1)

Sliding window compare

Note
* Isomorphic Strings: O(1) space only if alphabet is bounded (e.g., ASCII). For Unicode use O(n) space.
Practice Problems
  • LeetCode 344 — Reverse String

  • LeetCode 125 — Valid Palindrome

  • LeetCode 443 — String Compression

  • LeetCode 14 — Longest Common Prefix

  • LeetCode 205 — Isomorphic Strings

  • LeetCode 49 — Group Anagrams

  • LeetCode 796 — Rotate String (rotation check)

  • LeetCode 28 — Find the Index of the First Occurrence in a String (strStr)