How to Reverse a String
Reversing a string is one of the most common interview questions and a surprisingly deep problem when you factor in Unicode. The basic idea — flip the order of characters — works perfectly for ASCII text. But once emoji, accented letters, or combined scripts enter the picture, a naive reversal produces broken output.
This guide covers the correct approach for every language, explains why Unicode makes this tricky, and shows how to extend reversal into related tasks like palindrome detection and word-order reversal.
The Naive Approach
The simplest method is to split the string into an array of characters, reverse the array, and join it back:
// JavaScript (ASCII only — breaks with emoji)
"hello".split("").reverse().join("") // "olleh"
This works perfectly for plain ASCII. The problem arises with multi-codepoint Unicode characters.
Why Unicode Breaks Naive Reversal
Modern text is not just bytes or even code points — it is grapheme clusters: what a user perceives as a single character. A single grapheme can span multiple Unicode code points:
| Visual | Code points | Notes |
|---|---|---|
é |
U+0065 + U+0301 |
e + combining acute accent (NFD form) |
👨👩👧 |
5 code points joined by ZWJ | Family emoji |
🏳️🌈 |
4 code points + variation selector | Rainbow flag |
ñ |
U+00F1 (NFC) or U+006E + U+0303 (NFD) |
Depends on normalization |
Reversing code points instead of grapheme clusters splits these characters apart, producing unrecognisable output:
"café".split("").reverse().join("")
// Might produce "éfac" correctly (NFC) or "́efac" broken (NFD)
"😀👍".split("").reverse().join("")
// "👍😀" — looks fine here, but only by luck (both BMP supplementary)
"👨👩👧".split("").reverse().join("")
// Complete garbage — ZWJ sequences destroyed
The Correct Approach: Grapheme-Aware Reversal
JavaScript
Use the Intl.Segmenter API (available in all modern browsers and Node.js 16+):
function reverseString(str) {
const segmenter = new Intl.Segmenter();
const graphemes = [...segmenter.segment(str)].map(s => s.segment);
return graphemes.reverse().join("");
}
reverseString("café") // "éfac" (correct)
reverseString("hello 👋") // "👋 olleh" (correct)
reverseString("👨👩👧") // "👨👩👧" (correct — family stays intact)
reverseString("hello") // "olleh"
For environments without Intl.Segmenter, use the graphemer npm package:
import Graphemer from "graphemer";
function reverseString(str) {
const splitter = new Graphemer();
return splitter.splitGraphemes(str).reverse().join("");
}
Python
Python 3 strings are sequences of Unicode code points, so [::-1] works correctly for most text — but not for ZWJ sequences (multi-codepoint emoji):
# Works for most text
def reverse_string(s: str) -> str:
return s[::-1]
print(reverse_string("hello")) # "olleh"
print(reverse_string("café")) # "éfac" ✓ (NFC composed)
For full grapheme-cluster support (including ZWJ emoji), use the grapheme package:
# pip install grapheme
import grapheme
def reverse_string_unicode(s: str) -> str:
return "".join(reversed(list(grapheme.graphemes(s))))
print(reverse_string_unicode("👨👩👧")) # "👨👩👧" — intact ✓
Quick rule: If your text contains emoji that might be ZWJ sequences (family, couple, profession emoji), use the grapheme package. For typical accented Latin text, [::-1] is fine.
Go
Go strings are byte slices, so you must work at the rune (code point) level at minimum. For full grapheme support, use golang.org/x/text/unicode/norm plus golang.org/x/text:
package main
import (
"fmt"
"unicode/utf8"
)
// Reverses by rune (code point) — correct for precomposed NFC text
func reverseRunes(s string) string {
runes := []rune(s)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
return string(runes)
}
func main() {
fmt.Println(reverseRunes("hello")) // olleh
fmt.Println(reverseRunes("café")) // éfac ✓ (if NFC)
fmt.Println(utf8.RuneCountInString("café")) // 4 runes
}
For ZWJ emoji, use the github.com/rivo/uniseg package:
import "github.com/rivo/uniseg"
func reverseGraphemes(s string) string {
var graphemes []string
gr := uniseg.NewGraphemes(s)
for gr.Next() {
graphemes = append(graphemes, gr.Str())
}
// Reverse
for i, j := 0, len(graphemes)-1; i < j; i, j = i+1, j-1 {
graphemes[i], graphemes[j] = graphemes[j], graphemes[i]
}
result := ""
for _, g := range graphemes {
result += g
}
return result
}
PHP
PHP strings are byte arrays. Use mb_str_split (PHP 7.4+) to split by character, not byte:
<?php
// Reverses by UTF-8 character (code point) — works for NFC text
function reverse_string(string $s): string {
$chars = mb_str_split($s, 1, 'UTF-8');
return implode('', array_reverse($chars));
}
echo reverse_string("hello"); // olleh
echo reverse_string("café"); // éfac ✓
For emoji-safe reversal, use grapheme_str_split (requires the intl extension):
<?php
function reverse_string_grapheme(string $s): string {
// grapheme_str_split splits by grapheme cluster
$graphemes = preg_split('/(\X)/u', $s, -1,
PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
return implode('', array_reverse($graphemes));
}
echo reverse_string_grapheme("👨👩👧"); // intact family emoji ✓
Common Variations
Reverse Words (Keep Word Order Reversed, Not Characters)
// JavaScript
"hello world foo".split(" ").reverse().join(" ")
// "foo world hello"
// Python
" ".join("hello world foo".split()[::-1])
// "foo world hello"
Reverse Each Word but Keep Word Order
// JavaScript
"hello world".split(" ").map(w => reverseString(w)).join(" ")
// "olleh dlrow"
// Python
" ".join(w[::-1] for w in "hello world".split())
// "olleh dlrow"
Check if a String is a Palindrome
A palindrome reads the same forwards and backwards (ignoring case and spaces):
// JavaScript
function isPalindrome(s) {
const clean = s.toLowerCase().replace(/[^a-z0-9]/g, "");
const graphemes = [...new Intl.Segmenter().segment(clean)].map(g => g.segment);
return graphemes.join("") === graphemes.reverse().join("");
}
isPalindrome("racecar") // true
isPalindrome("A man a plan a canal Panama") // true
isPalindrome("hello") // false
# Python
def is_palindrome(s: str) -> bool:
clean = "".join(c.lower() for c in s if c.isalnum())
return clean == clean[::-1]
print(is_palindrome("racecar")) # True
print(is_palindrome("hello")) # False
Quick Reference
| Task | JavaScript | Python | Go | PHP |
|---|---|---|---|---|
| Reverse ASCII | s.split("").reverse().join("") |
s[::-1] |
[]rune(s) swap |
strrev($s) |
| Reverse by code point | Intl.Segmenter (works for BMP) |
s[::-1] |
[]rune(s) swap |
mb_str_split + array_reverse |
| Reverse grapheme clusters | Intl.Segmenter |
grapheme package |
uniseg package |
\X regex split |
| Reverse words | .split(" ").reverse().join(" ") |
" ".join(s.split()[::-1]) |
strings.Fields + reverse |
explode + array_reverse |
| Palindrome check | clean + compare | clean + [::-1] |
rune slice compare | strrev compare |
Common Pitfalls
1. Using split("") in JavaScript for non-ASCII"😀".split("").length is 2 in JavaScript — the emoji is split into two surrogate halves. Use [...str] (spread) which iterates by code point, or Intl.Segmenter for grapheme safety.
2. PHP strrev() is byte-aware onlystrrev("café") corrupts multi-byte characters. Always use mb_str_split + array_reverse for UTF-8 strings.
3. Go strings are bytes, not runes
Iterating for i, b := range str (wrong: str[i]) gives bytes. Use for _, r := range str to iterate runes, or convert to []rune first.
4. NFC vs NFD matters for reversal
If é is stored as NFD (two code points), reversing by grapheme cluster gives correct output. Reversing by code point gives wrong output because the combining accent detaches from its base letter. Normalize to NFC first: s.normalize("NFC") in JS, unicodedata.normalize("NFC", s) in Python.
5. Palindrome checks must normalize and strip
"Was it a car or a cat I saw?" is a palindrome, but only if you strip spaces and punctuation and lower-case everything first. Always clean before comparing.
6. Reversing RTL text (Arabic, Hebrew)
Right-to-left scripts should not be reversed character-by-character for display — the browser handles directionality via dir="rtl". Reversing Arabic text programmatically is usually wrong unless you specifically need the raw code-point order reversed for an algorithm.
Frequently Asked Questions
Why does "😀".split("").reverse().join("") work but "👨👩👧".split("").reverse().join("") break?😀 is a single surrogate pair — reversing two items puts them back in the same order by accident. 👨👩👧 is five code points joined by Zero Width Joiner characters; reversing them destroys the joining sequence.
Is there a performance cost to grapheme-aware reversal?
Yes — Intl.Segmenter and similar libraries have higher overhead than a simple split(""). For tight loops, normalize to NFC first and use code-point-level reversal if you are certain your data has no ZWJ sequences.
How do I reverse a string in-place without extra memory?
In languages with mutable strings (Go, C), convert to a slice, then swap from both ends toward the middle in a single pass: O(n/2) swaps, O(1) extra memory. In Python and JavaScript, strings are immutable — you always create a new string.
What is a palindrome number?
Convert the integer to a string, then check if the string equals its reverse. Beware: negative numbers (-121) are never palindromes, and the number 0 is always a palindrome.
Can I reverse a string with a stack?
Yes — push each character onto a stack, then pop them off. The LIFO order gives you the reversed string. This is O(n) time and O(n) space, identical to the array-reverse approach. It is useful as a teaching example, not a practical choice.
How do I reverse only a substring?
Slice the portion you want to reverse, apply reversal, then reconstruct: str.slice(0, start) + reverseString(str.slice(start, end)) + str.slice(end).