Skip to content
← All posts

Reverse Only Letters: Two Pointer Approach for Selective Reversal

4 min read
leetcodeproblemeasystringstwo-pointers

LeetCode 917, Reverse Only Letters, gives you a string and asks you to reverse only the English letters. Every non-letter character stays exactly where it is. Given "ab-cd", the output is "dc-ba". The letters a, b, c, d reverse to d, c, b, a, but the - at index 2 never moves.

This is a clean application of the two-pointer pattern with conditional skipping. If you have solved Reverse Vowels of a String, you already know the shape of the solution. The only difference is the skip condition: instead of skipping non-vowels, you skip non-letters.

0a1b2-3c4dleftright
The string "ab-cd" with letters (blue) and non-letters (yellow) highlighted. The left pointer starts at index 0 and the right pointer starts at index 4. Only letters participate in the reversal.

The approach: two pointers skipping non-letters

Start with a pointer at each end of the string. Move them toward each other. If the left pointer is not on a letter, advance it. If the right pointer is not on a letter, retreat it. When both pointers are on letters, swap them and move both inward.

The key insight is that non-letter characters act as fixed obstacles. The two pointers independently navigate around them, and the letters flow past into their reversed positions.

Think of it this way: extract all the letters, reverse them, then pour them back into the letter positions. The two-pointer approach does exactly that, but in-place without extra storage for the extracted letters.

def reverseOnlyLetters(self, s: str) -> str:
    chars = list(s)
    left, right = 0, len(chars) - 1
    while left < right:
        if not chars[left].isalpha():
            left += 1
        elif not chars[right].isalpha():
            right -= 1
        else:
            chars[left], chars[right] = chars[right], chars[left]
            left += 1
            right -= 1
    return ''.join(chars)

The if/elif/else chain handles three cases per iteration:

  1. Left is on a non-letter. Skip it by incrementing left.
  2. Right is on a non-letter. Skip it by decrementing right.
  3. Both are on letters. Swap and advance both.

Each iteration moves at least one pointer inward, so the loop terminates in at most n steps.

Step-by-step walkthrough: "ab-cd"

Step 1: left=0 (a), right=4 (d). Both are letters. Swap.

ab-cdleftright

Both pointers point to letters. Swap 'a' and 'd'. Move left to 1, right to 3.

Step 2: After swap. chars = [d, b, -, c, a]. left=1, right=3.

db-caleftright

Both pointers land on letters again. Swap 'b' and 'c'. Move left to 2, right to 2.

Step 3: After swap. chars = [d, c, -, b, a]. left=2, right=2.

dc-baleftright

left (2) is no longer less than right (2). The loop ends. The '-' stayed in place. Result: "dc-ba".

Notice how the - at index 2 is never touched by either pointer in a swap. The left pointer would skip past it (if it reached that position before the loop ended), and the right pointer never gets that far. Non-letters are invisible to the swap logic.

Complexity analysis

Complexity
TimeO(n)
SpaceO(n)

Time: O(n). Each pointer moves at most n positions total. The left pointer only moves right, the right pointer only moves left. Combined, they cover at most n steps regardless of how many non-letters exist in the string.

Space: O(n). The list(s) conversion creates a mutable copy of the input string. In a language with mutable strings (like C++), you could do this in O(1) extra space by modifying the string in place.

The building blocks

This problem is built on the same foundation as Reverse Vowels of a String and Valid Palindrome: opposite-end convergence with conditional skipping.

The template looks like this:

  • Initialize left = 0, right = len - 1.
  • While left < right, check a condition on each pointer.
  • If a pointer is on an "invalid" element, skip past it.
  • When both are on "valid" elements, perform the operation (swap, compare, etc.) and advance both.

The only thing that changes between problems is the definition of "valid":

  • Reverse String: every character is valid (no skipping at all)
  • Reverse Vowels: vowels are valid, consonants get skipped
  • Reverse Only Letters: letters are valid, non-letters get skipped
  • Valid Palindrome: alphanumeric characters are valid, others get skipped

Once you internalize this template, all four problems become mechanical variations of the same idea.

The isalpha() method in Python returns True for both uppercase and lowercase English letters. This matches the problem requirements exactly. In other languages, you might use Character.isLetter() (Java) or a regex test like /[a-zA-Z]/ (JavaScript).

Edge cases

No letters in the string. If the input is all special characters, like "---", both pointers skip inward without ever finding a letter to swap. The output is the unchanged input.

All letters. If every character is a letter, like "abcd", the pointers never skip. The behavior is identical to a full string reverse: "dcba".

Single character. "a" or "-" has left = right = 0. The while left < right condition is false immediately, so the string is returned as-is.

Mixed case. Letters swap positions but keep their original casing. If the input is "a-bC", the letters are a, b, C. Reversed, they become C, b, a. The output is "C-ba". The uppercase C moves to index 0 but stays uppercase.

From understanding to recall

The code for this problem is short. The logic is clear. But three weeks from now, will you remember the exact structure of the if/elif/else chain? Will you remember that each branch moves exactly one pointer (or both on swap)? These are the details that slip away without reinforcement.

Spaced repetition helps you drill the conditional-skip two-pointer template at increasing intervals. You are not memorizing this specific problem. You are building permanent recall of the pattern so that when you see any variant (skip non-vowels, skip non-alphanumeric, skip spaces), you can write the solution without hesitation.

Related posts