A common point of confusion arises when modifying variables directly inside method arguments using increment/decrement operators.

Core Concepts

  • Postfix (i++ or j--): The expression evaluates to the current value of the variable. Only after the entire expression/statement evaluates does the variable increment or decrement.
  • Prefix (++i or --j): The variable is modified first, and then that updated value is substituted into the expression.

Application in the Palindrome Example

In the recursive palindrome attempt:

isPalindrome(s.substring(i++, j));

Because i++ is postfix, Java evaluates s.substring(0, j) for that specific method call. Only after the substring method finishes calculating and the arguments are passed does i become 1 for the calling scope. However, because a brand-new stack frame is spawned with the newly returned substring, modifying the outer i variable had zero effect on the inner recursive execution anyway.

Analyzing the Snippet’s Approach vs. Postfix Mistakes

In the below snippet:

String newStr = str.substring(1, str.length() - 1);
return isPalindrome(newStr);

Here, explicit boundary extraction avoids inline postfix confusion entirely by calculating 1 and str.length() - 1 explicitly before passing them into substring.

If someone attempts to inline postfix operations like isPalindrome(str.substring(i++, j--)):

  1. Because it’s postfix, i++ evaluates to 0 for the current call before incrementing i behind the scenes.
  2. However, because substring creates a brand new string object rather than tracking index pointers in-place, the local variable modifications on i and j do not control the next frame; rather, the shrinking string length dictates the new boundaries. Mixing index variables with string slicing often leads to boundary bugs.

Recommended Solutions

Approach A: The Iterative Two-Pointer Standard (Best Practice)

For flat structures like strings or arrays, an iterative loop is always superior: time and space. Use this even for production code and strict interview time limit

class Solution {
    public boolean isPalindrome(String s) {
        if (s == null) return false;
        int i = 0;
        int j = s.length() - 1;
        
        while (i < j) {
            if (s.charAt(i) != s.charAt(j)) {
                return false;
            }
            i++;
            j--;
        }
        return true;
    }
}

Approach B: The Working Slice-Based Recursion

This version works cleanly because it avoids inline operator confusion and relies on string length reduction:

public static boolean isPalindrome(String str) {
    if (str.length() == 0 || str.length() == 1)
        return true;
 
    if (str.charAt(0) == str.charAt(str.length() - 1)) {
        // Note: Substring endIndex is exclusive (extends to endIndex - 1)
        String newStr = str.substring(1, str.length() - 1);
        return isPalindrome(newStr);
    }
    return false;
}
  • Trade-off: Elegant and easy to read, but incurs time complexity and higher memory overhead due to repeated substring heap allocations. It has a risk of timeout

Approach C:

If you must use recursion without triggering OutOfMemoryError or TimeLimitExceeded, avoid string slicing (substring). Instead, pass index pointers down the call stack:

class Solution {
    public boolean isPalindrome(String s) {
	    // Considering empty string " " as palindrome
        if (s == null) return true;
        return checkPalindrome(s, 0, s.length() - 1);
    }
    
    private boolean checkPalindrome(String s, int i, int j) {
        // Base case: pointers have crossed or met in the middle
        if (i >= j) {
            return true;
        }
        // Check outer boundaries
        if (s.charAt(i) != s.charAt(j)) {
            return false;
        }
        // Recurse inward using index pointer shifts ($O(1)$ space per frame beyond stack)
        return checkPalindrome(s, i + 1, j - 1);
    }
}
 

Comparison - Approach B and C

string is getting passed multiple times in checkPalindrome. Won't it degrade performance ?

It feels intuitive that passing s around in every single recursive call would cause a performance bottleneck. The secret lies in how Java handles variable and object passing and the difference comes down to Heap Allocation vs. Call Stack Overhead:

1. The Substring Approach (Causes Timeouts)

When you use s.substring(1, s.length() - 1), Java cannot magically shrink the existing string. Because Strings are immutable, Java has to:

  • Allocate a brand-new String object on the heap for every single recursive call.
  • Copy the character array subset over to that new object.
  • If a test string has characters, you are copying millions of characters across thousands of frames. This degrades performance to time complexity and triggers heavy garbage collection overhead, easily hitting LeetCode’s time limit.

2. The Pointer-Passing Approach (Passes Fast)

In the checkPalindrome(s, i + 1, j - 1) version:

  • The original string (String s parameter )never moves or gets copied. It stays right where it is in memory.

    • Every time the method recurses, Java copies the memory address value of s (a fixed-size 64-bit reference) onto the new stack frame.
    • The actual character array of the string on the heap is never copied, duplicated, or moved, which save time and memory.
      • As passing an object into a method of Java does not copy the object
    • Every recursive frame is simply handed a copy of that same memory address, pointing to the exact same original String object sitting on the heap.
    • Whether your string has 10 characters or 10 million characters, passing the variable s takes the exact same negligible amount of time ().
    • Check 2. Java Pass by value
  • You are only passing two primitive integers (i and j) down the call stack.

    • Java copies the primitive integer values (i + 1 and j - 1) onto the stack frame, which is practically zero memory
    • Primitive integer copying is a single CPU-register instruction and is instant

The time complexity drops straight down to because it only checks characters at the given indices in constant time ( work per frame) until the pointers meet. While it still uses stack space for the recursive frames (depth ), primitive values are so lightweight that it easily finishes well within the time limit for all test cases.