Your two pointers solution works perfectly on the example test cases. You submit with confidence.
Runtime Error. Or worse: Wrong Answer on test case 47/52.
You debug for 20 minutes. The issue? Your left pointer started at 1 instead of 0. Or you calculated window size as right - left instead of right - left + 1. Or your loop condition was left <= right when it should have been left < right.
Off-by-one errors are the silent killers of two pointers solutions. They're subtle, hard to spot, and often only appear on edge cases like empty arrays, single elements, or when pointers meet.
This guide will teach you a systematic approach to avoid off-by-one errors in two pointers problems, covering initialization, loop bounds, window size calculation, and edge case handling.
TL;DR
- Always initialize pointers consistently:
left = 0,right = n - 1for opposite direction - Use
left < rightfor opposite direction (not<=unless you want to process the same element) - Window size is
right - left + 1when both pointers are inclusive - Test edge cases: empty array, single element, two elements, all same values
- Document your invariants: write comments about what
leftandrightrepresent
The Three Sources of Off-by-One Errors
1. Incorrect Pointer Initialization
2. Wrong Loop Condition
3. Incorrect Window Size Calculation
Let's tackle each one systematically.
Source 1: Incorrect Pointer Initialization
The Standard Patterns
Opposite Direction (Converging Pointers):
left = 0 # First element
right = len(nums) - 1 # Last elementSame Direction (Fast/Slow or Read/Write):
slow = 0
fast = 0 # or fast = 1, depending on problemFixed-Size Window:
left = 0
right = k - 1 # Window of size kCommon Mistakes
Mistake 1: Starting right at len(nums)
# WRONG
right = len(nums) # This is out of bounds!
# CORRECT
right = len(nums) - 1Why it's wrong: Arrays are 0-indexed. If nums = [1, 2, 3], valid indices are 0, 1, 2. Index 3 is out of bounds.
Mistake 2: Starting left at 1
# WRONG (unless you have a specific reason)
left = 1
# CORRECT
left = 0Why it's wrong: You skip the first element, missing potential valid answers.
Edge Case: Empty Array
Always check for empty arrays before initializing pointers:
def twoSum(nums, target):
if not nums: # or: if len(nums) == 0
return []
left, right = 0, len(nums) - 1
# ... rest of logicWithout this check, right = len(nums) - 1 becomes -1 for an empty array, causing unexpected behavior.
Source 2: Wrong Loop Condition
The loop condition determines when your pointers stop. Getting this wrong causes you to either skip valid pairs or process the same element twice.
Opposite Direction: `left < right` vs `left <= right`
Use left < right when you're looking for pairs (two different elements):
# Finding pairs (e.g., Two Sum, Valid Palindrome)
while left < right:
# Process nums[left] and nums[right]
# These are always different elementsUse left <= right when you're processing individual elements and it's okay to process the middle element:
# Binary search (processing individual elements)
while left <= right:
mid = (left + right) // 2
# Process nums[mid]Why the Difference?
Example: nums = [1, 2, 3], looking for a pair that sums to 5.
With left < right:
- Iteration 1:
left=0, right=2→ sum = 1+3 = 4 - Iteration 2:
left=1, right=2→ sum = 2+3 = 5 ✓ - Loop ends when
left=2, right=2(not executed)
With left <= right (WRONG for pairs):
- Iteration 1:
left=0, right=2→ sum = 1+3 = 4 - Iteration 2:
left=1, right=2→ sum = 2+3 = 5 ✓ - Iteration 3:
left=2, right=2→ sum = 3+3 = 6 (using same element twice!)
Same Direction: When to Stop
For same-direction pointers, the fast pointer usually determines the loop:
# Fast/Slow pattern
while fast < len(nums):
# Process
fast += 1
# Or for linked lists
while fast and fast.next:
slow = slow.next
fast = fast.next.nextSource 3: Incorrect Window Size Calculation
This is the most common off-by-one error in two pointers problems.
The Rule: Inclusive Endpoints
When both left and right are inclusive (pointing to valid elements in the window):
window_size = right - left + 1Why the +1?
Example: nums = [10, 20, 30, 40], left = 1, right = 3
The window contains: [20, 30, 40] (indices 1, 2, 3)
- Number of elements: 3
- Calculation:
right - left + 1 = 3 - 1 + 1 = 3✓
Without the +1:
right - left = 3 - 1 = 2✗ (wrong!)
Visual Proof
Indices: 0 1 2 3
Values: [10, 20, 30, 40]
^ ^
left right
Elements in window: 20, 30, 40
Count: 3
Formula: right - left + 1 = 3 - 1 + 1 = 3 ✓Edge Case: Single Element Window
When left == right:
# left = 2, right = 2
window_size = 2 - 2 + 1 = 1 # Correct!The window contains one element, which is correct.
Systematic Checklist for Correct Bounds
Before you submit your two pointers solution, run through this checklist:
✅ Initialization Checklist
- Empty array check: Do I handle
len(nums) == 0? - Opposite direction: Is
left = 0andright = len(nums) - 1? - Same direction: Are both pointers starting at valid positions?
- Fixed window: Is
right = k - 1for window sizek?
✅ Loop Condition Checklist
- Pairs: Am I using
left < right(not<=)? - Individual elements: Am I using
left <= rightonly when appropriate? - Fast pointer: Is my condition
fast < len(nums)(not<=)?
✅ Window Size Checklist
- Inclusive endpoints: Am I using
right - left + 1? - Single element: Does my formula give 1 when
left == right?
✅ Edge Case Checklist
- Empty array:
nums = [] - Single element:
nums = [5] - Two elements:
nums = [1, 2] - All same:
nums = [3, 3, 3, 3]
Example: Valid Palindrome (Correct Implementation)
Let's apply these principles to a classic problem:
def isPalindrome(s: str) -> bool:
# Clean the string
cleaned = ''.join(c.lower() for c in s if c.isalnum())
# Edge case: empty or single character
if len(cleaned) <= 1:
return True
# Initialize pointers (opposite direction)
left = 0
right = len(cleaned) - 1 # NOT len(cleaned)
# Loop while pointers haven't met
while left < right: # NOT <=
if cleaned[left] != cleaned[right]:
return False
left += 1
right -= 1
return TrueWhy this is correct:
- ✅ Handles empty string and single character
- ✅
right = len(cleaned) - 1(not out of bounds) - ✅
left < right(doesn't compare middle element to itself) - ✅ Increments/decrements correctly
What Happens on Edge Cases?
Empty string: cleaned = ""
len(cleaned) = 0 <= 1→ returnsTrue✓
Single character: cleaned = "a"
len(cleaned) = 1 <= 1→ returnsTrue✓
Two characters (palindrome): cleaned = "aa"
left=0, right=1,cleaned[0] == cleaned[1]→ returnsTrue✓
Two characters (not palindrome): cleaned = "ab"
left=0, right=1,cleaned[0] != cleaned[1]→ returnsFalse✓
Example: Container With Most Water (Off-by-One Trap)
def maxArea(height: List[int]) -> int:
if len(height) < 2:
return 0
left, right = 0, len(height) - 1
max_area = 0
while left < right:
# Width calculation (common mistake: forgetting the distance)
width = right - left # NOT right - left + 1
# Height is the minimum of the two
h = min(height[left], height[right])
# Area calculation
area = width * h
max_area = max(max_area, area)
# Move the shorter line
if height[left] < height[right]:
left += 1
else:
right -= 1
return max_areaKey insight: Width is right - left (not +1) because we're measuring the distance between the two lines, not counting elements in a window.
Example: left=0, right=3
- Distance between positions 0 and 3 is 3 units
- Formula:
3 - 0 = 3✓
Common Patterns and Their Correct Bounds
Pattern 1: Two Sum (Sorted Array)
left, right = 0, len(nums) - 1
while left < right: # Not <=
current_sum = nums[left] + nums[right]
if current_sum == target:
return [left, right]
elif current_sum < target:
left += 1
else:
right -= 1Pattern 2: Remove Duplicates (Same Direction)
if not nums:
return 0
write = 1 # Start at 1, not 0
for read in range(1, len(nums)): # Start at 1
if nums[read] != nums[read - 1]:
nums[write] = nums[read]
write += 1
return write # This is the new lengthPattern 3: Longest Substring (Sliding Window)
left = 0
max_length = 0
for right in range(len(s)):
# Add s[right] to window
while window_invalid():
# Remove s[left] from window
left += 1
# Update max length
max_length = max(max_length, right - left + 1) # +1 for inclusiveDebugging Off-by-One Errors
When your solution fails on edge cases, add debug prints:
while left < right:
print(f"left={left}, right={right}, window_size={right-left+1}")
print(f"values: nums[{left}]={nums[left]}, nums[{right}]={nums[right]}")
# ... your logicLook for:
- Pointers going out of bounds (negative or >= len)
- Pointers crossing when they shouldn't
- Window size being 0 or negative
- Same element being processed twice
Practice Strategy
To master pointer bounds:
- Solve Valid Palindrome (#125) - practice opposite direction with
left < right - Solve Remove Duplicates (#26) - practice same direction with read/write pointers
- Solve Container With Most Water (#11) - practice width calculation
- Manually trace edge cases - empty, single element, two elements
- Use LeetCopilot's execution trace to visualize pointer movement
FAQ
Q: Should I use left < right or left <= right?
A: For pairs (two different elements), use left < right. For individual element processing (like binary search), use left <= right.
Q: Why is window size right - left + 1 and not just right - left?
A: Because both left and right are inclusive indices. If left=2 and right=2, the window contains 1 element, not 0.
Q: How do I handle empty arrays?
A: Always add an early return: if not nums: return [] or if len(nums) == 0: return 0.
Q: What if my pointers need to cross?
A: For most two pointers problems, pointers shouldn't cross. If they do, you've likely found your answer or determined no solution exists.
Q: How do I know if I have an off-by-one error?
A: Your solution fails on edge cases (empty, single element, two elements) or gives wrong window sizes. Add debug prints to trace pointer values.
Conclusion
Off-by-one errors in two pointers are preventable with a systematic approach:
- Initialize correctly:
left = 0,right = len(nums) - 1 - Choose the right loop condition:
left < rightfor pairs - Calculate window size correctly:
right - left + 1for inclusive endpoints - Test edge cases: empty, single element, two elements
- Document your invariants: comment what each pointer represents
Key takeaways:
- Always use
len(nums) - 1for the right pointer, neverlen(nums) - Use
left < rightfor pair-finding problems - Window size with inclusive endpoints is
right - left + 1 - Test edge cases before submitting
Master these principles, and off-by-one errors will become a thing of the past. For more on two pointers patterns, see the complete guide and opposite direction template.
Next time you write left = 0, you'll know exactly why—and you'll get it right the first time.
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
