Skip to main content

Strings

Common String Problems

Written by Updated

Four patterns cover most string questions. Once you can see which one you are looking at, the code is short.

Reversing

Two ways to reverse

javascript

// Readable. O(n) time, O(n) space.
function reverse(s) {
  return [...s].reverse().join("")
}

// In place on an array of characters. O(1) extra space.
function reverseChars(chars) {
  let left = 0
  let right = chars.length - 1

  while (left < right) {
    const temp = chars[left]
    chars[left] = chars[right]
    chars[right] = temp
    left++
    right--
  }

  return chars
}

console.log(reverse("abc"))                  // "cba"
console.log(reverseChars(["a", "b", "c"]))   // ["c", "b", "a"]

Use the first unless the problem explicitly asks for constant space. Clear beats clever when the cost is the same.

Palindromes

The version interviews actually ask for ignores punctuation and case:

Palindrome, cleaned

javascript

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

  let left = 0
  let right = clean.length - 1

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

  return true
}

console.log(isPalindrome("A man, a plan, a canal: Panama"))  // true

O(n) time, O(n) space for the cleaned copy. You can do it in O(1) space by skipping non-letters with the pointers instead of cleaning first - worth knowing if you are asked.

First non-repeating character

Two passes beat nested loops

javascript

function firstUnique(s) {
  const counts = new Map()
  for (const ch of s) counts.set(ch, (counts.get(ch) || 0) + 1)

  for (let i = 0; i < s.length; i++) {
    if (counts.get(s[i]) === 1) return i
  }

  return -1
}

console.log(firstUnique("leetcode"))  // 0
console.log(firstUnique("aabb"))      // -1

Two separate passes, still O(n). Beginners often reach for a nested loop here and land on O(n squared) for no gain.

Longest run of unique characters

This is a sliding window, covered properly later - but it is the string problem that appears most often, so it is worth seeing now:

Sliding window

javascript

function longestUnique(s) {
  const lastSeen = new Map()
  let best = 0
  let start = 0

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

    if (lastSeen.has(ch) && lastSeen.get(ch) >= start) {
      start = lastSeen.get(ch) + 1
    }

    lastSeen.set(ch, i)
    best = Math.max(best, i - start + 1)
  }

  return best
}

console.log(longestUnique("abcabcbb"))  // 3

One pass. The window never moves backwards, which is what keeps it O(n) rather than O(n squared).

Counting with a fixed alphabet

When the input is promised to be lowercase English letters, a 26-element array beats a Map. Index with charCodeAt(i) - 97. It is the same O(n) but with no hashing and no object allocation, and it makes the space bound obviously O(1).

Array counting for a known alphabet

javascript

function isAnagram(a, b) {
  if (a.length !== b.length) return false

  const counts = new Array(26).fill(0)

  for (let i = 0; i < a.length; i++) {
    counts[a.charCodeAt(i) - 97]++
    counts[b.charCodeAt(i) - 97]--
  }

  return counts.every((n) => n === 0)
}

console.log(isAnagram("listen", "silent"))  // true
console.log(isAnagram("hello", "world"))    // false

One pass over both strings at once, incrementing for the first and decrementing for the second. If they match, every counter returns to zero.

Grouping anagrams

The follow-up is almost always "group a list of words into anagram sets". The move is to build a canonical key for each word - either its sorted letters or its count signature - and use that key in a Map. Each word is visited once, so it stays linear in the total number of characters.

  • Palindrome - two pointers from both ends.
  • Anagram - count, do not sort.
  • First unique - count, then scan in order.
  • Longest unique run - sliding window with last-seen indices.
  • Group anagrams - canonical key into a Map.