Facebook Pixel

3823. Reverse Letters Then Special Characters in a String

EasyTwo PointersStringSimulation
LeetCode ↗

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:

  1. 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.
  2. 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 are a, b, c, d. Reversed, they become d, c, b, a, and they fill the letter positions in order, giving dc!ba. The single special character ! stays where it is (reversing one character changes nothing), so the result is "dc!ba".
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

How We Pick the Algorithm

Why Two pointers?

This problem maps to Two pointers through a short path in the full flowchart.

Two-pointerscan fromends?yesO(1)memoryconstraint?yesTwo pointers

Reversing letters then special characters in-place uses two pointers to swap characters only within each group's positions.

Open in Flowchart

Intuition

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 a holding all the letters in their original left-to-right order.
  • A list b holding 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:

  1. Build the two lists. We loop over every character c in s. We use c.isalpha() to decide which group it belongs to. Letters are appended to a, and special characters are appended to b. 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)
  2. Rebuild the string with reversal. We traverse s again. For each position, we check c.isalpha():

    • If it is a letter position, we call a.pop(), which removes and returns the last letter in a.
    • Otherwise, it is a special-character position, so we call b.pop(), returning the last special character in b.

    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)

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), where n is the length of s. We make two passes over the string, and each append/pop operation is O(1).
  • Space complexity: O(n), since the two lists together store all characters of s, 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:

Index0123456
Charle!et*c
Typeletterletterspecialletterletterspecialletter

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).

IndexCharTypeActionPoppeda afterb afterResult so far
0llettera.pop()c['l','e','e','t']['!','*']c
1elettera.pop()t['l','e','e']['!','*']ct
2!specialb.pop()*['l','e','e']['!']ct*
3elettera.pop()e['l','e']['!']ct*e
4tlettera.pop()e['l']['!']ct*ee
5*specialb.pop()!['l'][]ct*ee!
6clettera.pop()l[][]ct*ee!l

Result: "ct*ee!l"

Verifying the two independent reversals

  • Letters were l, e, e, t, c. Reversed they become c, 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        )
41
1class 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}
33
1class 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};
33
1/**
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}
44

Time and Space Complexity

  • Time Complexity: O(n), where n is the length of string s. The first loop iterates over each character in s to populate lists a and b, taking O(n) time. The second pass (inside ''.join(...)) also iterates over each character of s once, and each pop() operation on a list from the end runs in O(1). The final ''.join(...) concatenates n characters in O(n) time. Therefore, the overall time complexity is O(n).

  • Space Complexity: O(n), where n is the length of string s. Lists a and b together store all n characters of s, requiring O(n) space. The generator passed to ''.join(...) produces the result string of length n, also taking O(n) space. Hence, the total space complexity is O(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 Roadmap
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Get a Personalized Study Roadmap:

Consider the classic dynamic programming of fibonacci numbers, what is the recurrence relation?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More