Reverse String — The Two-Pointer Swap Pattern Every Interview Expects
Advertisement
Problem Statement
Write a function that reverses a string. The input string is given as an array of characters
s. You must do this by modifying the input array in-place with O(1) extra memory.
Example 1:
Input: s = ['h','e','l','l','o']
Output: ['o','l','l','e','h']Example 2:
Input: s = ['H','a','n','n','a','h']
Output: ['h','a','n','n','a','H']Constraints:
1 <= s.length <= 10^5s[i]is a printable ASCII character
Why This Problem Matters
Reverse String is listed as Easy on LeetCode, and that rating is accurate for the solution itself. But the reason interviewers ask it — especially at Meta, Microsoft, and Amazon — is not to test whether you can flip an array. It is to test whether you reach instinctively for the right tool: the two-pointer pattern.
The two-pointer technique is one of the highest-leverage tools in array and string interviews. It lets you process a structure from both ends simultaneously without allocating additional memory, reducing a class of problems that might naively require O(n) space down to O(1). Once you internalize it here, you will see it everywhere: palindrome validation, container with most water, trapping rain water, three sum, and more.
Beyond that, interviewers use Reverse String as a diagnostic for in-place thinking. There is a lazy solution — convert to a list, reverse, assign back — that technically works but misses the entire point of the constraint. When an interviewer says "O(1) extra memory," they are asking whether you understand that you cannot allocate a second array. Candidates who immediately reach for .reverse() or slicing often get asked: "What if you weren't allowed to use that built-in?" and then freeze. Candidates who internalize the two-pointer pattern answer that question before it is asked.
This problem is also the direct prerequisite for:
- Valid Palindrome (LC 125) — same left/right pointer logic, with a character filter
- Reverse Words in a String (LC 151) — reverse the whole string, then reverse each word
- Rotate Array (LC 189) — the three-reversal trick depends entirely on this primitive
- Reverse Linked List (LC 206) — same conceptual pattern, applied to a different structure
If you can explain the two-pointer swap clearly and confidently here, every one of those problems becomes an extension of something you already know.
The Core Insight / Key Technique
The naive mental model for reversing a string is: create a new empty array, iterate the original from right to left, and push each character into the new array. That gives you the right answer, but uses O(n) extra space — one slot in the new array for every character in the original. The problem explicitly forbids this.
The key insight is that you do not need a second array at all. You just need to move characters to where they belong. And you already know exactly where each character belongs: the character at position left should end up at position n - 1 - left, and vice versa. That is a symmetric relationship — and symmetry is the two-pointer pattern's natural habitat.
Here is the core idea stated plainly: plant one pointer at the very start of the array (left = 0) and one at the very end (right = n - 1). Swap the characters at these two positions. Then move both pointers one step inward — left goes right, right goes left. Repeat until the pointers meet or cross. That is the entire algorithm.
Why does this work? Because every swap you make is a permanent fix. Once you swap s[0] and s[n-1], both characters are in their final correct positions. You never need to revisit them. The same is true for every subsequent swap. You process n / 2 pairs of characters (for an odd-length array, the middle element never needs to move at all), performing exactly one swap per pair. No wasted work, no extra memory.
The stopping condition is left < right. When left === right (odd-length array, both pointers at the middle character), there is nothing to swap — a single character is already in its correct position. When left > right, the pointers have crossed and all swaps are done. Either way, you stop.
Visual Dry Run
Let's trace through s = ['h', 'e', 'l', 'l', 'o'] (length 5) step by step.
Initial state: left = 0, right = 4
Index: 0 1 2 3 4
Array: [ 'h', 'e', 'l', 'l', 'o' ]
^ ^
left rightStep 1: left=0, right=4 — left < right, so swap s[0] and s[4]
Swap 'h' ↔ 'o'
Index: 0 1 2 3 4
Array: [ 'o', 'e', 'l', 'l', 'h' ]Advance: left = 1, right = 3
Step 2: left=1, right=3 — left < right, so swap s[1] and s[3]
Swap 'e' ↔ 'l'
Index: 0 1 2 3 4
Array: [ 'o', 'l', 'l', 'e', 'h' ]Advance: left = 2, right = 2
Step 3: left=2, right=2 — left < right is FALSE (they are equal). Stop.
Index: 0 1 2 3 4
Array: [ 'o', 'l', 'l', 'e', 'h' ]
✓ ✓ ✓ ✓ ✓The middle character 'l' at index 2 is already in its correct position and was never touched. Done in 2 swaps for a 5-character array — exactly floor(n/2) swaps, as expected.
Step-by-step summary table:
| Step | left | right | s[left] | s[right] | Action | Array after |
|---|---|---|---|---|---|---|
| 1 | 0 | 4 | 'h' | 'o' | Swap | ['o','e','l','l','h'] |
| 2 | 1 | 3 | 'e' | 'l' | Swap | ['o','l','l','e','h'] |
| 3 | 2 | 2 | 'l' | 'l' | Stop | ['o','l','l','e','h'] |
Now let's also trace s = ['H', 'a', 'n', 'n', 'a', 'h'] (length 6, even) to see how the even-length case terminates:
| Step | left | right | Action | Array after |
|---|---|---|---|---|
| 1 | 0 | 5 | Swap 'H' and 'h' | ['h','a','n','n','a','H'] |
| 2 | 1 | 4 | Swap 'a' and 'a' | ['h','a','n','n','a','H'] (no visible change, same char) |
| 3 | 2 | 3 | Swap 'n' and 'n' | ['h','a','n','n','a','H'] |
| 4 | 3 | 2 | Stop (left > right) | Done |
For an even-length array of size 6, we perform exactly 3 swaps (floor(6/2) = 3) before the pointers cross.
Common Mistakes
These are the actual mistakes candidates make during live interviews — not obscure edge cases, but thinking errors that surface under pressure.
1. Destroying the swap with a wrong destructuring order in JavaScript.
This is the most common silent bug in JavaScript solutions. Consider this buggy code:
[s[left], s[right]] = [s[right], s[left]];
left++;
right--;This looks correct, but candidates sometimes write it as:
[s[left++], s[right--]] = [s[right], s[left]];The problem here is subtle: JavaScript evaluates the right-hand side before the left-hand side, but the left-hand side's index expressions (left++, right--) are evaluated during destructuring assignment. The order in which the indices are resolved versus incremented can produce wrong results. The safe pattern is always to do the swap first on one line, then increment the pointers on separate lines. Never mix the increment into the swap expression.
2. Using an extra array (failing the O(1) space constraint).
Many candidates instinctively write a solution like this in Python:
s[:] = s[::-1] # or: new_s = s[::-1]; return new_sThe slice s[::-1] creates a brand-new list of length n — that is O(n) extra space. The problem explicitly requires O(1) extra memory. Using s[:] = s[::-1] is a clever trick that writes back in-place, but it still allocates the temporary reversed copy. In an interview, if you use this, you need to acknowledge that it allocates O(n) intermediate memory. The interviewer will almost certainly follow up by asking for the O(1) version.
3. Getting the stopping condition wrong — using left <= right instead of left < right.
If you write while left <= right, you will perform one extra unnecessary swap when left === right (odd-length arrays). You will swap the middle element with itself — harmless, but wasteful and a signal that you have not thought through the termination condition. More importantly, if an interviewer asks "why do you stop when left < right and not left <= right?" and you cannot answer clearly, it reveals a gap. The correct reason: when left === right, both pointers point to the same element, which is already in its final position. No swap is needed. The loop can stop.
4. Forgetting that this is a void function — returning the array is wrong.
LeetCode 344 modifies the array in-place and returns None / void. A very common mistake is to write return s at the end of the function. In a language like Python where lists are mutable and passed by reference, this does not cause a wrong answer on LeetCode — but it signals a misunderstanding of how in-place modification works. The modification happens directly in the caller's array. There is nothing to return. In interviews, returning the array when the signature is void is a red flag.
5. Not handling the single-character edge case consciously.
When s = ['a'], left = 0 and right = 0. The condition left < right is immediately false, so the loop body never executes. The array is returned unchanged — which is correct, since a single character reversed is itself. Many candidates worry about this case and add special handling, which is unnecessary. Knowing that the two-pointer loop handles it automatically, and being able to explain why, demonstrates a complete understanding of the algorithm.
Solutions
Approach 1 — Two Pointers (Optimal)
Walk inward from both ends, swapping characters at each step. Stop when the pointers meet or cross. This is the canonical solution: O(n) time, O(1) space.
Python
from typing import List
class Solution:
def reverseString(self, s: List[str]) -> None:
"""
Do not return anything, modify s in-place instead.
"""
# Initialize left pointer at the start of the array
left = 0
# Initialize right pointer at the end of the array
right = len(s) - 1
# Continue swapping until the pointers meet or cross
while left < right:
# Swap the characters at the two pointer positions
# Python tuple unpacking handles this atomically — no temp variable needed
s[left], s[right] = s[right], s[left]
# Move left pointer one step toward the center
left += 1
# Move right pointer one step toward the center
right -= 1
# No return — the function modifies s in-place
# The caller's array is updated because lists are passed by referenceJavaScript
/**
* @param {character[]} s
* @return {void} Do not return anything, modify s in-place instead.
*/
function reverseString(s) {
// Left pointer starts at the beginning of the array
let left = 0;
// Right pointer starts at the end of the array
let right = s.length - 1;
// Keep swapping until the two pointers converge
while (left < right) {
// Store the left character in a temporary variable
const temp = s[left];
// Overwrite the left position with the right character
s[left] = s[right];
// Overwrite the right position with the saved left character
s[right] = temp;
// Advance left pointer inward
left++;
// Advance right pointer inward
right--;
}
// No return needed — the array is modified in-place
}Approach 2 — Recursive Two Pointers
The same two-pointer logic can be written recursively. The base case is when left >= right (pointers have met or crossed). Each recursive call swaps the outermost unprocessed pair and then recurses on the inner subarray.
This approach has the same O(n) time complexity but uses O(n) space on the call stack due to recursion depth. It is not the optimal solution for this problem, but interviewers sometimes ask for it to test whether you can translate an iterative pattern into a recursive one.
Python
from typing import List
class Solution:
def reverseString(self, s: List[str]) -> None:
"""
Recursive approach: swap outermost pair, then recurse inward.
Time: O(n) Space: O(n) call stack
"""
def helper(left: int, right: int) -> None:
# Base case: pointers have met or crossed — nothing left to swap
if left >= right:
return
# Swap the characters at the current outermost positions
s[left], s[right] = s[right], s[left]
# Recurse on the inner subarray, moving both pointers inward
helper(left + 1, right - 1)
# Kick off the recursion with the full array bounds
helper(0, len(s) - 1)JavaScript
/**
* @param {character[]} s
* @return {void}
*/
function reverseString(s) {
// Inner recursive helper that operates on a subarray defined by [left, right]
function helper(left, right) {
// Base case: pointers have met or crossed — stop recursing
if (left >= right) return;
// Swap the outermost characters of the current subarray
const temp = s[left];
s[left] = s[right];
s[right] = temp;
// Recurse inward — shrink the subarray from both sides
helper(left + 1, right - 1);
}
// Start the recursion with the full array
helper(0, s.length - 1);
}Complexity Analysis
| Approach | Time | Space | Notes |
|---|---|---|---|
| Two Pointers (iterative) | O(n) | O(1) | Optimal — exactly floor(n/2) swaps, no allocations |
| Recursive Two Pointers | O(n) | O(n) | Call stack depth is n/2 — not in-place in the stack sense |
Slice reversal (Python s[::-1]) | O(n) | O(n) | Creates a temporary reversed copy — violates the space constraint |
Built-in .reverse() | O(n) | O(1) | Correct space, but you must know what it does internally |
The iterative two-pointer solution is the expected answer. Every character is visited at most once (as part of one swap), and no data structures beyond two integer variables are allocated. If an interviewer asks you to implement .reverse() from scratch without built-ins, this is exactly what they want.
Follow-up Questions
These are real interview escalations that appear after you solve the base problem at Meta, Microsoft, and Google. Each one extends the core pattern in a different direction.
Q1: How would you reverse only the words in a string, not the characters within each word?
This is LeetCode 151 — Reverse Words in a String. The classic trick: reverse the entire string character by character using this exact two-pointer approach, then reverse each individual word. The two-step reversal moves the words into the right order while restoring the characters within each word. You need to handle extra spaces (leading, trailing, and multiple spaces between words) by cleaning the string first or tracking word boundaries carefully. This question tests whether you can compose the primitive you just built into a larger solution.
Q2: What if the input were a linked list instead of an array?
This is LeetCode 206 — Reverse Linked List. You cannot use index-based two pointers on a linked list because there is no O(1) random access — you cannot jump to position n - 1 in constant time. Instead, you use an iterative three-pointer approach: prev, curr, and next. Walk forward through the list, reversing each next pointer to point backward. This tests whether you understand the structural difference between arrays (random access, O(1) index) and linked lists (sequential access, O(n) to reach a node).
Q3: Can you reverse only a subarray — say, from index i to index j?
Yes. Initialize left = i and right = j instead of 0 and n - 1. The rest of the algorithm is identical. This is actually used as a subroutine in the rotate array solution (LeetCode 189): to rotate an array right by k positions, you reverse the entire array, reverse the first k elements, then reverse the remaining n - k elements — three calls to this exact subarray-reversal function.
Q4: How would you reverse a string that contains Unicode characters — including multi-byte emoji?
This is a real gotcha. In Python 3, strings are Unicode-aware and characters are code points, so s = list("café") (cafe with combining accent) has 5 elements — reversing gives ['́', 'e', 'f', 'a', 'c'] which renders incorrectly because the combining accent mark is now attached to the wrong character. The correct approach is to reverse at the grapheme cluster level using a library like regex with \X pattern matching. JavaScript has a similar issue with surrogate pairs and combining characters. This question probes whether you understand that "character" is ambiguous and that string manipulation at the code-unit level can break multi-byte sequences.
Q5: What if you needed to reverse a very large string that does not fit in memory?
You cannot load the whole string at once. A streaming approach would use a temporary file or two-ended buffer: read blocks from the start and end simultaneously, swap them, write them back. For a file-based approach, you would seek to position i from the start and position n - 1 - i from the end, read one block from each, swap them, and write back. The two-pointer concept is the same — only the I/O mechanism changes. This tests system design thinking layered on top of the algorithmic pattern.
This Pattern Solves
The two-pointer in-place swap pattern you learned here directly underpins these problems — no new fundamental concepts required, only adaptations:
- LeetCode 125 — Valid Palindrome: use left/right pointers, skip non-alphanumeric characters, compare instead of swap. The pointer movement is identical.
- LeetCode 151 — Reverse Words in a String: reverse the whole string with this technique, then reverse each word's characters with the same technique.
- LeetCode 189 — Rotate Array: the three-reversal trick — reverse all, reverse first
k, reverse lastn-k— uses this as its core primitive three times. - LeetCode 206 — Reverse Linked List: same conceptual direction-reversal, adapted for pointer rewiring instead of index swapping.
- LeetCode 680 — Valid Palindrome II: two-pointer palindrome check with one allowed deletion — same convergence logic with a single branch.
- LeetCode 167 — Two Sum II: two pointers converging from both ends of a sorted array — same termination logic, different action (comparison instead of swap).
Key Takeaways
- LeetCode 344 — Reverse String is an Easy two-pointer problem asked at Meta and Microsoft to verify in-place thinking under the O(1) space constraint.
- Use left and right pointers converging toward the center; swap elements until they meet — stop at
left >= right, notleft > right. - Python's
s.reverse()ors[::-1]is off-limits in interviews asking for O(1) space — demonstrate the manual swap. - The two-pointer converging pattern transfers directly to Valid Palindrome (LC 125), Reverse Words in a String (LC 151), and Reverse Vowels (LC 345).
- Time O(n), space O(1) — only n/2 swaps are needed, interviewers appreciate mentioning this efficiency detail.
- Variant: reverse only a portion of an array (LC 189 Rotate Array) uses the same swap pattern applied three times.
- "From both ends simultaneously" is the mental trigger for this pattern — palindromes, mirrors, symmetry all cue it.
Advertisement