A clear explanation of reversing only English letters while keeping all non-letter characters fixed.
Problem Restatement
We are given a string s.
Reverse only the English letters in the string.
All characters that are not English letters must stay at the same index. Both lowercase and uppercase English letters are reversed together.
For example:
s = "ab-cd"The letters are:
a, b, c, dAfter reversing the letters:
d, c, b, aThe hyphen stays in the same position.
Output:
"dc-ba"Input and Output
| Item | Meaning |
|---|---|
| Input | A string s |
| Output | String after reversing only English letters |
| Fixed characters | Digits, punctuation, and symbols stay in place |
| Reversed characters | Lowercase and uppercase English letters |
Function shape:
def reverseOnlyLetters(s: str) -> str:
...Examples
Example 1:
s = "ab-cd"Output:
"dc-ba"Example 2:
s = "a-bC-dEf-ghIj"Output:
"j-Ih-gfE-dCba"Example 3:
s = "Test1ng-Leet=code-Q!"Output:
"Qedo1ct-eeLg=ntse-T!"First Thought: Extract Letters, Then Put Them Back
One simple approach is:
- Collect all letters from the string.
- Reverse that letter list.
- Build a new string:
- If the original character is a letter, take the next reversed letter.
- Otherwise, keep the original character.
class Solution:
def reverseOnlyLetters(self, s: str) -> str:
letters = [ch for ch in s if ch.isalpha()]
letters.reverse()
result = []
index = 0
for ch in s:
if ch.isalpha():
result.append(letters[index])
index += 1
else:
result.append(ch)
return "".join(result)This works and is easy to reason about.
But we can solve it with two pointers and direct swaps.
Key Insight
This is a normal reverse operation with a filter.
In a normal string reversal, we swap the first character with the last, the second with the second-last, and so on.
Here, only letters participate in the reversal.
So we use two pointers:
| Pointer | Role |
|---|---|
left | Finds the next letter from the front |
right | Finds the next letter from the back |
When both pointers are at letters, swap them.
When a pointer is at a non-letter, skip it.
Because we never swap non-letter characters, they remain fixed.
Algorithm
- Convert the string into a list of characters.
- Set
left = 0. - Set
right = len(s) - 1. - While
left < right:- Move
leftrightward until it points to a letter. - Move
rightleftward until it points to a letter. - Swap the two letters.
- Move both pointers inward.
- Move
- Join the list and return the result.
Correctness
The algorithm swaps letters from the outside inward.
The left pointer always finds the next unreversed letter from the front.
The right pointer always finds the next unreversed letter from the back.
When these two letters are swapped, they move to their correct reversed positions among the letters.
Non-letter characters are only skipped. They are never moved or overwritten, so they stay at their original indices.
The process continues until the pointers meet or cross. At that point, every letter has been paired with its corresponding letter from the opposite side, and every non-letter remains fixed.
Therefore, the returned string has all English letters reversed and all non-letter characters unchanged.
Complexity
| Metric | Value | Why |
|---|---|---|
| Time | O(n) | Each pointer scans across the string once |
| Space | O(n) | Python strings are immutable, so we use a character list |
Implementation
class Solution:
def reverseOnlyLetters(self, s: str) -> str:
chars = list(s)
left = 0
right = len(chars) - 1
while left < right:
while left < right and not chars[left].isalpha():
left += 1
while left < right and not chars[right].isalpha():
right -= 1
chars[left], chars[right] = chars[right], chars[left]
left += 1
right -= 1
return "".join(chars)Code Explanation
Convert the string into a mutable list:
chars = list(s)Start two pointers at both ends:
left = 0
right = len(chars) - 1Skip non-letters on the left:
while left < right and not chars[left].isalpha():
left += 1Skip non-letters on the right:
while left < right and not chars[right].isalpha():
right -= 1Now both sides point to letters, so we swap them:
chars[left], chars[right] = chars[right], chars[left]Then move inward:
left += 1
right -= 1Finally, rebuild the string:
return "".join(chars)Testing
def run_tests():
s = Solution()
assert s.reverseOnlyLetters("ab-cd") == "dc-ba"
assert s.reverseOnlyLetters("a-bC-dEf-ghIj") == "j-Ih-gfE-dCba"
assert s.reverseOnlyLetters("Test1ng-Leet=code-Q!") == "Qedo1ct-eeLg=ntse-T!"
assert s.reverseOnlyLetters("7_28]") == "7_28]"
assert s.reverseOnlyLetters("a") == "a"
assert s.reverseOnlyLetters("A-b") == "b-A"
assert s.reverseOnlyLetters("z<*zj") == "j<*zz"
print("all tests passed")
run_tests()Test meaning:
| Test | Why |
|---|---|
"ab-cd" | Basic fixed hyphen case |
"a-bC-dEf-ghIj" | Mixed uppercase, lowercase, and symbols |
"Test1ng-Leet=code-Q!" | Main complex sample |
"7_28]" | No letters |
"a" | Single letter |
"A-b" | Case is preserved as characters move |
"z<*zj" | Multiple fixed symbols between letters |