Solving tips
- It's in-place array reversal with a filter: converge two pointers from the ends, but each first skips non-vowels, then swap when both rest on vowels.
- Skipping consonants preserves their positions for free; only vowel-vowel pairs swap, performing exactly the required permutation.
- Test membership against a set of both cases 'aeiouAEIOU'; keep the original character casing in the output.
- Strings are immutable in Python, so work on list(s) and ''.join; O(n) time, and re-check left < right each step so pointers never cross and double-swap.
Problem
Given a string s, reverse the order of only its vowels and return the result. Every consonant (and any other character) stays exactly where it is; the vowels swap positions among themselves so that the first vowel trades places with the last, the second with the second-to-last, and so on.
Vowels are a, e, i, o, u in both lowercase and uppercase. The string may contain letters and printable ASCII characters.
Examples
s = "hello" β "holle"
Vowels are e (index 1) and o (index 4); they swap, consonants h,l,l stay put.
s = "leetcode" β "leotcede"
Vowel sequence e,e,o,e reverses to e,o,e,e in the same four slots.
s = "aA" β "Aa"
Case matters for identity but not for membership: both characters are vowels and they swap.
Constraints
1 <= s.length <= 3 * 10^5
s consists of printable ASCII characters.
- Expected
O(n) time β with 3 * 10^5 characters, anything quadratic is too slow.
Think about it first
Hint 1
Only the vowels move, and they land exactly in the slots vowels already occupy β the set of vowel *positions* is unchanged, only which vowel sits where.
Hint 2
Think of how you'd reverse a whole array with two pointers converging from the ends. What should a pointer do when it's sitting on a consonant?
Hint 3
Walk `left` rightward until it hits a vowel and `right` leftward until it hits a vowel; swap them, step both inward, repeat until the pointers cross. Strings are immutable in Python, so do it on `list(s)` and join at the end.
TL;DR
Converging two pointers that skip non-vowels and swap vowels β O(n) time, O(n) space (Python strings are immutable; O(1) beyond the char buffer).
Collect all vowels in order, then rebuild the string, dealing the collected vowels back out from the end wherever a vowel slot appears.
class Solution:
def reverseVowels(self, s: str) -> str:
vowels = set("aeiouAEIOU")
found = [c for c in s if c in vowels]
out: list[str] = []
for c in s:
if c in vowels:
out.append(found.pop())
else:
out.append(c)
return "".join(out)
- Time:
O(n) β two passes (found.pop() from the end is O(1)).
- Space:
O(n) for the extracted vowels plus the output.
The constraints donβt actually kill this one β itβs linear. What kills it in an interview is the wasted second array of vowels: the two-pointer version does the same job with a single buffer and half the bookkeeping, and it is the transferable pattern. (A variant that pops from the front of a list, found.pop(0), does degrade to O(n^2) β a real trap.)
Approach 2 β Converging two pointers
The insight: this is the classic in-place array reversal (swap ends, move inward β the standard O(n) reverse), except each pointer first skips characters that arenβt allowed to move. Skipping preserves consonant positions for free; swapping only vowel-vowel pairs performs exactly the required permutation.
class Solution:
def reverseVowels(self, s: str) -> str:
vowels = set("aeiouAEIOU")
chars = list(s)
left, right = 0, len(chars) - 1
while left < right:
if chars[left] not in vowels:
left += 1
elif chars[right] not in vowels:
right -= 1
else:
chars[left], chars[right] = chars[right], chars[left]
left += 1
right -= 1
return "".join(chars)
Walkthrough on s = "leetcode" (indices 0-7, vowels at 1, 2, 5, 7):
| step | left | right | chars[left] / chars[right] | action | string state |
|---|
| 1 | 0 | 7 | l / e | l not vowel β left+1 | leetcode |
| 2 | 1 | 7 | e / e | swap, move both | leetcode (equal chars) |
| 3 | 2 | 6 | e / d | d not vowel β right-1 | leetcode |
| 4 | 2 | 5 | e / o | swap, move both | leotcede |
| 5 | 3 | 4 | t / c | t not vowel β left+1 | leotcede |
| 6 | 4 | 4 | pointers meet | loop ends | leotcede |
Return "leotcede".
- Time:
O(n) β each iteration advances at least one pointer, so at most n iterations.
- Space:
O(n) for the char list (unavoidable in Python; in a language with mutable strings this is O(1)).
Note the while left < right condition does double duty: it ends the scan and guards the inner skips β because each iteration moves exactly one pointer one step, the loop re-checks left < right before any swap, so the pointers can never cross and swap a pair twice.
Common pitfalls
- Forgetting uppercase vowels β
"aA" must become "Aa"; membership must test against all ten characters (or lowercase the char only for the test, never for the output).
- Trying to assign
s[i] = c on a Python str β strings are immutable; convert to list and "".join at the end.
- Skipping with nested inner
while loops without re-checking left < right, which can walk the pointers past each other on all-consonant strings and double-swap or index past the region.
- Testing vowels with
in "aeiouAEIOU" on a string is O(10) per check β fine here, but building a set once is the habit that scales.
Pattern takeaway
Converging two pointers handle any βmirror-swap a subset of positionsβ task: move each end inward past the elements that must not move, and swap when both pointers rest on movable ones. The invariant β everything outside [left, right] is already final β is the same as plain reversal; the skip steps just refine which elements participate. The identical skeleton (with a comparison instead of a swap) drives Valid Palindrome.