Valid Palindrome in Java DSA: Two Pointers from the Outside In
-
Last Updated: July 25, 2026
-
By: javahandson
-
Series
Learn Java in a easy way
Learn Valid Palindrome in Java DSA step by step. Go from brute force to a clean two-pointer scan with dry runs, full code, and complexity comparisons for interviews.
Valid Palindrome in Java is the seventh problem in this series. It follows Merge Sorted Array, and it opens a brand new topic: the two-pointer scan. 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. But there is a twist that trips up beginners.
You ignore anything that is not a letter or a number. You also ignore the difference between capital and small letters. So a messy phrase with spaces and commas can still be a clean palindrome underneath.
We build the answer in three steps. Brute force cleans the string first, then reverses it. A cleaner version keeps the reverse but drops one loop. The final two-pointer scan skips the extra string entirely and checks in place.
Every approach here comes with a full dry run. We trace the same input through each one, so you can watch the extra memory melt away step by step.
Let us fix the rules before we write any code. A clear problem statement now saves you from silent bugs later.
For that famous phrase the answer is true. Strip the spaces, commas and the colon, lowercase everything, and you get “amanaplanacanalpanama”. That reads the same from either side.
Notice one easy trap. An empty string counts as a palindrome. There is nothing to break the symmetry, so the answer is true.
The raw string is full of noise. Only the letters and digits carry meaning here.
Miss this rule and a real palindrome looks broken. The comparison would trip over a comma and wrongly answer false.
Our phrase is picked to be a little awkward. That keeps the traces honest.
A tidy word like “level” hides all of this. A messy phrase shows you what the cleaning and the pointers really do.
| 💡 Interview Insight Interviewers often ask what counts as a valid character. Confirm the rule out loud: keep letters and digits, drop everything else, and treat case as equal. Stating your assumptions early shows you clarify before you code. |
Two small ideas carry this whole problem. Both come back often in later string questions, so learn them well.
Java gives us small helpers for single characters. They do the boring work for us.
So we never build a big set of rules by hand. These two helpers cover every case we need.
Here is the idea that makes this problem click. You keep two pointers walking toward each other.
One pointer starts on the far left. It reads the first character and then steps to the right. We call it left.
The other pointer starts on the far right. It reads the last character and then steps to the left. We call it right.
Think of two people reading a shelf from opposite ends. They compare the books they hold, then each takes one step inward. If every pair matches until they meet, the shelf is a mirror image.
The first idea most people reach for feels natural. Clean the string into a fresh version first. Then reverse that clean string and compare the two.
It is simple, but it uses extra memory. We build two new strings along the way. Still, it is a good warm-up, and it proves you understand the goal.
clean = empty string
for each char c in s: // pass 1: keep only letters and digits
if c is a letter or a digit:
add lowercase(c) to clean
reversed = clean written backwards // pass 2: flip the clean string
if clean equals reversed: // pass 3: compare the two
return true
else:
return falseLet us read this slowly, one block at a time. The plan has three clear passes. First we clean, then we reverse, then we compare. Think of it as copying a note onto fresh paper, holding it up to a mirror, and checking the two match.
Start at the very top. We create an empty string called clean. Right now it holds nothing. It will slowly collect only the characters we care about.
Now look at the first loop. It walks the original string from left to right, one character at a time. At each character, it asks one yes-or-no question.
So this loop is a filter. Letters and digits pass through, and everything else falls away. Because we read left to right and never reorder, the good characters land in clean in their original sequence. That point matters, since order is the whole idea of a palindrome.
After that loop, pause and picture the state. For our phrase, clean now holds “amanaplanacanalpanama”. Every space, comma, and colon is gone, and every capital is now small. The messy input has become one tidy lowercase string.
Next comes the reverse step. We build a second string that is clean written backwards. If clean is “abcba”, then reversed is also “abcba”. If clean were “abcd”, then reversed would be “dcba”.
Here is the key insight behind the whole approach. A palindrome is a string that equals its own reverse. That is the exact definition we are leaning on.
That final comparison is the whole decision. We do not check character by character ourselves. We let the equals check compare the two strings for us and hand back the answer.
So the whole plan reads as three simple moves: filter the string down to clean characters, flip that clean string, then ask whether the two versions are identical.
public 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 = "A man, a plan, a canal: Panama";
System.out.println(isPalindrome(s)); // true
}
}Each part here has one clear job. Let us walk through them slowly.
One detail deserves a second look. We use StringBuilder, not a plain String, to collect characters.
This code is easy to read and hard to get wrong. That is its real strength. The weakness is the extra strings, and we trim them next.
Let us trace s = “A man, a plan, a canal: Panama”. Watch clean grow as we filter the first few characters.
| Step | char | Letter or digit? | clean after this step |
|---|---|---|---|
| 1 | A | Yes | a |
| 2 | (space) | No | a |
| 3 | m | Yes | am |
| 4 | a | Yes | ama |
| 5 | n | Yes | aman |
| 6 | , | No | aman |
| 7 | (space) | No | aman |
| 8 | a | Yes | amana |
The filter keeps going for the rest of the phrase. Once every character has been checked, clean holds the full tidy string.
| Stage | What happens | Value |
|---|---|---|
| Clean | Filter finishes over the whole phrase | amanaplanacanalpanama |
| Reverse | Flip the clean string end to end | amanaplanacanalpanama |
| Compare | Check if forward equals reversed | equal, so return true |
The filter did the heavy lifting, then one compare gave the answer. Let us break that down.
Now look at the hidden cost. We built two whole new strings to reach the answer.
Brute force walks the string a few times, but never nests loops. So the time stays linear.
Our phrase needed two extra strings. Grow the input to a million characters and you pay for two million extra slots. That memory cost is the pain point we solve next.
The dry run above handed us a hint. We do not really need the reverse string. Once we have the clean string, we can compare its two ends ourselves.
So let us keep the cleaning step, but drop the reverse. We put one pointer at the start of clean and one at the end. Then we walk them inward, comparing as we go.
This version still builds one clean string, so space is not perfect yet. But it removes the second copy and shows the pointer idea in a gentle way.
clean = empty string
for each char c in s: // still 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 trueThe first block is the same cleaning step as before, so we will not repeat it. The new idea starts once clean is ready. Instead of reversing, we set up two pointers and let them do the checking.
Look at the two lines that set up the pointers. We put left at index 0, the very first character. We put right at the last index, which is the length minus one.
Now the while loop drives everything. It keeps running as long as left is still to the left of right. The moment they meet or cross, the loop stops.
Inside the loop we make one comparison. We check whether the character at left is different from the character at right.
Here is why the two pointers move toward each other. A palindrome must mirror around its centre. So the first character should match the last, the second should match the second to last, and so on.
Let us picture it on a small clean string, say “amana”. At the start, left points at the first a and right points at the last a. They match, so left becomes 1 and right becomes 3. Now both point at m and n… wait, they point at m and n only if the string is not a palindrome. In “amana” they point at m and a, which do not match, so this shows how a mismatch would surface. For a true palindrome every such pair lines up.
One more thing about the exit. If the loop finishes without ever returning false, every pair matched. That means the string mirrored perfectly, so we return true at the end.
So the plan is two clean stages: build the tidy string once, then close two pointers inward until they meet, failing fast on any mismatch.
public 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 = "A man, a plan, a canal: Panama";
System.out.println(isPalindrome(s)); // true
}
}The shape is simple, so let us focus on why each piece exists.
Compare this with the brute force. The reverse string is gone completely, replaced by two small integers.
Take the small clean string “amana” to keep the trace short. Watch the pointers close inward.
| Step | left | right | clean[left] | clean[right] | Match? | Action |
|---|---|---|---|---|---|---|
| 1 | 0 | 4 | a | a | Yes | move both inward |
| 2 | 1 | 3 | m | n | No | return false |
Legend: left is the pointer moving from the start, right is the pointer moving from the end.
That short string “amana” is not a real palindrome, so it fails at step 2. For our true phrase “amanaplanacanalpanama”, every pair would match and the loop would end with true.
Two steps settled this small case. Let us look closely.
Notice the memory win over brute force. We used only two integer pointers here.
| 💡 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. |
The version above is close, but it still builds a clean string. So what is left to improve?
We can skip the clean string entirely. We run the two pointers straight on the original text. When a pointer lands on a space or comma, it simply steps over it.
Think of it as walking the raw string from both ends, ignoring the noise as you pass it. You compare only the real characters, right where they sit.
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 trueThis is the tightest version, so let us take it gently. There is no cleaning pass and no extra string. We work on the raw input the whole time. The trick is a pair of small inner loops that skip the junk.
As before, left starts at index 0 and right starts at the last index. The big outer while loop keeps them moving toward each other until they meet.
Now look at the first inner loop. It says: while left is not yet past right, and the character at left is junk, step left forward.
The second inner loop does the mirror job on the right side. While right sits on junk, it pulls right backward one step at a time. It stops the moment right lands on a real character.
After both inner loops finish, both pointers sit on real characters. Only now do we compare them. We lowercase each one first, so a capital and a small letter count as equal.
Here is the part beginners find clever. We never copy the string. The skipping happens live, inside the same scan that does the comparing. One walk does both jobs at once.
Let us walk a tiny piece on “a,a”. At the start left is on the first a and right is on the last a. Neither is junk, so no skipping happens. They match, so left moves to index 1 and right moves to index 1. Now left equals right, so the outer loop stops and we return true. The comma in the middle was simply never visited.
One last worry: could a pointer skip too far and cross over? No. Every inner loop carries the guard left < right. The moment the pointers meet, all skipping stops, so they never trade places.
So the plan is a single sweep: skip junk on the left, skip junk on the right, compare the two real characters, then step inward and repeat.
public 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 = "A man, a plan, a canal: Panama";
System.out.println(isPalindrome(s)); // true
}
}This version does the most thinking in the fewest lines, so let us go gently.
Two details separate this from the clean-and-compare version.
Because each character is visited at most once, one sweep is enough. That single idea is what makes this solution shine.
Now we trace a short raw string “a,a” one final time. Watch the comma get skipped only when a pointer reaches it.
| Step | left | right | s[left] | s[right] | Action |
|---|---|---|---|---|---|
| 1 | 0 | 2 | a | a | both real, match, move inward |
| 2 | 1 | 1 | ‘,’ | ‘,’ | left == right, loop ends |
Legend: left scans from the start, right scans from the end, and junk is skipped live.
Two steps end the whole thing. Let us look at each one closely.
This case never needed the skip loops, since the ends were both real letters. A messier string shows the skipping in action.
Same idea, same answer, three very different amounts of work and 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 |
The gap grows with size. On a short phrase 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 and the question we ask. The panel on the right shows what the pointers found.
Follow the colour cues as you read it.
Some people read pictures faster than tables. Use whichever version clicks for you, or sketch your own on paper while you follow the code.
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.
That climb from wasteful to lean is exactly the story interviewers want to hear. Jumping straight to the optimal answer skips the reasoning they came to observe.
| 💡 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 say you are finished. They catch more bugs than any normal 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 a valid palindrome because its cleaned form “amanaplanacanalpanama” mirrors itself.
A: Put one pointer at the start and one at the end of the string. 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 with no extra string. The brute-force version is also 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 of the same size. 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 turned three passes into one here. It will do the same for many string problems waiting further down the list.