Two Pointers in Python: A Complete Problem-Solving Guide
Two Pointers in Python. Part of the Data Structures & Algorithms with Python roadmap
This is the second article in the Arrays & Strings cluster. It builds on Arrays in Python: Operations, Complexity, and Common Patterns, so read that one first if you haven’t.
In the last article, you learned that arrays are fast to read and slow to change near the front. You also saw a short preview of five array patterns. The first one was two pointers.
Now let’s learn it properly.
Here is the problem. You get a list of numbers. You must find two of them that add up to a target. The first idea most people have is to check every pair. That works. But it gets slow very fast.
Two pointers is a better way. You place two markers on the list and move them with a simple rule. Each marker moves only forward or only inward. You never go back. So the work stays small.
This guide is for Python learners and interview candidates. You will learn how two pointers in Python works, the three main types, five worked examples, and when the technique does not work. At the end, you can try five practice problems with solutions.
In simple terms: The two pointers technique uses two index variables to walk through an array or string. The pointers move by a clear rule, so each item is visited only a few times. Many problems that need O(n²) with nested loops can be solved in O(n) this way.
What Is the Two Pointers Technique?
Imagine a long hallway of lockers. You and a friend need to find two lockers whose numbers add up to 13. The lockers are sorted by the number on the door.
You could walk the whole hallway for every locker. That would take forever.
Here is a smarter plan:
- You stand at the first locker.
- Your friend stands at the last locker.
- You add your two numbers.
- If the sum is too small, you take one step forward.
- If the sum is too big, your friend takes one step back.
- You stop when the sum is right, or when you meet in the middle.
That is the two pointers technique.
Here is the technical definition. Two pointers is a problem-solving pattern where you keep two index variables and move them through a sequence (an array, a string, or a linked list) by a fixed rule. Each pointer only moves in one direction, so the total number of moves is limited by the length of the input.
One note about the word “pointer.” In Python, a pointer is not a real memory address like in C. It is just an index, like left = 0 and right = 5. Sometimes it is a reference to a node in a linked list. The idea is the same.
Why Two Pointers Can Improve Algorithm Efficiency
Let’s look at the cost of the nested loop first.
Say you have 10,000 numbers. A nested loop checks every pair. The number of pairs is n × (n − 1) / 2. For 10,000 numbers, that is 49,995,000 pairs.
Now look at two pointers. Each round of the loop moves one pointer by one place. The loop runs at most n − 1 times. For 10,000 numbers, that is fewer than 10,000 rounds. This is an upper limit, not an exact count. The loop can also stop early when it finds the answer.
That is the big win:
| Approach | Work for n = 10,000 | Time complexity |
|---|---|---|
| Nested loops | up to about 50 million pair checks | O(n²) |
| Two pointers | up to about 10,000 steps | O(n) |

The reason is simple. Nested loops forget what they learned. They check a pair, throw away the result, and check the next pair.
Two pointers uses each comparison to throw away items for good. When the sum is too small, we know the left item can never work with anything we still have. So we drop it. We never look at it again.
If Big O still feels new, read What Is Big O Notation? first. To learn how to count operations yourself, read How to Analyze the Time and Space Complexity of an Algorithm.
How the Two Pointers Technique Works
Every two pointers solution follows the same four steps.
- Place the pointers. Decide where each one starts. Often it is both ends, or both at the start.
- Look at the items they point to. Compare them, add them, or check a condition.
- Move a pointer. Use a rule that tells you which pointer to move.
- Stop at the right time. Usually this is when the pointers meet or cross, or when one reaches the end.
Here is the shape of the code for pointers that start at both ends:
left, right = 0, len(items) - 1
while left < right:
# look at items[left] and items[right]
# decide which pointer to move
left += 1 # or
right -= 1Look at the loop condition: left < right. This is very important. It stops the loop when the pointers meet. If you write it wrong, you get bugs. We will come back to this in the mistakes section.
Types of Two Pointers Patterns
Most two pointers problems fit into one of three types. If you can name the type, you can solve the problem faster.

