Menu

Earn Premium with Referrals

Invite your friends and earn Premium rewards through our referral program.

See how it works and start inviting friends.

Two Pointers
DSA

Two Pointers

Learn how two pointers can efficiently process strings from both ends or across a sequence.

Two Pointers on strings moves inward from both ends to compare or swap characters.

Focus on recognizing:

“Palindrome” / “reverse” / “compare ends” → left at 0, right at n−1


Pattern 1: Valid Palindrome

Watch both pointers converge on "level" — every pair matches, so it’s a palindrome. Press to animate.

Valid Palindrome

Check if a string reads the same forwards and backwards. Two pointers start at the ends and walk inward, comparing mirrored characters; a single mismatch means it is not a palindrome.

Input: l e v e l. Left starts at 0, Right at 4. Compare the mirrored pair, and if they match move both inward. When the pointers cross the middle, every pair matched. The highlighted cells are the current L/R pair; L/R labels mark the pointers.

ARRAY VISUALIZER
Steps
l
0
e
1
v
2
e
3
l
4
Press ▶ to animate, or step through manually.
Variables
keys: ← → space F
Pseudocode

                        1
                        left = 0, right = n - 1
                      
                        2
                        while left < right:
                      
                        3
                          if s[left] != s[right]: return false
                      
                        4
                          left++, right--
                      
                        5
                        return true
                      
public boolean isPalindrome(String s) {
    int left = 0, right = s.length() - 1;

    while (left < right) {
        while (left < right
                && !Character.isLetterOrDigit(s.charAt(left)))
            left++;                       // skip non-alphanumeric

        while (left < right
                && !Character.isLetterOrDigit(s.charAt(right)))
            right--;

        if (Character.toLowerCase(s.charAt(left))
                != Character.toLowerCase(s.charAt(right))) {
            return false;
        }

        left++;
        right--;
    }

    return true;
}
def is_palindrome(s):
    left, right = 0, len(s) - 1

    while left < right:
        while left < right and not s[left].isalnum():
            left += 1                     # skip non-alphanumeric

        while left < right and not s[right].isalnum():
            right -= 1

        if s[left].lower() != s[right].lower():
            return False

        left += 1
        right -= 1

    return True
bool isPalindrome(string s) {
    int left = 0, right = s.size() - 1;

    while (left < right) {
        while (left < right && !isalnum(s[left]))
            left++;                       // skip non-alphanumeric

        while (left < right && !isalnum(s[right]))
            right--;

        if (tolower(s[left]) != tolower(s[right]))
            return false;

        left++;
        right--;
    }

    return true;
}
function isPalindrome(s) {
  const isAlnum = (c) => /[a-z0-9]/i.test(c);
  let left = 0,
    right = s.length - 1;

  while (left < right) {
    while (left < right && !isAlnum(s[left])) left++;
    while (left < right && !isAlnum(s[right])) right--;

    if (s[left].toLowerCase() !== s[right].toLowerCase())
      return false;

    left++;
    right--;
  }

  return true;
}

Skip invalid characters inside the loop — bounds check (left < right) on every inner while.


Compare ends → move both inward → stop when they meet. That’s the whole pattern.


Pattern 2: Reverse String (In-Place)

public void reverseString(char[] s) {
    int left = 0, right = s.length - 1;

    while (left < right) {
        char tmp = s[left];
        s[left] = s[right];
        s[right] = tmp;

        left++;
        right--;
    }
}
def reverse_string(s):
    left, right = 0, len(s) - 1

    while left < right:
        s[left], s[right] = s[right], s[left]
        left += 1
        right -= 1
void reverseString(vector<char>& s) {
    int left = 0, right = s.size() - 1;

    while (left < right) {
        swap(s[left], s[right]);
        left++;
        right--;
    }
}
function reverseString(s) {
  let left = 0,
    right = s.length - 1;

  while (left < right) {
    [s[left], s[right]] = [s[right], s[left]];
    left++;
    right--;
  }
}

Pattern 3: Valid Palindrome II (Delete At Most One)

On the first mismatch, try skipping EITHER side — one of the two must be a palindrome:

public boolean validPalindrome(String s) {
    int left = 0, right = s.length() - 1;

    while (left < right) {
        if (s.charAt(left) != s.charAt(right)) {
            return isRange(s, left + 1, right)
                || isRange(s, left, right - 1);
        }
        left++;
        right--;
    }

    return true;
}

private boolean isRange(String s, int i, int j) {
    while (i < j) {
        if (s.charAt(i++) != s.charAt(j--)) return false;
    }
    return true;
}
def valid_palindrome(s):
    def is_range(i, j):
        while i < j:
            if s[i] != s[j]:
                return False
            i += 1
            j -= 1
        return True

    left, right = 0, len(s) - 1

    while left < right:
        if s[left] != s[right]:
            return is_range(left + 1, right) \
                or is_range(left, right - 1)
        left += 1
        right -= 1

    return True
bool isRange(string& s, int i, int j) {
    while (i < j) {
        if (s[i++] != s[j--]) return false;
    }
    return true;
}

bool validPalindrome(string s) {
    int left = 0, right = s.size() - 1;

    while (left < right) {
        if (s[left] != s[right]) {
            return isRange(s, left + 1, right)
                || isRange(s, left, right - 1);
        }
        left++;
        right--;
    }

    return true;
}
function validPalindrome(s) {
  const isRange = (i, j) => {
    while (i < j) {
      if (s[i++] !== s[j--]) return false;
    }
    return true;
  };

  let left = 0,
    right = s.length - 1;

  while (left < right) {
    if (s[left] !== s[right]) {
      return isRange(left + 1, right) || isRange(left, right - 1);
    }
    left++;
    right--;
  }

  return true;
}

Both skip options are needed — you can’t know which side holds the “bad” character without checking.


Common Mistakes

Unbounded inner skips.

Every inner while needs its own left < right guard or you walk off the string.


Trying only one deletion side in Palindrome II.

"cbbcc" fails if you only skip the left — you must try both branches.


Reversing with an extra array.

The whole point is O(1) space — swap in place.


Complexity

PatternTimeSpace
Valid palindromeO(n)O(1)
ReverseO(n)O(1)
Palindrome IIO(n)O(1)

My Private Notes

Notes are auto-saved locally to this device.