Vowel Exchange
The Problem
Given a string, reverse only the vowels (a, e, i, o, u — both uppercase and lowercase) while leaving all other characters exactly where they are.
Input: "hello" → Output: "holle"
Input: "leetcode" → Output: "leotcede"
Examples
Example 1
Input: "hello"
Output: "holle"
Explanation: vowels are 'e' (index 1) and 'o' (index 4) — swap them.
Example 2
Input: "leetcode"
Output: "leotcede"
Explanation: vowels at indices 1(e), 2(e), 5(o), 7(e)
Reversed vowel order: e→e→o→e becomes e→o→e→e
Example 3
Input: "bcdfg"
Output: "bcdfg" (no vowels — nothing to swap)
Example 4
Input: "aeiou"
Output: "uoiea" (all vowels, fully reversed)
Intuition
The classic two-pointer reversal swaps every pair. This problem adds a filter: only swap when both pointers are sitting on vowels. When a pointer is sitting on a consonant, just skip it — slide inward until you find the next vowel.
Think of the two pointers as scouts. The left scout hunts for the next vowel from the left; the right scout hunts for the next vowel from the right. When both scouts have found a vowel, they swap and then both advance. If either scout hits the middle first, we’re done.
---
config:
theme: base
themeVariables:
primaryColor: "#dbeafe"
primaryBorderColor: "#3b82f6"
primaryTextColor: "#1e3a5f"
lineColor: "#64748b"
secondaryColor: "#ede9fe"
tertiaryColor: "#fef9c3"
---
flowchart TB
subgraph S0["Start — 'hello', left=0, right=4"]
direction LR
P0L(["left"]) --> H0["h"] --- E0["e"] --- L0["l"] --- L1["l"] --- O0["o"]
O0 --> P0R(["right"])
end
subgraph S1["'h' is not a vowel → left++ to find vowel"]
direction LR
P1L(["left"]) --> H1["h"] --- E1["e"] --- L2["l"] --- L3["l"] --- O1["o"]
O1 --> P1R(["right"])
note1(["left skips 'h'\nstops at 'e'"])
end
subgraph S2["'e' vowel found, 'o' vowel found → swap! left=2, right=3"]
direction LR
P2L(["left"]) --> H2["h"] --- O2["o"] --- L4["l"] --- L5["l"] --- E2["e"]
L4 --> P2R(["right"])
end
subgraph S3["'l' not a vowel, 'l' not a vowel → left ≥ right → done"]
DONE(["✓ 'holle'"])
end
S0 --> S1 --> S2 --> S3
Vowel exchange on "hello" — left skips the consonant 'h', finds vowel 'e'; right is already on vowel 'o'; they swap; pointers cross, done.
Applying the Diagnostic Questions
| Check | Answer for Vowel Exchange |
|---|---|
| ✅ Two positions simultaneously? | Yes — chars[left] and chars[right] are both evaluated and swapped together |
| ✅ One near start, one near end? | Yes — left = 0, right = n-1 |
| ✅ Both move inward? | Yes — both advance inward after each swap, plus inward scans to skip consonants |
| ✅ Simple work at each step? | Yes — scan to the next vowel on each side, then one swap |
This is a direct application with one variation: each pointer doesn’t step by exactly 1 per iteration — it scans past non-qualifying characters before acting. The template is still the same; the advance is just variable-distance.
Why scan independently from both ends? Vowels and consonants are interleaved arbitrarily. The left pointer needs to find the next vowel from the left independently of where the right pointer is, and vice versa. If you advanced both pointers by 1 each step, you’d land on consonants and attempt invalid swaps. The inner while loops decouple scanning from swapping — each side walks at its own pace to its next qualifying position.
What breaks if you use only one pointer? A single forward pointer can collect vowel positions into a list, but then you need a second pass and a stack (or reverse of that list) to know which vowel to pair each one with. Two pointers eliminate that storage — the right pointer always tracks the vowel that the left’s current vowel should swap with. The two-pointer structure implicitly encodes “pair the leftmost unswapped vowel with the rightmost unswapped vowel,” which is exactly what reversal of vowels requires.
Approach
- Convert the string to a list of characters (strings are immutable in Python)
left = 0,right = len - 1, defineVOWELS = set("aeiouAEIOU")- While
left < right:- Advance
leftwhileleft < rightandchars[left]is not a vowel - Retreat
rightwhileleft < rightandchars[right]is not a vowel - If
left < right: swapchars[left]andchars[right], thenleft++,right--
- Advance
- Return
"".join(chars)
Solution
class Solution:
def vowel_exchange(self, s: str) -> str:
VOWELS = set("aeiouAEIOU")
chars = list(s) # strings are immutable — work on a list
left = 0
right = len(chars) - 1
while left < right:
# Advance left until it sits on a vowel (or crosses right)
while left < right and chars[left] not in VOWELS:
left += 1
# Retreat right until it sits on a vowel (or crosses left)
while left < right and chars[right] not in VOWELS:
right -= 1
# Both pointers are on vowels — swap them
if left < right:
chars[left], chars[right] = chars[right], chars[left]
left += 1
right -= 1
return "".join(chars)
# --- Test ---
sol = Solution()
print(sol.vowel_exchange("hello")) # "holle"
print(sol.vowel_exchange("leetcode")) # "leotcede"
print(sol.vowel_exchange("bcdfg")) # "bcdfg"
print(sol.vowel_exchange("aeiou")) # "uoiea"
print(sol.vowel_exchange("a")) # "a"
Dry Run — “leetcode”
s = "leetcode", vowels at indices 1(e), 2(e), 5(o), 7(e)
| Round | left scans to | right scans to | Swap | String |
|---|---|---|---|---|
| 1 | index 1 'e' | index 7 'e' | 'e'↔'e' | "leetcode" (same chars) |
| 2 | index 2 'e' | index 5 'o' | 'e'↔'o' | "leotcede" |
| 3 | left=3, right=4 — no vowels found before crossing | — | — | done |
Return "leotcede" ✓
Complexity Analysis
| Complexity | Reasoning | |
|---|---|---|
| Time | O(n) | Each character is visited at most once by each pointer — total work is O(n) |
| Space | O(n) | The chars list copy of the input string |
If the input were a mutable character array (as in C++/Java), space would drop to O(1). In Python we need the list copy because strings are immutable.
Edge Cases
| Scenario | Input | Output | Note |
|---|---|---|---|
| No vowels | "bcdfg" | "bcdfg" | Pointers never stop to swap |
| All vowels | "aeiou" | "uoiea" | Every step swaps |
| Single character | "a" | "a" | Loop never runs |
| Already reversed vowels | "uoiea" | "aeiou" | Swap brings original back |
| Mixed case | "hEllo" | "hollE" | Uppercase vowels counted too |
Key Takeaway
Vowel Exchange introduces a new wrinkle: not every position deserves a swap. The two inner while loops act as scanners — they fast-forward each pointer past irrelevant characters until a qualifying element is found. This “scan then act” pattern appears in many two-pointer problems where only a subset of elements are candidates for the operation.