Two Pointers Technique in Java — A Practical Guide for Serious Engineers

Two Pointers is one of the most effective techniques for turning brute-force array/string solutions into linear-time solutions.

If you build backend systems, this matters for the same reason it matters in interviews: you learn to manage state and constraints precisely, with minimal memory overhead and predictable performance.


Why Do We Need Two Pointers?

Many array and string problems involve:

  • Pair comparisons
  • Subarray evaluation
  • Partitioning and compaction
  • Deduplication
  • Reversal and symmetry checks

A naive approach often uses nested loops:

for (int i = 0; i < n; i++) {
    for (int j = i + 1; j < n; j++) {
        // check condition
    }
}

Time complexity: O(n²)

Two pointers often reduces this to O(n) by moving indices strategically.


Core Idea

Maintain two indices and move them with a rule (an invariant) that guarantees progress.

Common patterns:

  1. Opposite direction: left from start, right from end (often sorted arrays / symmetry problems)
  2. Same direction (fast/slow): fast scans, slow compacts/builds answer in-place
  3. Sliding window: expand with right, shrink with left (subset of two pointers, but deserves its own deep dive)

Pattern 1: Opposite Direction (Sorted Array)

Problem: Two Sum in a Sorted Array

Given a sorted array, find two numbers whose sum equals target.

Efficient Solution (O(n), O(1))

public int[] twoSumSorted(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1;

    while (left < right) {
        int sum = nums[left] + nums[right];

        if (sum == target) {
            return new int[]{left, right};
        } else if (sum < target) {
            left++;
        } else {
            right--;
        }
    }
    return new int[]{-1, -1};
}

Why It Works (Invariant Thinking)

Because the array is sorted:

  • If nums[left] + nums[right] is too small, the only way to increase it is to move left rightward.
  • If it’s too large, the only way to decrease it is to move right leftward.

Invariant: At any time, all pairs outside [left, right] have already been ruled out.


Real-World Backend Example: Risk/Fraud Pair Detection on Sorted Streams

Imagine a fraud rule:

“Flag if two transactions by the same user within a short time window exceed a threshold.”

If transactions are sorted by timestamp:

  • left marks the start of the current time window
  • right scans forward
  • as right moves, advance left to keep the window valid

This avoids comparing every pair (O(n²)) and is scalable for large streams.


Pattern 2: Fast & Slow Pointer (In-Place Compaction)

This pattern is used to:

  • Remove duplicates
  • Remove elements matching a predicate
  • Partition arrays in-place
  • Reduce memory allocations

Example: Remove Duplicates (Sorted Array)

public int removeDuplicates(int[] nums) {
    if (nums.length == 0) return 0;

    int slow = 0; // last unique index

    for (int fast = 1; fast < nums.length; fast++) {
        if (nums[fast] != nums[slow]) {
            slow++;
            nums[slow] = nums[fast];
        }
    }
    return slow + 1;
}

Time: O(n)
Extra space: O(1)

Invariant: nums[0..slow] is always the “unique compacted prefix”.


Pattern 3: Two Pointers Over Strings (Symmetry / Palindrome)

Classic: validate palindrome by moving inward.

Example: Valid Palindrome (Ignoring Non-Alphanumerics)

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

    while (left < right) {
        char a = s.charAt(left);
        char b = s.charAt(right);

        if (!Character.isLetterOrDigit(a)) {
            left++;
            continue;
        }
        if (!Character.isLetterOrDigit(b)) {
            right--;
            continue;
        }

        if (Character.toLowerCase(a) != Character.toLowerCase(b)) {
            return false;
        }
        left++;
        right--;
    }
    return true;
}

Architecture Perspective (Why Backend Engineers Should Care)

Two pointers is more than a “DSA trick”:

  • In-place transformation → fewer allocations → less GC pressure
  • Cache-friendly sequential scans → better CPU locality than random-access-heavy approaches
  • Predictable performance → stable latency under load

If you’re processing large arrays, payload buffers, logs, or pre-sorted datasets, this technique is a practical optimization lever.


When Two Pointers Should Be Your First Thought

Try it first when:

  • The input is sorted (or you can sort without breaking constraints)
  • You need pairs/triplets with a condition (sum, distance, bounds)
  • You must modify data in place
  • The problem asks for the “longest/shortest subarray/substring with constraints” (often becomes sliding window)

Pros and Cons

Pros

  • Often reduces O(n²) → O(n)
  • Usually O(1) extra memory
  • Encourages clean invariants and simpler state
  • Great foundation for sliding window and partition problems

Cons

  • Easy to get boundary conditions wrong
  • Some variants require sorted input (or sorting changes meaning)
  • Debugging is harder without explicit invariants

Common Mistakes

  1. Moving the wrong pointer (breaks progress / misses solutions)
  2. Incorrect loop condition (left <= right vs left < right)
  3. Forgetting to handle duplicates correctly (e.g., 3Sum)
  4. Mixing responsibilities (pointer movement + business logic) without invariants

Best Practices

  1. Write the invariant as a comment
    • Example: // invariant: nums[0..slow] are unique
  2. Guarantee progress every loop
    • Every iteration must move left, right, fast, or slow.
  3. Be strict about preconditions
    • If the solution depends on sorted input, state it early.
  4. Keep pointer logic small
    • Extract helper methods when dealing with skipping rules (e.g., alphanumeric filter).

These problems cover the most useful two-pointer variants:

  1. Two Sum II — Input Array Is Sorted (LC 167)
    LeetCode

  2. 3Sum (LC 15) — sorting + opposite-direction pointers + duplicate handling
    LeetCode

  3. Container With Most Water (LC 11) — greedy pointer movement
    LeetCode
    GeeksforGeeks

  4. Trapping Rain Water (LC 42) — two-pointer with prefix-like reasoning
    LeetCode
    GeeksforGeeks

  5. Remove Duplicates from Sorted Array (LC 26) — fast/slow compaction
    LeetCode
    GeeksforGeeks

  6. Valid Palindrome (LC 125) — inward pointers + skipping rules
    LeetCode
    GeeksforGeeks (palindrome with two pointers)


Conclusion: Key Takeaways

  • Two pointers is a disciplined way to reason about constraints with minimal state.
  • The “secret” is not the pointers — it’s the invariant and progress rule.
  • Master the core patterns (opposite-direction, fast/slow, string symmetry) and you’ll recognize them everywhere.

If you want to get truly strong, practice the 6 problems above until you can implement each variant quickly and correctly in Java.


Author: Sandeep Bhardwaj
Senior Backend Engineer | Java | Distributed Systems