Palindrome Detection Algorithms: Techniques and Trade-offs

May 28, 20266 min read

A palindrome reads the same forward and backward — "racecar", "madam", and "A man, a plan, a canal: Panama" are all palindromes. Detecting them is a classic programming exercise, but the problem reveals interesting trade-offs in algorithm design, character handling, and real-world text processing.

The Naive Approach: Reverse and Compare

The simplest algorithm reverses the string and checks for equality:

function isPalindrome(str) {
  const reversed = str.split('').reverse().join('')
  return str === reversed
}

This works for clean inputs like "racecar", but fails on real-world text that includes spaces, punctuation, and mixed case. It also creates a full copy of the string, using O(n) extra memory.

Time complexity: O(n). Space complexity: O(n).

The Two-Pointer Technique

A more efficient approach uses two pointers — one starting at the beginning, one at the end — moving inward and comparing characters:

function isPalindrome(str) {
  let left = 0
  let right = str.length - 1

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

  return true
}

This halves the number of comparisons and uses O(1) extra memory. No string copy is needed.

Time complexity: O(n). Space complexity: O(1).

Ignoring Non-Alphanumeric Characters

Real-world palindromes like "A man, a plan, a canal: Panama" contain spaces and punctuation that should be ignored. You can handle this by preprocessing the string or by skipping non-alphanumeric characters in the two-pointer loop:

function isPalindrome(str) {
  let left = 0
  let right = str.length - 1

  while (left < right) {
    while (left < right && !isAlphaNumeric(str[left])) left++
    while (left < right && !isAlphaNumeric(str[right])) right--

    if (str[left].toLowerCase() !== str[right].toLowerCase()) return false

    left++
    right--
  }

  return true
}

function isAlphaNumeric(char) {
  return /[a-z0-9]/i.test(char)
}

This handles mixed case, spaces, and punctuation without creating a filtered copy of the string.

Unicode Considerations

JavaScript strings use UTF-16 encoding. Characters outside the Basic Multilingual Plane (emoji, some CJK characters) are represented as surrogate pairs. Naive reversal breaks these characters:

// Broken: splits surrogate pairs
'top🥞pot'.split('').reverse().join('')  // 'top��pot' (broken pancake emoji)

// Correct: use Array.from or spread syntax
[...'top🥞pot'].reverse().join('')  // 'top🥞pot' (intact)

For palindrome detection, the two-pointer approach naturally handles surrogate pairs because it compares characters from the outside in without reversing.

However, Unicode normalization adds another layer. The character é can be represented as a single code point (U+00E9) or as e + combining accent (U+0065 U+0301). These look identical but are not equal in string comparison:

const a = '\u00E9'       // é (precomposed)
const b = 'e\u0301'      // é (decomposed)
a === b                  // false!

// Normalize before comparing
a.normalize('NFC') === b.normalize('NFC')  // true

For palindrome checking of user-submitted text, normalize the input first.

Recursive Approach

Palindrome detection can be expressed recursively — a string is a palindrome if its first and last characters match and the substring between them is also a palindrome:

function isPalindrome(str, left = 0, right = str.length - 1) {
  if (left >= right) return true
  if (str[left] !== str[right]) return false
  return isPalindrome(str, left + 1, right - 1)
}

This is elegant but risks stack overflow on very long strings (JavaScript's call stack limit is typically around 10,000–25,000 frames). The iterative two-pointer approach is safer for production use.

Comparative Summary

AlgorithmTimeSpaceNotes
Reverse and compareO(n)O(n)Simple, allocates copy
Two-pointerO(n)O(1)Best for most use cases
Two-pointer with skipO(n)O(1)Handles punctuation/case
RecursiveO(n)O(n) stackElegant, stack overflow risk

For most practical applications, the two-pointer approach with character skipping strikes the best balance of clarity, performance, and correctness.

Key Takeaways

  • The two-pointer technique detects palindromes in O(n) time with O(1) space — no string copy needed
  • Real-world palindrome checking must handle case, spaces, and punctuation by skipping non-alphanumeric characters
  • UTF-16 surrogate pairs break naive string reversal — use Array.from() or spread syntax
  • Unicode normalization (NFC) ensures accented characters compare correctly
  • Recursive approaches are elegant but risk stack overflow on long strings
  • The two-pointer method with character skipping is the best general-purpose solution

Try It Yourself

Test your strings instantly with our free Text Reverser tool. Enter any text, see it reversed — and discover palindromes by comparing the original and reversed output side by side.