| Type | Where pointers start | How they move | Typical use |
|---|---|---|---|
| Opposite-direction | Both ends | Move toward each other | Pairs, palindromes, reversing |
| Same-direction | Both at the start | Both move forward, one faster than the other (in steps) | Filtering, removing, merging |
| Fast and slow | Both at the start | One moves 1 step, the other moves 2 | Linked lists, cycles, middle node |
Opposite-Direction Pointers
One pointer starts at the left end. The other starts at the right end. They walk toward each other.
Use this type when:
- The input is sorted, and you need a pair.
- You need to compare the two ends, like in a palindrome.
- You need to swap items from both ends, like reversing.
Same-Direction Pointers
Both pointers start at the beginning. They both move forward. But they do different jobs.
Think of a teacher checking homework. One hand reads each page. The other hand marks where the next good page goes. The reading hand always moves forward. The writing hand moves only when a good page is found.
In code, we call them read and write. You already saw this in the last article when we moved zeros to the end of a list.
Use this type when:
- You need to filter items in place.
- You need to remove duplicates.
- You need to merge two sorted lists.
Fast and Slow Pointers
Both pointers start at the same place. One moves one step at a time. The other moves two steps at a time.
Think of two runners on a track. The fast runner goes twice as quickly. If the track is a loop, the fast runner will catch the slow runner from behind. If the track is a straight road, the fast runner reaches the end when the slow runner is about halfway.
This type is mostly used on linked lists. A short example is below. The full guide is in the Fast and Slow Pointers article (Article 17).
Two Pointers Examples in Python
Let’s solve five classic problems. For each one, we will explain the idea first, then show the code.
Find a Pair With a Target Sum
Problem: Given a sorted list and a target, find two numbers that add up to the target. Return their positions.
Brute force first. Check every pair:
def pair_sum_brute(nums, target):
n = len(nums)
for i in range(n):
for j in range(i + 1, n):
if nums[i] + nums[j] == target:
return [i, j]
return []This works. But it is O(n²).
The insight. The list is sorted. So the left pointer holds the smallest value and the right pointer holds the largest. Let’s add them:
- If the sum is too small, the left item is too small to work with anything. Every other item still in play is at most the right item. So we drop the left item and move
leftforward. - If the sum is too big, the right item is too big to work with anything. Every other item still in play is at least the left item. So we drop the right item and move
rightbackward. - If the sum is equal, we are done.
def pair_with_target_sum(sorted_nums, target):
left, right = 0, len(sorted_nums) - 1
while left < right:
total = sorted_nums[left] + sorted_nums[right]
if total == target:
return [left, right]
if total < target:
left += 1
else:
right -= 1
return []
print(pair_with_target_sum([1, 3, 5, 8, 11], 13)) # [2, 3]
print(pair_with_target_sum([1, 3, 5, 8, 11], 100)) # []
Let’s trace it for target 13:
| Step | left | right | nums[left] | nums[right] | Sum | What we do |
|---|---|---|---|---|---|---|
| 1 | 0 | 4 | 1 | 11 | 12 | Too small, move left |
| 2 | 1 | 4 | 3 | 11 | 14 | Too big, move right |
| 3 | 1 | 3 | 3 | 8 | 11 | Too small, move left |
| 4 | 2 | 3 | 5 | 8 | 13 | Found it |
We found the answer in 4 steps. Time is O(n). Extra space is O(1).
Important: This only works because the list is sorted. If the list is not sorted, the “too small, too big” logic breaks. We will talk about this more in the “When Does It Work” section.
Reverse an Array In Place
Problem: Reverse a list without making a new list.
The idea: Swap the first and last items. Then swap the second and second-to-last. Keep going until the pointers meet.
def reverse_in_place(a):
left, right = 0, len(a) - 1
while left < right:
a[left], a[right] = a[right], a[left]
left += 1
right -= 1
nums = [1, 2, 3, 4, 5]
reverse_in_place(nums)
print(nums) # [5, 4, 3, 2, 1]Here the list has 5 items. The pointers meet at the middle item (index 2). The loop stops because left < right is no longer true. The middle item does not need a swap. It stays where it is.
Time is O(n). Extra space is O(1). This swap loop is also the base of the O(1)-space “rotate an array” solution from the last article.
Remove Duplicates From a Sorted Array
Problem: Remove duplicates from a sorted list in place. Return the number of unique items.
The idea: This is a same-direction problem. Use two pointers:
readlooks at every item.writemarks where the next unique item should go.
Because the list is sorted, equal values sit next to each other. So a new value is just one that is different from the last unique value we kept.
def remove_duplicates(a):
if not a:
return 0
write = 1
for read in range(1, len(a)):
if a[read] != a[write - 1]: # a new value
a[write] = a[read]
write += 1
return write # length of the unique part
nums = [0, 0, 1, 1, 1, 2, 2, 3, 3, 4]
k = remove_duplicates(nums)
print(k) # 5
print(nums[:k]) # [0, 1, 2, 3, 4]
Let’s see what happens step by step:
writestarts at 1, because the first item is always unique.readmoves forward through the list.- Each time
readfinds a value different from the last kept value, we copy it towriteand movewriteforward. - The items after position
kare leftovers. We ignore them.
Time is O(n). Extra space is O(1). Say out loud in an interview that this works only because the list is sorted.
Check Whether a String Is a Palindrome
Problem: A palindrome reads the same forward and backward. Check if a string is a palindrome. Ignore spaces, punctuation, and capital letters.
The idea: Compare the first and last characters. If they match, move both pointers inward. If any pair does not match, the answer is False. We skip characters that are not letters or digits.
def is_palindrome(s):
left, right = 0, len(s) - 1
while left < right:
if not s[left].isalnum():
left += 1
continue
if not s[right].isalnum():
right -= 1
continue
if s[left].lower() != s[right].lower():
return False
left += 1
right -= 1
return True
print(is_palindrome("A man, a plan, a canal: Panama")) # True
print(is_palindrome("race a car")) # False
print(is_palindrome("")) # TrueYou might know a shorter way: s == s[::-1]. That is fine for a quick check. But it builds a new reversed string, so it uses O(n) extra space. The two pointers version uses O(1) extra space and stops at the first mismatch.
Strings are a big topic of their own. You will see more patterns like this in String Algorithms in Python (Article 11).
Merge Two Sorted Arrays
Problem: Merge two sorted lists into one sorted list.
The idea: Put one pointer at the start of each list. Compare the two items. Take the smaller one. Move that pointer forward.
def merge_sorted(a, b):
i = j = 0
result = []
while i < len(a) and j < len(b):
if a[i] <= b[j]:
result.append(a[i])
i += 1
else:
result.append(b[j])
j += 1
result.extend(a[i:]) # at most one of these has items left
result.extend(b[j:])
return result
print(merge_sorted([1, 4, 7], [2, 3, 9])) # [1, 2, 3, 4, 7, 9]
print(merge_sorted([], [5, 6])) # [5, 6]The loop stops as soon as one list is used up. So at most one of the two leftover slices has items, and the other is empty. Time is O(n + m). Extra space is O(n + m) for the result. This same step is used inside merge sort, which you will meet later in the series.
Fast and Slow Pointers: Find the Middle of a Linked List
Here is a short preview of the third type. A linked list does not let you jump to the middle by index. So how do you find the middle in one pass?
Use two pointers. The slow one moves 1 step. The fast one moves 2 steps. When the fast pointer reaches the end, the slow pointer is at the middle. If the list has an even number of nodes, there are two middle nodes, and this code returns the second one.
class Node:
def __init__(self, val, next=None):
self.val = val
self.next = next
def find_middle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow
head = Node(1, Node(2, Node(3, Node(4, Node(5)))))
print(find_middle(head).val) # 3
head = Node(1, Node(2, Node(3, Node(4))))
print(find_middle(head).val) # 3 (the second of the two middle nodes, 2 and 3)Time is O(n). Extra space is O(1). The same idea, with one small change, detects a cycle in a linked list. We cover both in Article 17.
Two Pointers vs Nested Loops
Both approaches can solve many of the same problems. Here is how they compare.
| Nested loops | Two pointers | |
|---|---|---|
| Time | O(n²) | O(n) |
| Extra space | O(1) | O(1) |
| Needs special input? | No | Often sorted, or a clear movement rule |
| Easy to write? | Very | Needs care with pointer rules |
| Good for | Small inputs, first draft | Large inputs, interviews |
Use this simple process in an interview:
- Say the brute force solution out loud. Name its cost.
- Ask: “Is the input sorted? Or can I sort it?”
- Ask: “Can one comparison let me drop an item for good?”
- If yes, move to two pointers.
Interviewers like this flow. It shows you can find the slow part and fix it.
Time and Space Complexity Analysis
Here are the costs for the examples above.
| Problem | Time | Extra space | Why |
|---|---|---|---|
| Pair with target sum (sorted) | O(n) | O(1) | Pointers move inward, at most n − 1 steps |
| Reverse in place | O(n) | O(1) | n / 2 swaps |
| Remove duplicates (sorted) | O(n) | O(1) | read visits each item once |
| Palindrome check | O(n) | O(1) | Each pointer moves at most n / 2 steps |
| Merge two sorted lists | O(n + m) | O(n + m) | Each item is placed once; result list is new |
| Middle of linked list | O(n) | O(1) | Fast pointer visits each node at most once |
Why O(n)? The pair sum loop runs at most n − 1 rounds. Every round moves one pointer, and neither pointer ever goes back. The same reasoning covers the other rows.
Two things to remember:
- Sorting is not free. If the input is not sorted and you sort it first, the total time is O(n log n). The sort becomes the slowest part.
- The extra space is O(1) because we only store a few index variables. The exception is merging, where we build a new list.
When Does the Two Pointers Technique Work?
This is the most important section. Knowing how it works is half the skill. Knowing when to use it is the other half.

