Reverse Vowels of a String: Two Pointers with Conditional Swaps
LeetCode 345, Reverse Vowels of a String, gives you a string and asks you to reverse only the vowels while leaving every consonant exactly where it is. Given "hello", the output is "holle". The e at index 1 and the o at index 4 swap places. The h, l, and l never move.
That selective nature is what makes this problem interesting. A plain string reverse is one swap per character pair. Here, you need to decide which pairs are eligible before you swap — and the two pointers handle that decision independently, each advancing past consonants on its own until both land on vowels.
The approach: two pointers with independent skipping
The algorithm is a close cousin of the two-pointer approach you use in Valid Palindrome. Start one pointer at the left end of the string and one at the right. Move them toward each other until they meet.
The key difference from reversing an entire string is that each pointer skips forward (or backward) independently until it lands on a vowel. Only when both pointers are sitting on vowels do you swap and advance both. If either pointer hits a consonant, you move that pointer and check again — the other pointer stays put.
The vowels are a, e, i, o, u in both uppercase and lowercase. Using a set for the lookup keeps each check O(1).
def reverse_vowels(s):
vowels = set("aeiouAEIOU")
s = list(s)
left, right = 0, len(s) - 1
while left < right:
while left < right and s[left] not in vowels:
left += 1
while left < right and s[right] not in vowels:
right -= 1
if left < right:
s[left], s[right] = s[right], s[left]
left += 1
right -= 1
return "".join(s)
The inner while loops each move one pointer independently. The left loop advances until it finds a vowel or crosses right. The right loop retreats until it finds a vowel or crosses left. After both inner loops finish, if left < right guards the swap — if the pointers have already met, there is nothing left to swap.
Converting to a list first is necessary because Python strings are immutable. The list gives you O(1) index assignment.
Step-by-step walkthrough: "hello"
Step 1: left=0 (h). Not a vowel — advance left. left=1.
'h' is not a vowel. The left pointer advances past it to index 1, landing on 'e'.
Step 2: left=1 (e), right=4 (o). Both vowels. Swap. → "holle". left=2, right=3.
Both pointers are on vowels. Swap 'e' and 'o'. The string becomes "holle". Move left to 2, right to 3.
Step 3: left=2 (l). Not a vowel — advance left. left=3.
'l' at index 2 is not a vowel. Advance left to 3.
Step 4: left=3 (l). Not a vowel — advance left. left=4.
'l' at index 3 is not a vowel. Advance left to 4. Now left (4) is greater than right (3).
Step 5: left (4) >= right (3). Stop. Result: "holle".
The pointers have crossed. All vowels have been processed. The final string is "holle".
The consonants h, l, and l are never involved in a swap. The left pointer skips h immediately and the algorithm never revisits it. Once the two l consonants become the next candidates, the pointers have already crossed, so the loop exits without touching them.
Complexity analysis
| Complexity | |
|---|---|
| Time | O(n) |
| Space | O(n) |
Time: O(n). Each pointer travels across the string at most once. The left pointer only moves right; the right pointer only moves left. Even with the nested loops, the total number of steps is bounded by the length of the string.
Space: O(n). The list(s) conversion creates a mutable copy of the input. If the language allowed in-place string mutation (as C or C++ do), this would drop to O(1).
The building blocks
This problem is built on a single pattern: two-pointer opposite-end convergence. You can read a full treatment of it in The Two Pointer Pattern, but the short version is:
- Initialize
left = 0,right = len - 1. - While
left < right, examine both ends. - Move one or both pointers based on a condition.
- The search space shrinks by at least one element per outer iteration.
Reverse String is the base case of this pattern — every pair swaps unconditionally, so there are no inner skip loops. Reverse Vowels of a String adds the conditional: skip past elements that do not meet the criteria before swapping. The same extension appears in Valid Palindrome, where you skip past non-alphanumeric characters before comparing.
If you are comfortable writing the unconditional reverse-string swap from scratch, adding the vowel-skip inner loops is one small step further.
Edge cases
No vowels in the string. If the input contains only consonants, like "bcdfg", the inner skip loops will advance the pointers until they cross without ever making a swap. The output is the unchanged input.
All vowels. If every character is a vowel, like "aeiou", the inner skip loops never run. Every outer iteration immediately swaps and advances both pointers. The result is the fully reversed string.
Single character. "a" has left = right = 0. The outer while left < right is false from the start, so the loop body never executes. The single character is returned as-is.
Uppercase vowels. The problem statement says vowels include uppercase letters. Because the vowel set is defined as set("aeiouAEIOU"), uppercase A, E, I, O, U are treated identically to their lowercase counterparts. A common mistake is only including lowercase vowels in the set and then failing test cases with inputs like "hEllo".
From understanding to recall
Reading through this solution, the logic feels clear. Two pointers, each skipping non-vowels, swap when both are on vowels. But the specific details — the left < right guard inside the inner loops, the final if left < right before the swap, the need to convert the string to a list — are easy to conflate with similar problems.
Spaced repetition turns that "I understand it now" feeling into reliable recall weeks later. You practice the specific building block (conditional two-pointer skip) at the right intervals, and the details stay accessible when you need them in an interview or during a timed contest. The goal is not to memorize this exact problem but to internalize the skip-then-swap extension of the two-pointer template so you can apply it without hesitation.
Related posts
- Reverse String - The unconditional version of this swap pattern, useful as a warm-up
- Valid Palindrome - Uses the same skip-past-invalid-characters technique with converging pointers
- The Two Pointer Pattern - A deep dive into the template underlying both problems