Valid Palindrome in Java DSA: Two Pointers from the Outside In
-
Last Updated: July 28, 2026
-
By: javahandson
-
Series

Learn Valid Palindrome in Java DSA step by step. Move from brute force to a clean two-pointer scan with full dry runs, code, and complexity comparisons for interviews.
Valid Palindrome in Java is where two-pointer thinking gets its first real workout. You have met arrays already in this series. Here you learn to walk a string from both ends at once.
The task sounds simple. You get a string of text. You must decide if it reads the same forwards and backwards.
One twist trips people up. You ignore anything that is not a letter or a number. You also treat capital and small letters as the same.
We build the answer in three steps, same as always. Brute force cleans the string, then reverses it. A cleaner version keeps the clean string but drops the reverse. The final one skips the extra string and walks two pointers inward in place.
Every approach comes with full, step-by-step dry runs on the same inputs. We trace a real palindrome and a non-palindrome, so you see both a true and a false result. Nothing is skipped.
Let us pin down the rules before touching code.
For that famous phrase the answer is true. Strip the spaces, commas and colon, lowercase everything, and you get “amanaplanacanalpanama”. That reads the same from either side. An empty string counts as a palindrome too.
Java gives us two small helpers that do the boring work for us.
Keep two pointers walking toward each other. One pointer left starts at the far left. The other pointer right starts at the far right end.
Think of two people reading a shelf from opposite ends. They compare the books they hold, then each steps inward. If every pair matches until they meet, the shelf is a mirror image.
| 💡 Interview Insight A common opener is “how do you decide which characters count?” Say two things: keep letters and digits, then lowercase before comparing. Naming both shows you clarify the rules before you code. |
Clean the string into a fresh version. Then reverse that clean string and compare the two. It uses extra memory, but it proves you understand the goal.
clean = empty string
for each char c in s: // keep only letters and digits
if c is a letter or a digit:
add lowercase(c) to clean
reversed = clean written backwards // flip the clean string
if clean equals reversed: // compare the two
return true
else:
return falsepublic class ValidPalindromeBrute {
public static boolean isPalindrome(String s) {
StringBuilder clean = new StringBuilder();
for (char c : s.toCharArray()) {
if (Character.isLetterOrDigit(c)) {
clean.append(Character.toLowerCase(c));
}
}
String forward = clean.toString();
String reversed = clean.reverse().toString();
return forward.equals(reversed);
}
public static void main(String[] args) {
String s = "Nurses run";
System.out.println(isPalindrome(s)); // true
}
}Input: s = “Nurses run”. The loop checks each character in order and builds clean. We trace all ten characters, so nothing is hidden. Watch clean grow on the right.
| Step | i | char | Letter or digit? | Action | clean after |
|---|---|---|---|---|---|
| 1 | 0 | N | Yes | lowercase to n, append | n |
| 2 | 1 | u | Yes | append u | nu |
| 3 | 2 | r | Yes | append r | nur |
| 4 | 3 | s | Yes | append s | nurs |
| 5 | 4 | e | Yes | append e | nurse |
| 6 | 5 | s | Yes | append s | nurses |
| 7 | 6 | (space) | No | skip, do nothing | nurses |
| 8 | 7 | r | Yes | append r | nursesr |
| 9 | 8 | u | Yes | append u | nursesru |
| 10 | 9 | n | Yes | append n | nursesrun |
After the loop, clean is “nursesrun”. Now the last two lines finish the job.
| Stage | What happens | Value |
|---|---|---|
| Reverse | Flip “nursesrun” end to end | nursesrun |
| Compare | Check if “nursesrun” equals “nursesrun” | equal, so return true |
Let us walk the whole trace and see what each step did to clean.
Steps 1 to 6: the first word goes in.
Step 7: the space is skipped.
Steps 8 to 10: the second word goes in.
The final check.
So the filter dropped one space and kept nine letters. The two extra strings, clean and reversed, are the memory cost we cut next.
Two extra strings is fine for five characters. For a million it doubles the memory, which is why we improve it.
We do not really need the reverse string. Once we have the clean string, we can compare its two ends ourselves. Keep the cleaning step, but drop the reverse.
clean = empty string
for each char c in s: // clean the string first
if c is a letter or a digit:
add lowercase(c) to clean
left = 0
right = clean.length - 1
while left < right: // walk inward from both ends
if clean[left] != clean[right]:
return false
left = left + 1
right = right - 1
return truepublic class ValidPalindromeClean {
public static boolean isPalindrome(String s) {
StringBuilder sb = new StringBuilder();
for (char c : s.toCharArray()) {
if (Character.isLetterOrDigit(c)) {
sb.append(Character.toLowerCase(c));
}
}
String clean = sb.toString();
int left = 0;
int right = clean.length() - 1;
while (left < right) {
if (clean.charAt(left) != clean.charAt(right)) {
return false;
}
left++;
right--;
}
return true;
}
public static void main(String[] args) {
String s = "Nurses run";
System.out.println(isPalindrome(s)); // true
}
}Input: s = “Nurses run”. Cleaning gives clean = “nursesrun” (the same ten-step filter as before). Now the pointers compare the ends of “nursesrun”. We trace every pass, nothing skipped.
| Step | left | right | clean[left] | clean[right] | Match? | Action |
|---|---|---|---|---|---|---|
| 1 | 0 | 8 | n | n | Yes | move both inward |
| 2 | 1 | 7 | u | u | Yes | move both inward |
| 3 | 2 | 6 | r | r | Yes | move both inward |
| 4 | 3 | 5 | s | s | Yes | move both inward |
| end | 4 | 4 | — | — | — | left < right false, stop |
After step 4, left is 4 and right is 4, both on the middle e. Since left is no longer less than right, the loop ends and the method returns true.
Now try a non-palindrome to see a false result. Input: s = “Race a car”, which cleans to “raceacar”. Watch it fail partway.
| Step | left | right | clean[left] | clean[right] | Match? | Action |
|---|---|---|---|---|---|---|
| 1 | 0 | 7 | r | r | Yes | move both inward |
| 2 | 1 | 6 | a | a | Yes | move both inward |
| 3 | 2 | 5 | c | c | Yes | move both inward |
| 4 | 3 | 4 | e | a | No | return false |
The first three pairs match, so “raceacar” looks promising. But at step 4, e does not equal a, so the method returns false right away.
Let us walk both traces and see how the pointers decide the answer.
Palindrome case: “nursesrun”.
Non-palindrome case: “raceacar”.
Notice the win over brute force. We used only two integer pointers, no reverse string. But we still built the clean string once, which the final approach removes.
We dropped the reverse copy, which is real progress. One clean string still costs memory, and the final approach removes even that.
| 💡 Interview Insight A sharp follow-up asks why you return false the instant a pair differs. Answer that early exit avoids wasted work: once one pair breaks the mirror, the rest cannot fix it. Failing fast keeps the average run short. |
Skip the clean string entirely. Run the two pointers straight on the original text. When a pointer lands on a space or comma, it simply steps over it.
left = 0
right = s.length - 1
while left < right:
while left < right and s[left] is not a letter or digit:
left = left + 1 // skip junk on the left
while left < right and s[right] is not a letter or digit:
right = right - 1 // skip junk on the right
if lowercase(s[left]) != lowercase(s[right]):
return false
left = left + 1
right = right - 1
return truepublic class ValidPalindromeTwoPointer {
public static boolean isPalindrome(String s) {
int left = 0;
int right = s.length() - 1;
while (left < right) {
while (left < right && !Character.isLetterOrDigit(s.charAt(left))) {
left++;
}
while (left < right && !Character.isLetterOrDigit(s.charAt(right))) {
right--;
}
char a = Character.toLowerCase(s.charAt(left));
char b = Character.toLowerCase(s.charAt(right));
if (a != b) {
return false;
}
left++;
right--;
}
return true;
}
public static void main(String[] args) {
String s = "Nurses run";
System.out.println(isPalindrome(s)); // true
}
}Input: s = “Nurses run”, used raw with no cleaning. The pointers scan the original string and skip the space live. We trace every pass, nothing skipped.
| Step | left | right | Skip? | s[left] | s[right] | lowercased compare | Action |
|---|---|---|---|---|---|---|---|
| 1 | 0 | 9 | none | N | n | n == n | match, move inward |
| 2 | 1 | 8 | none | u | u | u == u | match, move inward |
| 3 | 2 | 7 | none | r | r | r == r | match, move inward |
| 4 | 3 | 5 | right skips space at 6 | s | s | s == s | match, move inward |
| end | 4 | 4 | — | e | e | — | left < right false, stop |
Step 4 is the interesting one. After step 3, right sat on the space at index 6. The inner skip loop pulled it back to index 5 before comparing. After step 4, both pointers meet at index 4, the middle e, so the loop ends and returns true.
Now the non-palindrome. Input: s = “Race a car”, scanned raw. Watch it fail partway.
| Step | left | right | Skip? | s[left] | s[right] | lowercased compare | Action |
|---|---|---|---|---|---|---|---|
| 1 | 0 | 9 | none | R | r | r == r | match, move inward |
| 2 | 1 | 8 | none | a | a | a == a | match, move inward |
| 3 | 2 | 7 | none | c | c | c == c | match, move inward |
| 4 | 3 | 5 | right skips space at 6 | e | a | e != a | return false |
At step 4, right skips the space at index 6, then compares e with a. They differ, so the method returns false without scanning the rest.
Let us walk both scans and see how the skip loops and the compare work together.
Palindrome case: “Nurses run”.
Non-palindrome case: “Race a car”.
Notice how the skip happens live, in the middle of the scan, with no separate cleaning pass. That is what keeps the extra memory down to two pointers.
Same idea, same answer, three very different amounts of memory.
| Approach | Passes over input | Extra memory used |
|---|---|---|
| Brute force | Clean, reverse, compare | Two strings the size of input |
| Clean then compare | Clean, then one scan | One clean string |
| Two-pointer scan | One scan, skipping live | Two int pointers |
On five characters the difference looks small. On a very long string it becomes the difference between two extra copies and almost no extra memory at all.
| 💡 Interview Insight Expect this question: why skip junk with an inner loop instead of cleaning first? Because cleaning builds a whole new string, while the inner loops skip in place. The two-pointer scan reaches O(1) extra space, which is the key efficiency point. |
Tables are precise, but a sketch often lands faster. Here is the same two-pointer scan drawn by hand.