Clues in the problem
Look for these signs:
- The input is sorted, and you need a pair, a triple, or a closest value.
- The problem says “in place” or “O(1) extra space.”
- You need to compare the two ends, like a palindrome.
- You need to remove or filter items from a list.
- You must merge two sorted sequences.
- It is a linked list and you need the middle, or a cycle.
When it does NOT work
Be honest about the limits. Two pointers is not magic.
| Situation | Why two pointers fails | Better tool |
|---|---|---|
| Pair sum on an unsorted list, and you need the original indexes | Sorting changes the positions | A dictionary (see Two Sum in the last article) |
| Sum of a contiguous subarray that may contain negative numbers | A pointer cannot decide when to move | Prefix sum with a dictionary |
| Problems about a window of items | A window is a better fit | Sliding window |
| Finding an item in a sorted list | One pointer is enough | Binary search |
A common question: does it need sorted input? Not always. Palindrome checks, reversing, and removing zeros work on unsorted data. Pair sum, removing duplicates, and merging need sorted data.
Also, negative numbers do not break the sorted pair sum. If the list is sorted, [-4, -1, 0, 3, 10] works fine with the same code.
Common Two Pointers Mistakes
| Mistake | What goes wrong | How to avoid it |
|---|---|---|
| Using it on unsorted data when sorting is required | Wrong answers, with no error message | Check: does my logic depend on order? |
Writing left <= right instead of left < right | A pointer pairs with itself | Use < when you need two different items |
| Forgetting to move a pointer | Infinite loop | Make sure every branch moves at least one pointer |
| Moving the wrong pointer | Missing the correct answer | Write the rule first: “too small means move which pointer?” |
| Ignoring empty input | IndexError on a[0] | Check if not a: at the start |
| Forgetting single-item input | Loop never runs, or wrong result | Test with one item |
| Changing the input when you shouldn’t | The caller’s data is changed | Say clearly if the function works in place |
| Skipping duplicates incorrectly | Duplicate answers appear in results | Skip equal values after you record an answer |
Here is the infinite loop mistake in action:
# Wrong: when total == target, no pointer moves
def bad_pair_sum(nums, target):
left, right = 0, len(nums) - 1
while left < right:
total = nums[left] + nums[right]
if total < target:
left += 1
elif total > target:
right -= 1
# equal case: nothing changes, loop runs foreverAlways make sure each branch either moves a pointer or returns.
Edge cases to test every time:
- an empty list
- one item
- two items
- all items equal
- duplicate values
- negative numbers
- no valid answer
Practice Problems With Solutions
Try each problem on your own for a few minutes. Then check the answer.
| # | Problem | Level | Pointer type |
|---|---|---|---|
| 1 | Is Subsequence | Easy | Same direction |
| 2 | Squares of a Sorted Array | Easy | Opposite direction |
| 3 | Container With Most Water | Medium | Opposite direction |
| 4 | Three Sum | Medium | Opposite direction + loop |
| 5 | Middle of a Linked List | Easy | Fast and slow |
You already saw problem 5 above. Here are the other four.
Problem 1: Is Subsequence
Check if string s is a subsequence of string t. A subsequence keeps the order but can skip characters.
Hint: Walk through t once. Move a pointer in s only when you find a match.
def is_subsequence(s, t):
i = 0 # pointer in s
for ch in t:
if i < len(s) and ch == s[i]:
i += 1
return i == len(s)
print(is_subsequence("ace", "abcde")) # True
print(is_subsequence("aec", "abcde")) # FalseTime is O(n), where n is the length of t. Extra space is O(1). Note that s = "" returns True, which is correct, because an empty string is a subsequence of anything.
Problem 2: Squares of a Sorted Array
Given a sorted list that can have negative numbers, return the squares in sorted order.
Hint: The biggest square is at one of the two ends. A big negative number has a big square. Fill the answer from the back.
def sorted_squares(nums):
n = len(nums)
result = [0] * n
left, right = 0, n - 1
for pos in range(n - 1, -1, -1):
if abs(nums[left]) > abs(nums[right]):
result[pos] = nums[left] ** 2
left += 1
else:
result[pos] = nums[right] ** 2
right -= 1
return result
print(sorted_squares([-4, -1, 0, 3, 10])) # [0, 1, 9, 16, 100]Time is O(n). Extra space is O(n) for the output. Squaring everything and calling sort() would cost O(n log n). Two pointers is faster because the input is already sorted.
Problem 3: Container With Most Water
You get a list of heights. Each height is a vertical line. Pick two lines. Together with the x-axis, they form a container. Find the most water a container can hold.
Hint: The water depends on the shorter line and the distance between the lines. Start with the widest container. Then move the shorter line inward, because moving the taller line can never help.
def max_area(height):
left, right = 0, len(height) - 1
best = 0
while left < right:
h = min(height[left], height[right])
best = max(best, h * (right - left))
if height[left] < height[right]:
left += 1
else:
right -= 1
return best
print(max_area([1, 8, 6, 2, 5, 4, 8, 3, 7])) # 49Why move the shorter line? The width is shrinking every step. The only way to get more water is to find a taller line. The shorter line limits the height, so keeping it can never improve the answer.
Time is O(n). Extra space is O(1). The brute force way checks every pair in O(n²).
Problem 4: Three Sum
Find all unique triplets in a list that add up to zero.
Hint: Sort the list. Fix one number with a loop. Then use two pointers on the rest to find a pair that makes the total zero.
def three_sum(nums):
nums.sort()
result = []
for i in range(len(nums) - 2):
if i > 0 and nums[i] == nums[i - 1]: # skip duplicate first numbers
continue
left, right = i + 1, len(nums) - 1
while left < right:
total = nums[i] + nums[left] + nums[right]
if total < 0:
left += 1
elif total > 0:
right -= 1
else:
result.append([nums[i], nums[left], nums[right]])
left += 1
right -= 1
while left < right and nums[left] == nums[left - 1]:
left += 1
while left < right and nums[right] == nums[right + 1]:
right -= 1
return result
print(three_sum([-1, 0, 1, 2, -1, -4])) # [[-1, -1, 2], [-1, 0, 1]]Time is O(n²). The outer loop runs n times. The two pointers inside run in O(n) each time. Extra space is O(1), not counting the output and the sort. The brute force way uses three loops, which is O(n³).
Notice how the duplicate skipping works. After we record a triplet, we move past equal values. That is how we avoid repeating the same answer.
After these, try the same ideas with empty lists, one item, and all zeros.
Frequently Asked Questions
Does the two pointers technique need a sorted array?
Not always. Sorted input is needed for pair sums, removing duplicates, and merging. It is not needed for reversing, palindrome checks, or moving zeros. Check whether your logic depends on order.
What is the time complexity of two pointers?
It is usually O(n), because each pointer moves in one direction and never goes back. If you must sort first, the total is O(n log n). The extra space is usually O(1).
What is the difference between two pointers and sliding window?
Two pointers is the broader idea. Sliding window is a special case where the two pointers mark the start and end of a contiguous window. Sliding window keeps a running value for that window. We cover it in the Sliding Window Technique in Python article.
Are pointers in Python real memory addresses?
No. In Python, a “pointer” is usually just an index, like
left = 0. For linked lists, it is a reference to a node. The pattern is the same as in other languages.Can I use two pointers on strings?
Yes. Strings work like arrays for this purpose. You can use indexes to compare characters. Remember that Python strings are immutable, so you cannot swap characters in place. You need to convert to a list first.
When should I use binary search instead of two pointers?
Use binary search when you look for one value in a sorted list. It runs in O(log n). Use two pointers when you need to work with pairs or move through the list in a steady way.
Does two pointers work on linked lists?
Yes. The fast and slow version is made for linked lists. It finds the middle node and detects cycles without extra memory.
Conclusion
Two pointers is a simple idea. Place two markers. Move them by a clear rule. Never go back.
You saw the three types: opposite-direction, same-direction, and fast and slow. You solved the pair sum, reverse, remove duplicates, palindrome, and merge problems. You also saw when the technique does not work.
The key habit is this. When you see a nested loop, ask one question: “Can one comparison let me throw away an item for good?” If the answer is yes, two pointers may turn O(n²) into O(n).
Next, learn the pattern that comes from this one. Read the Sliding Window Technique in Python: Complete Guide. To see the whole path, visit the DSA with Python guide.
Official External Resources
- Python Tutorial: Data Structures: the official guide to lists, slicing, and list methods.
- Python Docs: str.isalnum: how the string check used in the palindrome example works.
- Python Wiki: Time Complexity: the cost of list operations.
- Python Docs: bisect module: binary search on sorted lists.
- MIT OpenCourseWare: Introduction to Algorithms (6.006): free lectures on algorithms.

Leave a Reply