Reverse Only Letters
Reverse only the letters in a string while every non-letter character stays exactly where it started - the same two-pointer swap, with a skip condition added on each side.
Problem Statement
Given a string s, reverse it according to two rules:
- Every character that is not an English letter stays in its original position.
- Every English letter (upper or lower case) gets reversed relative to the other letters.
Input: s = "ab-cd"
Output: "dc-ba"
Input: s = "a-bC-dEf-ghIj"
Output: "j-Ih-gfE-dCba"
Approach: Two Pointers with a Skip Condition
This is Reverse String's exact two-pointer skeleton with one addition: before swapping, each pointer needs to check whether it's actually sitting on a letter.
If chars[left] isn't a letter, advance left without touching it - its
final position is already correct.
Same idea from the other end: if chars[right] isn't a letter, pull right
in without swapping.
Only once both pointers are parked on letters do you swap them and move both inward. Non-letters never move; letters get swapped pairwise from the outside in, exactly like the basic reversal.
function reverseOnlyLetters(s) {
const chars = s.split("");
let left = 0;
let right = chars.length - 1;
const isLetter = (c) => /[a-zA-Z]/.test(c);
while (left < right) {
if (!isLetter(chars[left])) {
left++;
} else if (!isLetter(chars[right])) {
right--;
} else {
[chars[left], chars[right]] = [chars[right], chars[left]];
left++;
right--;
}
}
return chars.join("");
}
console.log(reverseOnlyLetters("ab-cd")); // "dc-ba"Complexity Analysis
Both pointers move strictly inward on every iteration - whether it's a skip
or a swap, some progress is made, so the loop runs at most n times total.
Unlike the in-place array version, s.split("") is required because JS
string primitives are immutable - the character array is the one piece of
extra memory this variant needs.
Forgetting to advance the pointer inside the if/else if skip branches is
the classic bug here - it produces an infinite loop, since left or right
never moves past a non-letter. It's also easy to test letters with a
lowercase-only check and silently mismatch on uppercase input; the regex
/[a-zA-Z]/ (or /[a-z]/i) has to cover both cases.
How to Explain it in an Interview
Both branches are independent if/else if checks, so only one pointer
advances per iteration in that case - the loop still terminates correctly,
just one skip at a time rather than two.
Swap the regex for a Unicode property escape like /\p{L}/u, which matches
any Unicode letter category instead of just the ASCII A-Z/a-z range.
Not in place - JS strings can't have individual characters reassigned, so some O(n) structure (array, or building a new string) is unavoidable here.