You've written a two pointers solution. The logic seems sound. You run it on the example test case—it works. You submit.
Wrong Answer on test case 23/47.
You stare at your code. Everything looks right. You add a print statement, run it again, and get a wall of output you can't make sense of. 30 minutes pass. You're stuck.
Debugging two pointers solutions is different from debugging other algorithms. The bug is often in the pointer movement logic—when to move left, when to move right, when to stop. These bugs are subtle and hard to spot by just reading code.
This guide will teach you practical debugging techniques for two pointers problems, including visualization strategies, systematic tracing, and how to use tools like LeetCopilot to identify bugs faster.
TL;DR
- Print pointer state at each iteration:
left,right, current values, window state - Visualize pointer movement on paper or with diagrams
- Check invariants after each pointer movement
- Test edge cases systematically: empty, single element, two elements, all same
- Use execution traces to see step-by-step pointer movement
- Common bug patterns: wrong loop condition, incorrect pointer update, missing edge case handling
The Two Pointers Debugging Framework
When your solution fails, follow this systematic approach:
Step 1: Understand What Failed
Step 2: Add Strategic Debug Prints
Step 3: Visualize Pointer Movement
Step 4: Check Your Invariants
Step 5: Test Edge Cases Systematically
Let's explore each step.
Step 1: Understand What Failed
Before you start debugging, understand what failed and why.
Read the Failed Test Case
LeetCode shows you the input that failed. Write it down.
Example:
Input: nums = [1, 3, 5, 7, 9], target = 10
Expected: [1, 3]
Your output: []Ask Yourself:
- Is this an edge case? (empty, single element, etc.)
- Is this a normal case that should work?
- What makes this input different from the examples?
Reproduce Locally
Copy the failed test case and run it in your local environment:
nums = [1, 3, 5, 7, 9]
target = 10
result = twoSum(nums, target)
print(f"Expected: [1, 3], Got: {result}")Step 2: Add Strategic Debug Prints
Don't just print everything—print strategically.
The Essential Debug Print
Add this at the start of your main loop:
def twoSum(nums, target):
left, right = 0, len(nums) - 1
while left < right:
# DEBUG: Print state at each iteration
print(f"Iteration: left={left}, right={right}")
print(f" Values: nums[{left}]={nums[left]}, nums[{right}]={nums[right]}")
print(f" Sum: {nums[left] + nums[right]}, Target: {target}")
print()
current_sum = nums[left] + nums[right]
if current_sum == target:
return [left, right]
elif current_sum < target:
print(f" -> Sum too small, moving left from {left} to {left+1}")
left += 1
else:
print(f" -> Sum too large, moving right from {right} to {right-1}")
right -= 1
print("Loop ended, no solution found")
return []Sample Output
Iteration: left=0, right=4
Values: nums[0]=1, nums[4]=9
Sum: 10, Target: 10
-> Found! Returning [0, 4]What to Print
Always print:
- Pointer positions:
left,right - Pointer values:
nums[left],nums[right] - Current state: sum, count, or whatever you're tracking
- Decision made: which pointer moved and why
For sliding window, also print:
5. Window contents: nums[left:right+1]
6. Window size: right - left + 1
Step 3: Visualize Pointer Movement
Seeing the pointers move visually helps you spot patterns.
Paper Visualization
Draw the array and trace pointer movement by hand:
Input: nums = [2, 7, 11, 15], target = 9
Step 0:
[2, 7, 11, 15]
^ ^
L R
Sum = 2 + 15 = 17 (too large, move R left)
Step 1:
[2, 7, 11, 15]
^ ^
L R
Sum = 2 + 11 = 13 (too large, move R left)
Step 2:
[2, 7, 11, 15]
^ ^
L R
Sum = 2 + 7 = 9 (found!)ASCII Art in Code Comments
Add this to your code for future reference:
# Visual trace for nums = [2, 7, 11, 15], target = 9:
#
# [2, 7, 11, 15]
# ^ ^ sum=17, move R
# ^ ^ sum=13, move R
# ^ ^ sum=9, found!Table Format
Create a table showing each iteration:
| Iteration | left | right | nums[left] | nums[right] | sum | Action |
|---|---|---|---|---|---|---|
| 0 | 0 | 3 | 2 | 15 | 17 | R-- |
| 1 | 0 | 2 | 2 | 11 | 13 | R-- |
| 2 | 0 | 1 | 2 | 7 | 9 | Found! |
Step 4: Check Your Invariants
An invariant is a condition that should always be true. When it's violated, you've found your bug.
Common Two Pointers Invariants
Opposite Direction:
left < right(pointers haven't crossed)0 <= left < len(nums)(left in bounds)0 <= right < len(nums)(right in bounds)left <= right(left never passes right)
Sliding Window:
0 <= left <= right < len(nums)(valid window)- Window state matches actual elements (e.g., sum equals sum of window)
Add Invariant Checks
def twoSum(nums, target):
left, right = 0, len(nums) - 1
while left < right:
# Check invariants
assert 0 <= left < len(nums), f"left out of bounds: {left}"
assert 0 <= right < len(nums), f"right out of bounds: {right}"
assert left < right, f"pointers crossed: left={left}, right={right}"
# ... rest of logicIf an assertion fails, you've found exactly where the bug occurs.
Step 5: Test Edge Cases Systematically
Most two pointers bugs only appear on edge cases. Test these before submitting:
The Essential Edge Cases
# Test 1: Empty array
assert twoSum([], 5) == []
# Test 2: Single element
assert twoSum([5], 5) == []
# Test 3: Two elements (valid)
assert twoSum([2, 7], 9) == [0, 1]
# Test 4: Two elements (invalid)
assert twoSum([2, 3], 10) == []
# Test 5: All same values
assert twoSum([3, 3, 3, 3], 6) == [0, 1] # or [0, 3], etc.
# Test 6: Answer at boundaries
assert twoSum([1, 2, 3, 4], 5) == [0, 3] # first + last
# Test 7: Large array
assert twoSum(list(range(1000)), 1997) == [998, 999]Create a Test Suite
def test_twoSum():
test_cases = [
([], 5, []),
([5], 5, []),
([2, 7], 9, [0, 1]),
([2, 3], 10, []),
([3, 3, 3, 3], 6, [0, 1]),
([1, 2, 3, 4], 5, [0, 3]),
([2, 7, 11, 15], 9, [0, 1]),
]
for i, (nums, target, expected) in enumerate(test_cases):
result = twoSum(nums, target)
assert result == expected, f"Test {i} failed: {nums}, {target} -> {result} (expected {expected})"
print("All tests passed!")
test_twoSum()Common Bug Patterns and How to Spot Them
Bug Pattern 1: Wrong Loop Condition
Symptom: Solution fails on two-element arrays or processes the same element twice.
Debug:
# Add this check
while left < right: # Should be <, not <=
print(f"Processing: left={left}, right={right}")
if left == right:
print("ERROR: Processing same element twice!")Fix: Use left < right for pair problems, not left <= right.
Bug Pattern 2: Incorrect Pointer Update
Symptom: Infinite loop or pointers don't move as expected.
Debug:
while left < right:
old_left, old_right = left, right
# ... your logic ...
# Check if pointers moved
if left == old_left and right == old_right:
print("ERROR: Pointers didn't move! Infinite loop incoming.")
breakFix: Ensure at least one pointer moves in every iteration.
Bug Pattern 3: Off-by-One in Window Size
Symptom: Window size is always 1 less than expected.
Debug:
window_size = right - left + 1 # Should have +1
print(f"Window: [{left}, {right}], Size: {window_size}")
print(f"Actual elements: {nums[left:right+1]}")
assert len(nums[left:right+1]) == window_sizeFix: Use right - left + 1 for inclusive endpoints.
Bug Pattern 4: Missing Edge Case Handling
Symptom: Runtime error or wrong answer on empty/single-element arrays.
Debug:
def twoSum(nums, target):
print(f"Input: nums={nums}, len={len(nums)}, target={target}")
if len(nums) < 2:
print("Edge case: array too small")
return []
# ... rest of logicFix: Add early returns for edge cases.
Bug Pattern 5: Incorrect Pointer Initialization
Symptom: Index out of bounds error or missing first/last element.
Debug:
left, right = 0, len(nums) - 1
print(f"Initial: left={left}, right={right}")
print(f"Valid indices: 0 to {len(nums)-1}")
assert 0 <= left < len(nums)
assert 0 <= right < len(nums)Fix: Use len(nums) - 1, not len(nums).
Using LeetCopilot for Debugging
LeetCopilot provides tools specifically designed for debugging two pointers:
1. Execution Trace
See step-by-step pointer movement with visual highlights:
Ask: "Show me the execution trace for this input"
LeetCopilot will display:
Step 1: left=0, right=4, sum=10
[1, 3, 5, 7, 9]
^ ^
Step 2: left=0, right=3, sum=8
[1, 3, 5, 7, 9]
^ ^2. Smart Context
Get hints about what's wrong without spoiling the solution:
Ask: "My solution fails on [1,3,5,7,9], target=10. What should I check?"
LeetCopilot: "Your pointers are moving correctly, but check your loop
condition. Are you stopping too early?"3. Edge Case Generation
Generate edge cases you might have missed:
Ask: "Generate edge cases for two sum with sorted array"
LeetCopilot provides:
- Empty array: []
- Single element: [5]
- No solution: [1, 2, 3], target=10
- Multiple solutions: [1, 2, 2, 3], target=4Debugging Checklist
When stuck, go through this checklist:
✅ Pointer Initialization
- Is
left = 0? - Is
right = len(nums) - 1(notlen(nums))? - Did I handle empty arrays?
✅ Loop Condition
- Am I using
left < rightfor pairs? - Does the loop terminate on edge cases?
✅ Pointer Movement
- Do pointers move in every iteration?
- Am I moving the correct pointer based on the condition?
- Can pointers get stuck in an infinite loop?
✅ Window/State Calculation
- Is window size
right - left + 1? - Am I updating state correctly when pointers move?
✅ Edge Cases
- Empty array:
[] - Single element:
[x] - Two elements:
[x, y] - All same:
[x, x, x] - Answer at boundaries
✅ Invariants
- Are pointers always in bounds?
- Do pointers never cross incorrectly?
- Does window state match actual elements?
Example: Debugging a Broken Solution
Let's debug a broken Valid Palindrome solution:
# BROKEN CODE
def isPalindrome(s: str) -> bool:
s = ''.join(c.lower() for c in s if c.isalnum())
left, right = 0, len(s) # BUG: should be len(s) - 1
while left <= right: # BUG: should be left < right
if s[left] != s[right]:
return False
left += 1
right -= 1
return TrueStep 1: Add Debug Prints
def isPalindrome(s: str) -> bool:
s = ''.join(c.lower() for c in s if c.isalnum())
print(f"Cleaned string: '{s}', length: {len(s)}")
left, right = 0, len(s)
print(f"Initial: left={left}, right={right}")
while left <= right:
print(f"Comparing: s[{left}]='{s[left]}' vs s[{right}]='{s[right]}'")
if s[left] != s[right]:
return False
left += 1
right -= 1
return True
# Test
isPalindrome("racecar")Step 2: Run and Observe
Cleaned string: 'racecar', length: 7
Initial: left=0, right=7
Comparing: s[0]='r' vs s[7]=ERROR: IndexErrorFound bug #1: right = len(s) is out of bounds!
Step 3: Fix and Test Again
left, right = 0, len(s) - 1 # FIXED
print(f"Initial: left={left}, right={right}")
while left <= right:
print(f"Comparing: s[{left}]='{s[left]}' vs s[{right}]='{s[right]}'")
# ...Initial: left=0, right=6
Comparing: s[0]='r' vs s[6]='r' ✓
Comparing: s[1]='a' vs s[5]='a' ✓
Comparing: s[2]='c' vs s[4]='c' ✓
Comparing: s[3]='e' vs s[3]='e' ✓ (same element!)Found bug #2: We're comparing the middle element to itself!
Step 4: Final Fix
while left < right: # FIXED: < instead of <=Now it works correctly!
Practice Strategy
To improve your debugging skills:
- Deliberately break working code and practice finding the bug
- Solve problems without running code first, then debug when it fails
- Keep a bug journal of common mistakes you make
- Use LeetCopilot's execution trace to visualize pointer movement
- Practice on these problems:
- Valid Palindrome (#125) - simple opposite direction
- Two Sum II (#167) - sorted array two pointers
- Container With Most Water (#11) - greedy pointer movement
FAQ
Q: How do I know which pointer to move?
A: Add a print statement showing the decision logic: "Sum too small → move left" or "Sum too large → move right". If the decision doesn't make sense, that's your bug.
Q: My solution works on examples but fails on hidden test cases. How do I debug?
A: Generate edge cases systematically (empty, single element, two elements, all same, boundaries). One of these will likely reproduce the bug.
Q: Should I use a debugger or print statements?
A: For two pointers, print statements are often better because you can see the entire trace of pointer movement. Debuggers are great for complex state, but two pointers is usually simple enough for prints.
Q: How do I visualize pointer movement without drawing?
A: Use LeetCopilot's execution trace feature, or create a simple visualization function that prints the array with pointers marked.
Conclusion
Debugging two pointers solutions requires a systematic approach:
- Understand what failed - read the test case carefully
- Add strategic debug prints - pointer positions, values, decisions
- Visualize pointer movement - on paper or with diagrams
- Check invariants - ensure pointers stay in bounds and don't cross incorrectly
- Test edge cases - empty, single element, two elements, boundaries
Key debugging tools:
- Print statements showing pointer state
- Visual diagrams of pointer movement
- Invariant assertions
- Systematic edge case testing
- Execution traces (LeetCopilot)
Master these techniques, and you'll debug two pointers problems in minutes instead of hours. For more on two pointers patterns, see the complete guide and off-by-one errors.
Next time you're stuck, don't stare at the code—trace the pointers. The bug will reveal itself.
Want to Practice LeetCode Smarter?
LeetCopilot is a free browser extension that enhances your LeetCode practice with AI-powered hints, personalized study notes, and realistic mock interviews — all designed to accelerate your coding interview preparation.
Also compatible with Edge, Brave, and Opera
