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
publicclassTwoSumSorted {
/**
* 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
*/
publicstaticint[] findPairWithTargetSum(int[] sortedNumbers, int targetSum) {
int leftIndex = 0; // Start at the smallest element
int rightIndex = sortedNumbers.length - 1; // Start at the largest elementwhile (leftIndex < rightIndex) {
int currentSum = sortedNumbers[leftIndex] + sortedNumbers[rightIndex];
if (currentSum == targetSum) {
// Found it — return 1-based indices as the problem specifiesreturnnewint[]{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 thisreturnnewint[]{-1, -1};
}
publicstaticvoidmain(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 caseint[] 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.
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
publicclassRemoveDuplicatesInPlace {
/**
* 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.
*/
publicstaticintremoveDuplicates(int[] sortedArray) {
if (sortedArray.length == 0) return0;
// slowWriter points to where the next unique element should be writtenint slowWriter = 0;
// fastReader scouts ahead to find the next unique elementfor (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
}
publicstaticvoidmain(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 sameint[] 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 allint[] 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
publicclassLinkedListCycleDetector {
// Standard linked list nodestaticclassListNode {
int value;
ListNode next;
ListNode(int value) {
this.value = value;
this.next = null;
}
}
/**
* Detect whether a linked list contains a cycle.
* UsesFloyd'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
*/
publicstaticbooleanhasCycle(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 timeListNode fastRunner = head; // Moves 2 steps at a timewhile (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 existsif (slowRunner == fastRunner) {
return true; // They've met inside the cycle
}
}
// fastRunner reached the end — no cyclereturnfalse;
}
/**
* 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.
*/
publicstaticListNodefindCycleStart(ListNode head) {
ListNode slowRunner = head;
ListNode fastRunner = head;
// Phase 1: detect the cyclewhile (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 pointListNode entryFinder = head;
while (entryFinder != slowRunner) {
entryFinder = entryFinder.next;
slowRunner = slowRunner.next;
}
return entryFinder; // This is the start of the cycle
}
}
return null; // No cycle
}
publicstaticvoidmain(String[] args) {
// Build: 3 -> 2 -> 0 -> -4// ^ |// |_________| (cycle back to node with value 2)ListNode node1 = newListNode(3);
ListNode node2 = newListNode(2);
ListNode node3 = newListNode(0);
ListNode node4 = newListNode(-4);
node1.next = node2;
node2.next = node3;
node3.next = node4;
node4.next = node2; // Creates the cycle — tail points back to node2System.out.println("Has cycle: " + hasCycle(node1)); // trueListNode cycleStart = findCycleStart(node1);
System.out.println("Cycle starts at node with value: " + cycleStart.value); // 2// Now test a list with no cycleListNode linearHead = newListNode(1);
linearHead.next = newListNode(2);
linearHead.next.next = newListNode(3);
System.out.println("\nLinear list has cycle: " + hasCycle(linearHead)); // falseSystem.out.println("Cyclestart (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
publicclassContainerWithMostWater {
/**
* 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)
*/
publicstaticintmaxArea(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 inwardif (height[left] < height[right]) {
left++;
} else {
right--;
}
}
return maxArea;
}
publicstaticvoidmain(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 linesint[] twoLines = {1, 2};
System.out.println("Two lines max area: " + maxArea(twoLines)); // 1// Edge case: all same heightint[] 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.*;
publicclassThreeSum {
/**
* 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)
*/
publicstaticList<List<Integer>> findTriplets(int[] nums) {
List<List<Integer>> result = newArrayList<>();
Arrays.sort(nums);
for (int i = 0; i < nums.length - 2; i++) {
// Skip duplicate values for the fixed elementif (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 rightwhile (left < right && nums[left] == nums[left + 1]) left++;
while (left < right && nums[right] == nums[right - 1]) right--;
left++;
right--;
} elseif (sum < target) {
left++;
} else {
right--;
}
}
}
return result;
}
publicstaticvoidmain(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 zerosint[] 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
publicclassPalindromeChecker {
/**
* Checkif a string is a palindrome (alphanumeric only, case-insensitive).
* Uses opposite-ends two pointers.
*
* Time: O(n)
* Space: O(1)
*/
publicstaticbooleanisPalindrome(String s) {
if (s == null) returnfalse;
int left = 0;
int right = s.length() - 1;
while (left < right) {
// Skip non-alphanumeric characters from leftwhile (left < right && !Character.isLetterOrDigit(s.charAt(left))) {
left++;
}
// Skip non-alphanumeric characters from rightwhile (left < right && !Character.isLetterOrDigit(s.charAt(right))) {
right--;
}
if (Character.toLowerCase(s.charAt(left)) != Character.toLowerCase(s.charAt(right))) {
returnfalse;
}
left++;
right--;
}
returntrue;
}
publicstaticvoidmain(String[] args) {
System.out.println(isPalindrome("A man, a plan, a canal: Panama")); // trueSystem.out.println(isPalindrome("race a car")); // falseSystem.out.println(isPalindrome(" ")); // trueSystem.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.
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
Aspect
Opposite-Ends Variant
Same-Direction (Slow/Fast) Variant
Fast-Slow (Floyd's) Variant
Pointer starting positions
One at index 0, one at index n-1
Both start at index 0 (or head)
Both start at head
Pointer movement direction
Converging toward the middle
Both move right; fast moves faster
Fast moves 2 steps, slow moves 1 step
Requires sorted input?
Yes — sorting gives the logical ordering needed
Yes (for duplicate adjacency) — but works on sorted or unsorted for other filters
No — works on any linked list
Primary use cases
Two-sum, three-sum, container with most water, palindrome
Remove duplicates, remove value, filter in-place
Cycle detection, find middle, duplicate number in array
Time complexity
O(n) after sorting — O(n log n) overall if you sort
O(n) — single pass regardless
O(n) — linear traversal
Space complexity
O(1) extra
O(1) extra
O(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 structures
Arrays (sorted) and strings
Arrays (usually sorted) and linked lists
Linked 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;
}
``
Q02 of 05SENIOR
You have a linked list and you suspect it might contain a cycle. How would you detect it in O(1) space? What would change in your approach if I asked you to also return the node where the cycle begins?
ANSWER
I'll use Floyd's cycle detection algorithm (fast-slow pointers). Start both at head. Move slow one step, fast two steps. If they meet, there's a cycle. To find the start, after they meet, reset slow to head and move both one step at a time. The point where they meet again is the start of the cycle. The proof relies on the distance from head to cycle start being the same as the distance from meeting point to cycle start when moving one step. Space is O(1).
Q03 of 05SENIOR
I've given you a solution using a HashSet to remove duplicates from a sorted array. It passes all test cases. Why might an interviewer still reject it, and how would you rewrite it to address that concern?
ANSWER
The HashSet solution uses O(n) extra space. In coding interviews, many problems explicitly ask for O(1) space. For a sorted array, we can use the two-pointer slow/fast approach in-place. The slow pointer marks where to write the next unique element, and the fast pointer scouts for new values. Since the array is sorted, duplicates are adjacent. We do it in O(n) time and O(1) space. The interviewer wants to see that you can optimise space when possible.
Q04 of 05SENIOR
Given an array of integers, find the container that holds the most water. Explain the two-pointer approach and its correctness.
ANSWER
The brute force is O(n^2). Two-pointer: start left at 0, right at last index. The area is min(height[left], height[right]) * (right-left). Move the pointer with the shorter line inward. Why? Because the area is limited by the shorter line. If we move the taller line, the width decreases and the height can never exceed the shorter line, so area can only decrease. The only way to possibly get a larger area is to try a different shorter line. This eliminates n-2 possibilities per step, giving O(n).
Q05 of 05JUNIOR
Check if a string is a palindrome considering only alphanumeric characters and ignoring case. Can you implement it with two pointers?
ANSWER
Yes. I'll use two pointers: left at 0, right at length-1. While left < right: while left < right and char at left is not alphanumeric, increment left. Similarly for right. Then compare lowercased characters. If they don't match, return false. Move both inward. If loop finishes, return true. O(n) time, O(1) space.
``java
public boolean isPalindrome(String s) {
int left = 0, 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--;
if (Character.toLowerCase(s.charAt(left)) != Character.toLowerCase(s.charAt(right))) return false;
left++; right--;
}
return true;
}
``
01
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.
SENIOR
02
You have a linked list and you suspect it might contain a cycle. How would you detect it in O(1) space? What would change in your approach if I asked you to also return the node where the cycle begins?
SENIOR
03
I've given you a solution using a HashSet to remove duplicates from a sorted array. It passes all test cases. Why might an interviewer still reject it, and how would you rewrite it to address that concern?
SENIOR
04
Given an array of integers, find the container that holds the most water. Explain the two-pointer approach and its correctness.
SENIOR
05
Check if a string is a palindrome considering only alphanumeric characters and ignoring case. Can you implement it with two pointers?
JUNIOR
FAQ · 5 QUESTIONS
Frequently Asked Questions
01
When should I use two pointers instead of a HashMap in an interview?
Use two pointers when the input is sorted and you need O(1) space — the sorted order gives you the directional logic to converge efficiently. Use a HashMap when the input is unsorted and you can't or shouldn't sort it, and when O(n) extra space is acceptable. If an interviewer explicitly says 'constant space', two pointers is almost always the answer.
Was this helpful?
02
Can the two pointer technique work on strings?
Absolutely. Strings are just arrays of characters, so opposite-ends two pointers are the standard approach for palindrome checking — start one pointer at index 0 and one at the last index, compare characters, and move inward. Problems like 'reverse a string in-place' and 'valid palindrome with character filtering' both use this variant.
Was this helpful?
03
What's the difference between two pointers and the sliding window technique?
Both use two boundary markers on an array, but their intent differs. Two pointers typically looks for a specific pair or relationship between two elements, and the pointers move based on a comparison result. Sliding window maintains a contiguous subarray of variable or fixed size and expands or contracts it to satisfy a condition — think 'longest substring without repeating characters'. If you're comparing elements at two specific positions, think two pointers. If you care about everything between two positions as a group, think sliding window.
Was this helpful?
04
Is two-pointer always O(n) space?
Yes, the two-pointer technique itself uses O(1) extra space — just a few integer variables for indices. However, if you need to sort the input first, that may require O(n) space depending on the sorting algorithm (e.g., Arrays.sort in Java uses quicksort which is O(log n) space). The pointer walk itself is always O(1).
Was this helpful?
05
Can I use two pointers on an unsorted array if I don't need sorting?
The opposite-ends variant requires sorted order to work correctly. However, the same-direction slow/fast variant can work on unsorted arrays for tasks like removing a specific value (not duplicates) — the fast pointer scouting works regardless of order. The fast-slow cycle detection works on any linked list, sorted or not.