Junior 6 min · March 06, 2026

Two Pointer Bug: Unsorted Data Skips Valid Pairs

Unsorted input causes opposite-ends two-pointer to skip valid pairs silently — no error, wrong results.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Core concept: Two pointers eliminate O(n²) brute force by using sorted order or relative speeds to skip impossible pairs.
  • Three variants exist: opposite-ends for sorted pair-finding, same-direction slow/fast for in-place modification, and fast-slow (Floyd's) for cycle detection.
  • Each variant: O(n) time, O(1) space — no extra data structures.
  • Performance insight: After sorting O(n log n), the pointer walk is O(n) — total O(n log n) beats O(n²) at input sizes above ~200.
  • Production insight: Applying opposite-ends on unsorted data silently returns wrong results — always sort first or use a HashMap.
  • Biggest mistake: Moving both pointers when only one should move — you'll skip valid pairs and produce false negatives.
Plain-English First

Imagine you're looking for two puzzle pieces that fit together inside a sorted box. Instead of picking every possible pair and testing them one by one, you start with one person at each end of the box and they walk toward each other — one steps inward when the pair is too big, the other steps inward when it's too small. They meet in the middle having checked every useful combination without ever doubling back. That walk-toward-each-other trick is exactly what the two pointer technique is.

Every coding interview has a handful of patterns that show up again and again. Two pointers is one of the most common — and one of the most misunderstood. Candidates often brute-force their way through problems that a senior dev would knock out in O(n) time because they don't recognise the signal that two pointers is the right tool. Missing that signal doesn't just cost you efficiency points; it costs you the job.

The two pointer technique exists to eliminate the need for nested loops. Any time you find yourself writing a loop inside a loop to compare or relate two positions in the same collection, there's a good chance you're doing more work than you need to. Two pointers lets you move through the data from two positions simultaneously — either from opposite ends converging toward the middle, or from the same starting point racing at different speeds — and cut your time complexity from O(n²) down to O(n).

By the end of this article you'll be able to identify which of the three two-pointer variants to reach for (opposite ends, same direction, or fast-slow), implement the three most interview-common problems from scratch without hints, and explain your reasoning out loud — which is the part that actually gets you hired.

Why Two Pointers Beat Brute Force — The Core Intuition

Let's get the intuition rock solid before touching code. The brute-force approach to 'find two numbers that sum to a target' is to check every pair: index 0 with index 1, index 0 with index 2, and so on. That's n*(n-1)/2 comparisons — O(n²). It works, but it's slow and it signals to an interviewer that you haven't spotted the pattern.

The key insight is this: if your data is sorted, you already know something about every element's relationship to every other element without checking them all. If the number at the left pointer plus the number at the right pointer is too large, you don't need to check the right pointer against anything to its right — they'd all be even larger. So you move the right pointer one step inward. If the sum is too small, you move the left pointer one step inward. Each step eliminates an entire class of possibilities.

This is why two pointers almost always requires a sorted array for the opposite-ends variant. The sort itself costs O(n log n), but the pointer walk is O(n), so your overall complexity is O(n log n) — still far better than O(n²) for large inputs. When the input is already sorted, you get pure O(n).

Now here's a nuance most tutorials skip: the elimination trick only works because the array is monotonic. If you applied the same logic to an unsorted array, moving the left pointer when the sum is too small might actually skip the correct pair — because a larger value could be hiding to the left of the current left pointer. That's why sorting is non-negotiable for this variant.

TwoSumSorted.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class TwoSumSorted {

    /**
     * Given a SORTED array of integers and a target sum,
     * return the 1-based indices of the two numbers that add up to the target.
     * Assumes exactly one solution exists.
     *
     * Time:  O(n)  — single pass with two pointers
     * Space: O(1)  — no extra data structures
     */
    public static int[] findPairWithTargetSum(int[] sortedNumbers, int targetSum) {
        int leftIndex = 0;                          // Start at the smallest element
        int rightIndex = sortedNumbers.length - 1;  // Start at the largest element

        while (leftIndex < rightIndex) {
            int currentSum = sortedNumbers[leftIndex] + sortedNumbers[rightIndex];

            if (currentSum == targetSum) {
                // Found it — return 1-based indices as the problem specifies
                return new int[]{leftIndex + 1, rightIndex + 1};
            }

            if (currentSum < targetSum) {
                // Sum is too small — we need a bigger left value, so move left pointer right
                leftIndex++;
            } else {
                // Sum is too large — we need a smaller right value, so move right pointer left
                rightIndex--;
            }
        }

        // Problem guarantees a solution exists, but good practice to handle this
        return new int[]{-1, -1};
    }

    public static void main(String[] args) {
        int[] prices = {2, 7, 11, 15};
        int budget = 9;

        int[] result = findPairWithTargetSum(prices, budget);
        System.out.println("Indices (1-based): [" + result[0] + ", " + result[1] + "]");
        System.out.println("Values: " + prices[result[0]-1] + " + " + prices[result[1]-1] + " = " + budget);

        // Second test case
        int[] scores = {1, 3, 4, 5, 7, 10, 11};
        int targetScore = 9;
        int[] scoreResult = findPairWithTargetSum(scores, targetScore);
        System.out.println("\nIndices (1-based): [" + scoreResult[0] + ", " + scoreResult[1] + "]");
        System.out.println("Values: " + scores[scoreResult[0]-1] + " + " + scores[scoreResult[1]-1] + " = " + targetScore);
    }
}
Output
Indices (1-based): [1, 2]
Values: 2 + 7 = 9
Indices (1-based): [3, 5]
Values: 4 + 5 = 9
Interview Gold:
When you see 'sorted array' and 'find a pair', your brain should immediately fire 'two pointers, opposite ends'. Saying that out loud before writing a single line of code tells the interviewer you recognise patterns — that's the real signal they're looking for.
Production Insight
The opposite-ends variant fails silently on unsorted arrays — it produces wrong results without an error.
In production, always validate that your input is sorted or sort it explicitly at the start.
Rule: if you rely on sortedness, check it or sort it — don't assume it's sorted.
Key Takeaway
Two pointers eliminate nested loops by exploiting order.
Each pointer move eliminates a class of possibilities, not just one pair.
Rule: sorted data → opposite ends; in-place → slow/fast; cycles → fast/slow.
Choose the Right Two-Pointer Variant
IfInput is sorted or sortable, looking for a pair with a condition
UseUse opposite-ends (converging) variant — O(n) after sort.
IfNeed to modify an array in-place (remove duplicates, filter values)
UseUse same-direction slow/fast variant — O(n) time, O(1) space.
IfDetect a cycle in a linked list or find the middle element
UseUse fast-slow (Floyd's) variant — two steps vs one step.
IfInput is unsorted and cannot be sorted
UseDo not use two pointers — switch to HashMap or other O(n) space approach.

Removing Duplicates In-Place — The Same-Direction Variant

Not every two-pointer problem has the pointers moving toward each other. The same-direction variant uses a slow pointer and a fast pointer both starting at the left — the fast pointer scouts ahead while the slow pointer marks where the next valid element should land.

This pattern is perfect for in-place array modification problems: removing duplicates, removing a specific value, or filtering elements without allocating a new array. The slow pointer essentially tracks 'the last confirmed valid position', and the fast pointer asks 'is this element worth keeping?'

Think of it like editing a manuscript. The fast-reading editor races through the draft, and every time they find a sentence worth keeping, they hand it to the slow typist who places it into the clean copy. The typist never skips ahead; the editor never looks back.

This approach runs in O(n) time and O(1) space — a big deal in interviews where the constraint 'do it in-place without extra memory' is explicitly stated. If you reach for a HashSet here, you've violated that constraint even though you'd get the right answer.

One subtlety: this variant requires that duplicates are adjacent, which is guaranteed when the array is sorted. For unsorted arrays, this slow/fast pattern won't remove all duplicates — only adjacent ones. If the input is unsorted, you either sort first (and the problem often allows it) or use a set.

RemoveDuplicatesInPlace.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class RemoveDuplicatesInPlace {

    /**
     * Remove duplicates from a sorted array IN-PLACE.
     * Returns the count of unique elements.
     * Elements beyond the returned count don't matter.
     *
     * Time:  O(n)  — single pass
     * Space: O(1)  — no extra arrays or sets
     *
     * Key constraint: the array is SORTED, so duplicates are always adjacent.
     * That's what makes the two-pointer approach valid here.
     */
    public static int removeDuplicates(int[] sortedArray) {
        if (sortedArray.length == 0) return 0;

        // slowWriter points to where the next unique element should be written
        int slowWriter = 0;

        // fastReader scouts ahead to find the next unique element
        for (int fastReader = 1; fastReader < sortedArray.length; fastReader++) {

            // If fastReader found a new unique value (different from what slowWriter last wrote)...
            if (sortedArray[fastReader] != sortedArray[slowWriter]) {
                slowWriter++;  // Advance slowWriter to the next available slot
                sortedArray[slowWriter] = sortedArray[fastReader];  // Write the unique value there
            }
            // If they're equal, fastReader keeps moving — we skip the duplicate
        }

        return slowWriter + 1;  // +1 because slowWriter is an index, not a count
    }

    public static void main(String[] args) {
        int[] temperatures = {1, 1, 2, 3, 3, 3, 4, 5, 5};

        System.out.println("Before: " + java.util.Arrays.toString(temperatures));

        int uniqueCount = removeDuplicates(temperatures);

        System.out.println("Unique count: " + uniqueCount);
        System.out.print("Unique elements: ");
        for (int i = 0; i < uniqueCount; i++) {
            System.out.print(temperatures[i] + " ");
        }
        System.out.println();

        // Edge case: all elements are the same
        int[] allSame = {7, 7, 7, 7};
        int allSameCount = removeDuplicates(allSame);
        System.out.println("\nAll-same array unique count: " + allSameCount);
        System.out.println("First unique element: " + allSame[0]);

        // Edge case: no duplicates at all
        int[] noDupes = {1, 2, 3, 4};
        int noDupesCount = removeDuplicates(noDupes);
        System.out.println("\nNo-duplicate array unique count: " + noDupesCount);
    }
}
Output
Before: [1, 1, 2, 3, 3, 3, 4, 5, 5]
Unique count: 5
Unique elements: 1 2 3 4 5
All-same array unique count: 1
First unique element: 7
No-duplicate array unique count: 4
Pro Tip:
The slow/fast naming is critical for clarity in interviews. If you name your variables 'i' and 'j' an interviewer has to mentally trace your code. If you name them 'slowWriter' and 'fastReader' (or 'writeAt' and 'scanner'), your intent is self-documenting and you spend less time explaining.
Production Insight
In-place modifications with slow/fast pointers can cause data corruption if you overwrite elements that fast hasn't read yet.
Always ensure that slowWriter never overtakes fastReader — fast must stay ahead or equal.
Rule: slow marks where to write, fast scouts ahead — never let slow pass fast.
Key Takeaway
Same-direction slow/fast pointers are ideal for in-place array filtering.
The array must be sorted (duplicates adjacent) for this to work correctly.
Rule: Name your pointers descriptively — it saves explanation time in interviews.

Detecting a Cycle — The Fast-Slow (Floyd's) Variant

The third variant is the most elegant: the fast pointer moves two steps for every one step the slow pointer takes. If there's a cycle in a linked list, the fast pointer will eventually lap the slow pointer and they'll meet inside the cycle. If there's no cycle, the fast pointer will fall off the end of the list.

Why does this work? Think of two runners on a circular track — one running twice as fast as the other. They'll always meet eventually, no matter how long the track is. If the track is a straight road with a dead end, the faster runner hits the wall first and you know it's not circular.

This is Floyd's Cycle Detection algorithm. It's O(n) time and O(1) space — compare that to the HashSet approach, which is also O(n) time but O(n) space. In an interview, the follow-up 'can you do it without extra memory?' is almost guaranteed, and this is your answer.

This pattern extends beyond linked lists. It appears in problems about finding the duplicate number in an array, detecting infinite loops in graph traversal, and even in cryptography.

One common variation: after detecting the cycle, you can find the exact node where the cycle begins. Reset the slow pointer to head, then move both pointers one step at a time. The node where they meet is the start of the cycle. The proof involves the distance from head to cycle start being equal to the distance from the meeting point around the cycle back to the start.

LinkedListCycleDetector.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
public class LinkedListCycleDetector {

    // Standard linked list node
    static class ListNode {
        int value;
        ListNode next;

        ListNode(int value) {
            this.value = value;
            this.next = null;
        }
    }

    /**
     * Detect whether a linked list contains a cycle.
     * Uses Floyd's fast-slow pointer algorithm.
     *
     * Time:  O(n) — in the worst case we traverse the list once
     * Space: O(1) — only two pointer variables, no extra storage
     */
    public static boolean hasCycle(ListNode head) {
        if (head == null || head.next == null) {
            return false;  // A list with 0 or 1 nodes can't have a cycle
        }

        ListNode slowRunner = head;       // Moves 1 step at a time
        ListNode fastRunner = head;       // Moves 2 steps at a time

        while (fastRunner != null && fastRunner.next != null) {
            slowRunner = slowRunner.next;        // 1 step
            fastRunner = fastRunner.next.next;   // 2 steps

            // If they ever point to the same node, a cycle exists
            if (slowRunner == fastRunner) {
                return true;  // They've met inside the cycle
            }
        }

        // fastRunner reached the end — no cycle
        return false;
    }

    /**
     * Bonus: find WHERE the cycle starts (common interview follow-up).
     * After detection, reset slow to head and move both one step at a time.
     * They meet at the cycle entry point.
     */
    public static ListNode findCycleStart(ListNode head) {
        ListNode slowRunner = head;
        ListNode fastRunner = head;

        // Phase 1: detect the cycle
        while (fastRunner != null && fastRunner.next != null) {
            slowRunner = slowRunner.next;
            fastRunner = fastRunner.next.next;
            if (slowRunner == fastRunner) {
                // Phase 2: find the entry point
                // Reset slow to head; fast stays at meeting point
                ListNode entryFinder = head;
                while (entryFinder != slowRunner) {
                    entryFinder = entryFinder.next;
                    slowRunner = slowRunner.next;
                }
                return entryFinder;  // This is the start of the cycle
            }
        }
        return null;  // No cycle
    }

    public static void main(String[] args) {
        // Build: 3 -> 2 -> 0 -> -4
        //                ^         |
        //                |_________| (cycle back to node with value 2)
        ListNode node1 = new ListNode(3);
        ListNode node2 = new ListNode(2);
        ListNode node3 = new ListNode(0);
        ListNode node4 = new ListNode(-4);

        node1.next = node2;
        node2.next = node3;
        node3.next = node4;
        node4.next = node2;  // Creates the cycle — tail points back to node2

        System.out.println("Has cycle: " + hasCycle(node1));  // true

        ListNode cycleStart = findCycleStart(node1);
        System.out.println("Cycle starts at node with value: " + cycleStart.value);  // 2

        // Now test a list with no cycle
        ListNode linearHead = new ListNode(1);
        linearHead.next = new ListNode(2);
        linearHead.next.next = new ListNode(3);

        System.out.println("\nLinear list has cycle: " + hasCycle(linearHead));  // false
        System.out.println("Cycle start (should be null): " + findCycleStart(linearHead));  // null
    }
}
Output
Has cycle: true
Cycle starts at node with value: 2
Linear list has cycle: false
Cycle start (should be null): null
Watch Out:
Always check fastRunner != null && fastRunner.next != null before advancing the fast pointer. Moving two steps means you access .next.next — if fastRunner.next is null, that second .next throws a NullPointerException. This is the single most common runtime error candidates hit on a whiteboard under pressure.
Production Insight
Floyd's algorithm works because the speed difference guarantees they'll meet if a cycle exists.
In production, always handle the case where the list is empty or has a single node — return false immediately.
Rule: after detection, finding the cycle start requires resetting one pointer to head and moving both at same speed.
Key Takeaway
Fast-slow (Floyd's) detects cycles in O(n) time and O(1) space.
The fast pointer moves two steps; slow moves one. They meet if a cycle exists.
Rule: Always guard fastRunner.next before advancing twice — null pointer is the top bug.

Container With Most Water — Real Interview Walkthrough

This classic problem is a perfect test of opposite-ends two-pointer thinking. You're given an array of non-negative integers where each value represents the height of a vertical line at that index. You need to find two lines that together with the x-axis form a container that holds the most water.

The brute force checks every pair — O(n²). The two-pointer solution: start left at index 0 and right at the last index. The area is limited by the shorter line: area = min(height[left], height[right]) * (right - left). Move the pointer with the shorter line inward. Why? Because the only way to possibly get a larger area is to replace the limiting side. If you move the taller line, the width shrinks and the height can never exceed the shorter line — area can only stay the same or decrease.

This is the exact same elimination logic as two-sum: each move discards all pairs that would be worse than the current best. The algorithm runs in O(n) time and O(1) space. Here's the implementation in Java.

ContainerWithMostWater.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class ContainerWithMostWater {

    /**
     * Find the maximum area of water a container can hold.
     * Given an array of heights, find two lines that form the maximum area.
     *
     * Time:  O(n) — single pass
     * Space: O(1)
     */
    public static int maxArea(int[] height) {
        int left = 0;
        int right = height.length - 1;
        int maxArea = 0;

        while (left < right) {
            int currentHeight = Math.min(height[left], height[right]);
            int width = right - left;
            int currentArea = currentHeight * width;
            maxArea = Math.max(maxArea, currentArea);

            // Move the pointer with the shorter line inward
            if (height[left] < height[right]) {
                left++;
            } else {
                right--;
            }
        }

        return maxArea;
    }

    public static void main(String[] args) {
        int[] heights = {1, 8, 6, 2, 5, 4, 8, 3, 7};
        int result = maxArea(heights);
        System.out.println("Max area: " + result);  // Expected 49 (between 8 and 7 at indices 1 and 8)

        // Edge case: two lines
        int[] twoLines = {1, 2};
        System.out.println("Two lines max area: " + maxArea(twoLines));  // 1

        // Edge case: all same height
        int[] sameHeight = {5, 5, 5, 5};
        System.out.println("Same height max area: " + maxArea(sameHeight));  // 15 (between first and last)
    }
}
Output
Max area: 49
Two lines max area: 1
Same height max area: 15
Interview Gold:
When explaining 'Container With Most Water', state the key observation out loud: 'The area is limited by the shorter line. Moving that line inward is the only way to possibly increase the area.' This shows you understand why the algorithm works, not just how to code it.
Production Insight
The two-pointer solution for Container With Most Water assumes heights are non-negative — that's given in the problem.
In production, if heights could be negative or zero, area calculations become invalid.
Rule: With opposite-ends, always move the pointer with the smaller height — that's the key correctness invariant.
Key Takeaway
Container With Most Water uses opposite-ends two pointers, moving the shorter line.
Each move discards all pairs that would be worse — O(n) time.
Rule: The area is bounded by the shorter line; only moving that line can improve it.

Three Sum Problem: Extending the Pattern to Multiple Pointers

The two-pointer pattern extends naturally to three-sum problems. The classic 'Given an array of integers, find all unique triplets that sum to zero' is a staple of coding interviews. The approach: sort the array, then fix one number at a time using a loop, and for the remaining range apply the two-sum two-pointer technique to find pairs that sum to -fixedNumber.

This gives O(n²) overall complexity — because the outer loop runs n times and the inner two-pointer walk is O(n). Without two pointers you'd be at O(n³).

The key challenge is handling duplicate values. After sorting, if you skip duplicate values for the fixed number, you avoid duplicate triplets. Similarly, inside the two-pointer walk, when you find a valid pair, move both pointers inward past any duplicate values before continuing.

This generalises to k-sum: fix k-2 numbers, then use two pointers on the remaining range. Complexity O(n^(k-1)).

ThreeSum.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import java.util.*;

public class ThreeSum {

    /**
     * Find all unique triplets that sum to zero.
     * Uses sorting + two-pointer for the inner pair.
     *
     * Time:  O(n²)
     * Space: O(1) extra (excluding output)
     */
    public static List<List<Integer>> findTriplets(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        Arrays.sort(nums);

        for (int i = 0; i < nums.length - 2; i++) {
            // Skip duplicate values for the fixed element
            if (i > 0 && nums[i] == nums[i - 1]) continue;

            int left = i + 1;
            int right = nums.length - 1;
            int target = -nums[i];

            while (left < right) {
                int sum = nums[left] + nums[right];
                if (sum == target) {
                    result.add(Arrays.asList(nums[i], nums[left], nums[right]));
                    // Skip duplicates for left and right
                    while (left < right && nums[left] == nums[left + 1]) left++;
                    while (left < right && nums[right] == nums[right - 1]) right--;
                    left++;
                    right--;
                } else if (sum < target) {
                    left++;
                } else {
                    right--;
                }
            }
        }
        return result;
    }

    public static void main(String[] args) {
        int[] nums = {-1, 0, 1, 2, -1, -4};
        List<List<Integer>> triplets = findTriplets(nums);
        System.out.println("Unique triplets: " + triplets);
        // Expected: [[-1, -1, 2], [-1, 0, 1]]

        // Edge case: all zeros
        int[] zeros = {0, 0, 0, 0};
        System.out.println("All zeros triplets: " + findTriplets(zeros));
        // Expected: [[0, 0, 0]]
    }
}
Output
Unique triplets: [[-1, -1, 2], [-1, 0, 1]]
All zeros triplets: [[0, 0, 0]]
Common Pitfall:
Forgetting to skip duplicate values in the outer loop and inner pointer movements will produce duplicate triplets in the result. Always skip duplicates to maintain correct output and avoid O(n^3) blow-up due to extra iterations.
Production Insight
Three-sum with two-pointer works well for moderate-sized arrays, but for huge arrays (10^5+) the O(n²) can still be slow.
Consider using a HashMap-based approach for better average case if duplicates are not an issue.
Rule: Sorting is required — without it, the two-pointer approach for three-sum fails.

String Palindromes: Two Pointers on Characters

Strings are arrays of characters, so two pointers work perfectly for palindrome checking. Start one pointer at index 0 and one at the last index. Compare characters, then move inward. If any pair doesn't match, it's not a palindrome.

This is O(n) time and O(1) space. A variant with character filtering (ignore non-alphanumeric, case-insensitive) is equally common — just advance pointers over invalid characters before comparing.

Two pointers also solve problems like 'reverse a string in-place' (swap characters from both ends) and 'valid palindrome II' (allow at most one deletion).

PalindromeChecker.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class PalindromeChecker {

    /**
     * Check if a string is a palindrome (alphanumeric only, case-insensitive).
     * Uses opposite-ends two pointers.
     *
     * Time:  O(n)
     * Space: O(1)
     */
    public static boolean isPalindrome(String s) {
        if (s == null) return false;
        int left = 0;
        int right = s.length() - 1;

        while (left < right) {
            // Skip non-alphanumeric characters from left
            while (left < right && !Character.isLetterOrDigit(s.charAt(left))) {
                left++;
            }
            // Skip non-alphanumeric characters from right
            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;
    }

    public static void main(String[] args) {
        System.out.println(isPalindrome("A man, a plan, a canal: Panama"));  // true
        System.out.println(isPalindrome("race a car"));  // false
        System.out.println(isPalindrome(" "));  // true
        System.out.println(isPalindrome("ab_a"));  // true
    }
}
Output
true
false
true
true
Interview Note:
The 'at most one deletion' palindrome variant is a common follow-up. When you find a mismatch, try skipping the left character OR the right character and check if the remaining substring is a palindrome. This is still O(n) because you only need to attempt deletion at the first mismatch.
Production Insight
Character filtering with while loops inside the main while loop looks fine but can cause infinite loops if you forget the outer while condition in the inner loops.
Always include 'left < right' in the inner while conditions to avoid index overflow.
Rule: For palindrome with filtering, use Character.isLetterOrDigit and toLower to normalize.
● Production incidentPOST-MORTEMseverity: high

Payment Reconciliation Silent Failure: Two-Pointer on Unsorted Data

Symptom
The reconciliation system reported fewer matching transaction pairs than the accounting team expected. No errors were thrown — just missing pairs.
Assumption
The team assumed the transaction IDs were sorted by timestamp, but the actual input order was insertion order, not sorted by ID value.
Root cause
The opposite-ends two-pointer implementation assumed input was sorted. On unsorted data, the algorithm doesn't converge correctly — it skips valid pairs because the comparison logic depends on sorted order to eliminate possibilities.
Fix
Add a sort by ID before the two-pointer walk. If sorting is not allowed (e.g., preserve original order for audit), switch to a HashMap-based approach.
Key lesson
  • Always verify input sortedness before using opposite-ends two pointers.
  • If the input is not sorted, either sort it first or use a different algorithm.
  • Silent failures from algorithm assumptions are the hardest to debug — they produce wrong results, not crashes.
Production debug guideSymptom → Action checklist for the three most common two-pointer bugs3 entries
Symptom · 01
Two-sum returns wrong pair or no pair, but you know a valid pair exists
Fix
Check that the input array is sorted. If not, either sort it first (O(n log n)) or switch to a HashMap (O(n) time, O(n) space).
Symptom · 02
Cycle detection throws NullPointerException
Fix
Inspect the while loop condition. You must check fastRunner != null && fastRunner.next != null before moving the fast pointer two steps.
Symptom · 03
In-place duplicate removal leaves extra elements beyond the unique count
Fix
Verify that the slow pointer only advances AFTER writing a new unique value. Common bug: increment slow before write, causing overlapping writes.
★ Two-Pointer Quick Debug Cheat SheetWhen your two-pointer code isn't working, run these checks in order.
Wrong answer or missing pair (opposite-ends)
Immediate action
Print the current sum and both pointer values at each iteration.
Commands
log.debug("left=%d, right=%d, sum=%d", left, right, sum);
Check if array is sorted by iterating and comparing adjacent elements.
Fix now
Sort the array before the while loop, or use a HashMap for unsorted input.
NullPointerException in fast-slow cycle detection+
Immediate action
Add null checks before fastRunner.next.next.
Commands
while (fastRunner != null && fastRunner.next != null) {
Print the values of slowRunner and fastRunner at the top of the loop.
Fix now
Ensure the while condition also checks fastRunner.next != null.
Infinite loop (pointers never meet or exit)+
Immediate action
Verify that at least one pointer moves in every iteration.
Commands
Add a maximum iterations guard: int iterations = 0; while(... && iterations++ < array.length + 1)
Check for off-by-one errors: left < right vs left <= right.
Fix now
Ensure you have an else branch that advances the appropriate pointer on non-match.
Two-Pointer Variants at a Glance
AspectOpposite-Ends VariantSame-Direction (Slow/Fast) VariantFast-Slow (Floyd's) Variant
Pointer starting positionsOne at index 0, one at index n-1Both start at index 0 (or head)Both start at head
Pointer movement directionConverging toward the middleBoth move right; fast moves fasterFast moves 2 steps, slow moves 1 step
Requires sorted input?Yes — sorting gives the logical ordering neededYes (for duplicate adjacency) — but works on sorted or unsorted for other filtersNo — works on any linked list
Primary use casesTwo-sum, three-sum, container with most water, palindromeRemove duplicates, remove value, filter in-placeCycle detection, find middle, duplicate number in array
Time complexityO(n) after sorting — O(n log n) overall if you sortO(n) — single pass regardlessO(n) — linear traversal
Space complexityO(1) extraO(1) extraO(1) extra
Typical interview trigger'Find a pair that satisfies condition X in sorted array''Do it in-place', 'remove duplicates', 'modify without extra memory''Detect a cycle', 'find middle of linked list', 'find duplicate number'
Data structuresArrays (sorted) and stringsArrays (usually sorted) and linked listsLinked lists and arrays (for duplicate detection)

Key takeaways

1
Two pointers reduce O(n²) nested-loop solutions to O(n) by eliminating impossible pairs at each step rather than checking them
the 'eliminating a class of possibilities' insight is the whole pattern.
2
Pick your variant before you code
opposite-ends for 'find a pair/triplet in a sorted array', same-direction slow/fast for 'modify in-place or remove without extra memory', fast-slow at different speeds for 'detect a cycle or find the middle'.
3
The fast pointer null check (fastRunner != null && fastRunner.next != null) is not boilerplate
it's load-bearing logic that prevents NullPointerExceptions on every cycle-detection problem.
4
Naming your pointers descriptively ('slowRunner', 'fastReader', 'leftBound', 'rightBound') signals seniority in an interview and eliminates the need to explain your variable semantics out loud.
5
Two pointers generalises to k-sum
fix k-2 elements, then use two pointers on the rest — O(n^(k-1)).
6
Always check that the input is sorted for opposite-ends variants; unsorted data silently produces wrong results.

Common mistakes to avoid

4 patterns
×

Applying opposite-ends two pointers to unsorted array

Symptom
The algorithm returns the wrong pair or no pair even though a valid pair exists. No error is thrown, so the bug is silent.
Fix
Either sort the array first (O(n log n)) or switch to a HashMap-based approach (O(n) time, O(n) space) if sorting is not allowed.
×

Not guarding the fast pointer's second step in cycle detection

Symptom
NullPointerException when the linked list has odd length or no cycle, because fastRunner.next.next is called when fastRunner.next is null.
Fix
Always write the while loop condition as while (fastRunner != null && fastRunner.next != null) before accessing fastRunner.next.next.
×

Moving both pointers when only one should move (two-sum variant)

Symptom
Valid pairs are skipped — the algorithm returns no result or an incomplete result because incrementing both pointers on a non-match can skip the correct pair entirely.
Fix
Only move the pointer whose side caused the imbalance: if sum too small, move left pointer right; if sum too large, move right pointer left. Never move both in the same iteration.
×

Forgetting to skip duplicate values in three-sum

Symptom
The result contains duplicate triplets, violating the problem constraint 'unique triplets'.
Fix
After sorting, skip duplicate values in the outer loop (if i>0 && nums[i]==nums[i-1] continue). Inside the inner loop, after finding a valid triplet, skip duplicates for left and right pointers before advancing.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Given an array of integers sorted in ascending order, write a function t...
Q02SENIOR
You have a linked list and you suspect it might contain a cycle. How wou...
Q03SENIOR
I've given you a solution using a HashSet to remove duplicates from a so...
Q04SENIOR
Given an array of integers, find the container that holds the most water...
Q05JUNIOR
Check if a string is a palindrome considering only alphanumeric characte...
Q01 of 05SENIOR

Given an array of integers sorted in ascending order, write a function that finds all unique triplets that sum to zero. Walk me through your approach and complexity before you code.

ANSWER
I'll use the three-pointer approach: sort the array (already sorted here), then fix one element at a time using a loop, and for the remaining range I'll use two-pointers to find pairs that sum to -fixed. The outer loop runs O(n), the inner two-pointer is O(n), so total O(n^2). Space is O(1) extra. I need to handle duplicates by skipping same values in the outer loop and after finding a triplet in the inner loop. Let me code it. ``java public List<List<Integer>> threeSum(int[] nums) { List<List<Integer>> result = new ArrayList<>(); Arrays.sort(nums); for (int i = 0; i < nums.length - 2; i++) { if (i > 0 && nums[i] == nums[i-1]) continue; int left = i+1, right = nums.length-1; int target = -nums[i]; while (left < right) { int sum = nums[left] + nums[right]; if (sum == target) { result.add(Arrays.asList(nums[i], nums[left], nums[right])); while (left < right && nums[left] == nums[left+1]) left++; while (left < right && nums[right] == nums[right-1]) right--; left++; right--; } else if (sum < target) left++; else right--; } } return result; } ``
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
When should I use two pointers instead of a HashMap in an interview?
02
Can the two pointer technique work on strings?
03
What's the difference between two pointers and the sliding window technique?
04
Is two-pointer always O(n) space?
05
Can I use two pointers on an unsorted array if I don't need sorting?
🔥

That's Coding Patterns. Mark it forged?

6 min read · try the examples if you haven't

Previous
Sliding Window Interview Problems
8 / 17 · Coding Patterns
Next
Bit Manipulation Interview Problems