3823. Reverse Letters Then Special Characters in a String
Problem Description
You are given a string s made up of lowercase English letters and special characters (any character that is not a lowercase letter).
You need to perform the following two operations, in this exact order:
- Reverse the lowercase letters. Take all the lowercase letters from
s, reverse their order, and put them back into the positions that were originally occupied by letters. The positions of the special characters stay untouched during this step. - Reverse the special characters. Take all the special characters from
s, reverse their order, and put them back into the positions that were originally occupied by special characters. The positions of the letters stay untouched during this step.
In other words, letters can only move into spots that held letters, and special characters can only move into spots that held special characters. Each group is independently reversed in place.
Return the final string after both reversals have been applied.
Example:
- If
s = "ab!cd", the letters area, b, c, d. Reversed, they becomed, c, b, a, and they fill the letter positions in order, givingdc!ba. The single special character!stays where it is (reversing one character changes nothing), so the result is"dc!ba".
How We Pick the Algorithm
Why Two pointers?
This problem maps to Two pointers through a short path in the full flowchart.
Reversing letters then special characters in-place uses two pointers to swap characters only within each group's positions.
Open in FlowchartIntuition
The key observation is that the two reversal operations don't actually interfere with each other. Letters only move into letter positions, and special characters only move into special-character positions. So we can think about each group completely separately.
When we reverse a group and then place it back into the same set of positions in order, what really happens? The first position that belongs to that group ends up holding the last element of the group, the second position holds the second-to-last element, and so on. This is exactly the behavior of a stack: if we collect all the elements of a group into a list and then pop from the end, we hand out the elements in reverse order.
This gives us a clean idea. We make two collections:
- A list
aholding all the letters in their original left-to-right order. - A list
bholding all the special characters in their original left-to-right order.
Then we walk through s one character at a time. Whenever we hit a letter position, we pop() the last letter from a; whenever we hit a special-character position, we pop() the last special character from b. Because pop() always removes from the back, the letters come out reversed, and the special characters come out reversed, each filling only their own positions.
This naturally produces both reversals at once in a single pass, without needing two separate reverse-and-reinsert steps, since reversing letters and reversing special characters are independent and can be handled together.
Pattern Learn more about Two Pointers patterns.
Solution Approach
Solution 1: Simulation
We first store the letters and special characters from string s into two separate lists a and b respectively. Then we traverse the string s. If the current position is a letter, we pop the last letter from list a and place it back at that position; otherwise, we pop the last special character from list b and place it back at that position.
After the traversal is complete, we obtain the result string.
Step-by-step breakdown:
-
Build the two lists. We loop over every character
cins. We usec.isalpha()to decide which group it belongs to. Letters are appended toa, and special characters are appended tob. At this point, both lists hold their elements in original left-to-right order.a = [] b = [] for c in s: if c.isalpha(): a.append(c) else: b.append(c) -
Rebuild the string with reversal. We traverse
sagain. For each position, we checkc.isalpha():- If it is a letter position, we call
a.pop(), which removes and returns the last letter ina. - Otherwise, it is a special-character position, so we call
b.pop(), returning the last special character inb.
Because
pop()always takes from the end of the list, the letters are handed out in reverse order, and the special characters are handed out in reverse order. Each element lands only in a position of its own type, which is exactly what the two reversal operations require.return ''.join(a.pop() if c.isalpha() else b.pop() for c in s) - If it is a letter position, we call
Data structures used: two lists a and b, each used as a stack (we only push with append and remove with pop).
Complexity analysis:
- Time complexity:
O(n), wherenis the length ofs. We make two passes over the string, and eachappend/popoperation isO(1). - Space complexity:
O(n), since the two lists together store all characters ofs, plus the space for the output string.
Example Walkthrough
Let's trace through the solution using s = "le!et*c".
Identify the groups
Scanning left to right, we classify each character:
| Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| Char | l | e | ! | e | t | * | c |
| Type | letter | letter | special | letter | letter | special | letter |
Step 1: Build the two lists
We append each character to its group, preserving original left-to-right order:
- Letters →
a = ['l', 'e', 'e', 't', 'c'] - Special characters →
b = ['!', '*']
Step 2: Rebuild the string with reversal
Now we walk through s again. At every letter position we a.pop() (takes from the end of a), and at every special position we b.pop() (takes from the end of b).
| Index | Char | Type | Action | Popped | a after | b after | Result so far |
|---|---|---|---|---|---|---|---|
| 0 | l | letter | a.pop() | c | ['l','e','e','t'] | ['!','*'] | c |
| 1 | e | letter | a.pop() | t | ['l','e','e'] | ['!','*'] | ct |
| 2 | ! | special | b.pop() | * | ['l','e','e'] | ['!'] | ct* |
| 3 | e | letter | a.pop() | e | ['l','e'] | ['!'] | ct*e |
| 4 | t | letter | a.pop() | e | ['l'] | ['!'] | ct*ee |
| 5 | * | special | b.pop() | ! | ['l'] | [] | ct*ee! |
| 6 | c | letter | a.pop() | l | [] | [] | ct*ee!l |
Result: "ct*ee!l"
Verifying the two independent reversals
- Letters were
l, e, e, t, c. Reversed they becomec, t, e, e, l, which fill the letter positions (0,1,3,4,6) in order → matches our output. - Special characters were
!, *. Reversed they become*, !, which fill the special positions (2,5) in order → matches our output.
Notice how pop() from the end naturally handed out each group in reverse, and each element landed only in a slot of its own type — accomplishing both reversals in a single pass.
Solution Implementation
1class Solution:
2 def reverseByType(self, s: str) -> str:
3 # Collect alphabetic characters into one list
4 # and non-alphabetic characters into another list,
5 # preserving their original relative order.
6 alpha_chars: list[str] = []
7 non_alpha_chars: list[str] = []
8 for char in s:
9 if char.isalpha():
10 alpha_chars.append(char)
11 else:
12 non_alpha_chars.append(char)
13
14 # Rebuild the string by iterating over the original string:
15 # - For each alphabetic position, take the last remaining alpha char (reversing that group).
16 # - For each non-alphabetic position, take the last remaining non-alpha char (reversing that group).
17 # Using pop() from the end achieves an in-place reversal per type.
18 return ''.join(
19 alpha_chars.pop() if char.isalpha() else non_alpha_chars.pop()
20 for char in s
21 )
22```
23
24**Notes on the changes:**
251. **Naming:** Renamed `a` → `alpha_chars`, `b` → `non_alpha_chars`, and the loop variable `c` → `char` for clarity. The method name `reverseByType` is kept unchanged as requested.
262. **Type hints:** Added `list[str]` annotations (valid in Python 3.9+; for earlier Python 3 versions you'd use `List[str]` with `from typing import List`).
273. **Comments:** Added explanations describing the two-phase logic.
28
29**Alternative perspective:** If you want to avoid mutating lists with `pop()`, you could use index pointers or pre-reverse the lists and consume them with iterators:
30
31```python3
32class Solution:
33 def reverseByType(self, s: str) -> str:
34 # Pre-reverse each group, then consume them in order via iterators.
35 alpha_iter = iter([c for c in s if c.isalpha()][::-1])
36 non_alpha_iter = iter([c for c in s if not c.isalpha()][::-1])
37 return ''.join(
38 next(alpha_iter) if c.isalpha() else next(non_alpha_iter)
39 for c in s
40 )
411class Solution {
2 public String reverseByType(String s) {
3 // Collect letters and non-letters separately, preserving their order
4 StringBuilder letters = new StringBuilder();
5 StringBuilder others = new StringBuilder();
6 char[] chars = s.toCharArray();
7 for (char c : chars) {
8 if (Character.isLetter(c)) {
9 letters.append(c);
10 } else {
11 others.append(c);
12 }
13 }
14
15 // Pointers starting from the end of each collected group
16 int letterIndex = letters.length();
17 int otherIndex = others.length();
18
19 // Rebuild the string: for each original position, take the next
20 // character of the same type from the back, effectively reversing
21 // letters among letter positions and non-letters among their positions
22 for (int i = 0; i < chars.length; ++i) {
23 if (Character.isLetter(chars[i])) {
24 chars[i] = letters.charAt(--letterIndex);
25 } else {
26 chars[i] = others.charAt(--otherIndex);
27 }
28 }
29
30 return new String(chars);
31 }
32}
331class Solution {
2public:
3 string reverseByType(string s) {
4 string letters; // Stores all alphabetic characters in original order
5 string others; // Stores all non-alphabetic characters in original order
6
7 // First pass: separate characters into two buckets by type
8 for (char c : s) {
9 if (isalpha(static_cast<unsigned char>(c))) {
10 letters.push_back(c);
11 } else {
12 others.push_back(c);
13 }
14 }
15
16 // Pointers to the end of each bucket, used to read characters in reverse
17 int letterIndex = static_cast<int>(letters.size());
18 int otherIndex = static_cast<int>(others.size());
19
20 // Second pass: fill each position with the corresponding type's
21 // character taken from the back, achieving an in-type reversal
22 for (size_t i = 0; i < s.size(); ++i) {
23 if (isalpha(static_cast<unsigned char>(s[i]))) {
24 s[i] = letters[--letterIndex];
25 } else {
26 s[i] = others[--otherIndex];
27 }
28 }
29
30 return s;
31 }
32};
331/**
2 * Reverses letters and non-letters independently within the string.
3 * Letters keep their positions among other letters but in reversed order;
4 * non-letters do the same among non-letters.
5 *
6 * @param s - The input string to process.
7 * @returns The string with letters and non-letters reversed by type.
8 */
9function reverseByType(s: string): string {
10 // Holds all alphabetic characters in their original order.
11 const letters: string[] = [];
12 // Holds all non-alphabetic characters in their original order.
13 const nonLetters: string[] = [];
14
15 // Split the input string into an array of individual characters.
16 const chars: string[] = s.split('');
17
18 // Classify each character into the letters or nonLetters bucket.
19 for (const char of chars) {
20 if (/[a-zA-Z]/.test(char)) {
21 letters.push(char);
22 } else {
23 nonLetters.push(char);
24 }
25 }
26
27 // Pointers starting at the end of each bucket to consume them in reverse.
28 let letterIndex = letters.length;
29 let nonLetterIndex = nonLetters.length;
30
31 // Walk through the original positions, refilling each slot with the
32 // corresponding reversed character from its matching bucket.
33 for (let i = 0; i < chars.length; i++) {
34 if (/[a-zA-Z]/.test(chars[i])) {
35 chars[i] = letters[--letterIndex];
36 } else {
37 chars[i] = nonLetters[--nonLetterIndex];
38 }
39 }
40
41 // Reassemble the characters back into a single string.
42 return chars.join('');
43}
44Time and Space Complexity
-
Time Complexity:
O(n), wherenis the length of strings. The first loop iterates over each character insto populate listsaandb, takingO(n)time. The second pass (inside''.join(...)) also iterates over each character ofsonce, and eachpop()operation on a list from the end runs inO(1). The final''.join(...)concatenatesncharacters inO(n)time. Therefore, the overall time complexity isO(n). -
Space Complexity:
O(n), wherenis the length of strings. Listsaandbtogether store allncharacters ofs, requiringO(n)space. The generator passed to''.join(...)produces the result string of lengthn, also takingO(n)space. Hence, the total space complexity isO(n).
Pattern Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Forgetting that pop() must come from the end of the list
The most frequent mistake is reaching for pop(0) instead of pop(), thinking "I stored the characters in order, so I should take them from the front." This silently breaks the reversal.
a.pop()removes the last element → hands out characters in reverse order → ✅ correct reversal.a.pop(0)removes the first element → hands out characters in original order → ❌ no reversal at all.
Worse, pop(0) is also O(n) because every remaining element must shift left, turning the whole solution into O(n²).
# WRONG: produces the original string, and is O(n^2) return ''.join(a.pop(0) if c.isalpha() else b.pop(0) for c in s) # CORRECT: O(1) pop from the end gives the reversal for free return ''.join(a.pop() if c.isalpha() else b.pop() for c in s)
Pitfall 2: Misclassifying characters with the wrong predicate
The problem defines a special character as "any character that is not a lowercase letter." Using a predicate that is too broad or too narrow misroutes characters into the wrong group, and the two pop() streams desynchronize from the positions they should fill.
char.isalpha()treats uppercase letters ('A'–'Z') and many Unicode letters as "letters" too. If the input is strictly lowercase as stated, this is fine — but if uppercase ever appears, an uppercase letter would be reversed within the letter group rather than the special-character group.char.isalnum()would wrongly bundle digits with letters.
To match the specification exactly, guard against only lowercase letters:
def is_letter(char: str) -> bool:
return 'a' <= char <= 'z' # strictly lowercase, matches the problem definition
Then use is_letter(char) consistently in both the partitioning loop and the rebuild loop. The cardinal rule: the predicate used to build the lists must be the exact same predicate used to consume them. If they differ, the counts won't line up and you'll hit an IndexError: pop from empty list.
Pitfall 3: Mutating a list while assuming the original order is preserved
If you need the lists for anything else after the rebuild, remember that pop() empties them. By the end of the generator both alpha_chars and non_alpha_chars are empty. Should you require the original characters again, either work on a copy or use the iterator-based alternative (which pre-reverses into separate iterators and leaves the source list intact):
# Safe if you still need the data afterward
alpha_iter = iter([c for c in s if c.isalpha()][::-1])
non_alpha_iter = iter([c for c in s if not c.isalpha()][::-1])
result = ''.join(
next(alpha_iter) if c.isalpha() else next(non_alpha_iter)
for c in s
)
Pitfall 4: Assuming the empty-string / single-character edge cases need special handling
They don't — and adding extra branching for them is an unnecessary source of bugs. An empty string yields two empty lists and an empty join. A string with a single character (of either type) produces one list with one element, and reversing one element leaves it unchanged. The general algorithm already covers these cases correctly, so resist the urge to special-case them.
Ready to land your dream job?
Unlock your dream job with a 5-minute quiz for a personalized study roadmap!
Get My RoadmapConsider the classic dynamic programming of fibonacci numbers, what is the recurrence relation?
Recommended Readings
Two Pointers Technique Explained If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe width 560 height 315 src https www youtube nocookie com embed rQhNcycbf8w si lE7qtd1h_JSQwGpW title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion If you prefer videos here's a video that explains recursion in a fun and easy way Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask him
Want a Structured Path to Master System Design Too? Don’t Miss This!