The string sits along the top with its indices. Each step block on the left shows the two characters we compare. The panel on the right shows what the pointers found.
All three give a correct answer. They just pay different prices for it.
| Approach | Time | Space | Note |
|---|---|---|---|
| Brute force | O(n) | O(n) | Simple, but builds two strings |
| Clean then compare | O(n) | O(n) | One clean string, clear pointers |
| Two-pointer scan | O(n) | O(1) | In place, single pass, expected answer |
Notice that all three run in linear time. The real story here is space, not speed. The brute force wastes two strings, while the final scan carries just two integers.
In an interview, start with brute force to show you understand the goal. Then explain the wasted memory you spotted. Finally tighten it into the two-pointer scan.
| 💡 Interview Insight If asked to handle Unicode or accented letters, mention that isLetterOrDigit and toLowerCase already understand more than plain ASCII. Naming that edge shows you think past the simple English case. |
A few small traps catch beginners here. Keep them in mind.
Run those last three cases through your code before you call it done. They catch more bugs than any ordinary input will.
A: A valid palindrome is a string that reads the same forwards and backwards once you ignore case and keep only letters and digits. For example, “A man, a plan, a canal: Panama” is valid because its cleaned form “amanaplanacanalpanama” mirrors itself.
A: Put one pointer at the start and one at the end. Skip any character that is not a letter or a digit, lowercase the two real characters, and compare them. Move both pointers inward and repeat until they meet. If every pair matches, the string is a palindrome.
A: The two-pointer scan runs in O(n) time because each character is visited at most once, and O(1) space because it uses only two integer pointers. The brute-force version is O(n) time but O(n) space, since it builds a cleaned string and a reversed copy.
A: Yes. An empty string, and a string made only of punctuation, both count as palindromes. There are no letters or digits to break the symmetry, so the method returns true.
Valid Palindrome in Java looks like a warm-up, and in a way it is. But the two-pointer, in-place pattern inside it is a real skill.
Our traces showed the payoff clearly. Brute force needed two extra strings. The two-pointer scan needed just two integers and a single sweep.
So take the pattern, not just the answer. Reach for a clean copy only when you truly must. The moment you can scan in place, a left pointer and a right pointer often do the whole job.
That habit turns three passes into one here. It will do the same for many string problems waiting further down the list.
javahandson.com | DSA Series | Arrays & Strings