The two-pointer technique is one of the most common and powerful patterns used in competitive programming, data structures, and algorithms. It helps solve problems that involve searching, sorting, or traversing arrays, strings, or linked lists in an efficient way.
Table of Contents
Core
The two-pointer technique is a way to solve array, string, and linked list problems using two variables that move through the data instead of one.
This technique uses two pointers that either move towards each other, away from each other, or in a synchronous manner, to scan the array or list in one or two passes.
At its core, the idea is simple. Instead of brute-forcing through every possible pair (which usually takes O(n²) time), we strategically move two pointers to find solutions in O(n) or O(n log n).

Different flavours
Depending on the requirements, the two pointers can move toward or away from each other, or at different speeds.
We can also restrict the pointers to stay within a certain boundary, which we commonly call a sliding window.
[1, 2, 3, 4, 5, 6, 7]
L R
[1, 2, 3, 4, 5, 6, 7]
└───────┘
window PythonL is the left boundary, and R is the right boundary. The elements between them form the current window.
These windows can be either fixed-size or variable-size, depending on the problem requirements.
- In a fixed-size window, the distance between the two pointers remains constant as the window moves.
- In a variable-size window, the distance between the pointers can change based on a given condition.
One pointer expands the window, while the other shrinks it when necessary.
Note: We don’t need to focus too much on these types here. A basic understanding of the two-pointer approach is enough.
Sliding Windows
At its core, Sliding Window is an optimisation trick. Instead of recalculating results from scratch every time you move across an array or string, you “slide” a window and reuse previous computations by adding the new element and removing the old one.
Think of it like a magnifying glass scanning across text. You don’t throw away everything you saw before; you just update what changed.
Cycles
One interesting case to note is when the two pointers move at different speeds. For example, one pointer can move one step at a time while the other moves two steps at a time. This approach is commonly used to detect a cycle in a linked list.
This algorithm is called as Floyd’s Cycle Detection Algorithm (Tortoise and Hare).

Analogy
Imagine two runners on a circular track. One runner moves 1 step at a time, while the other moves 2 steps at a time.
Because the track is circular, the faster runner keeps gaining on the slower runner. Eventually, the faster runner catches up to and overtakes the slower runner. At the exact point where the faster runner catches up, they occupy the same position on the track.
The same principle applies to cycle detection: the fast pointer eventually catches the slow pointer if a cycle exists.
Example: Imagine a circular track with 10 positions:
0 → 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9
↑ ↓
└───────────────────────────────────────────┘PythonSuppose:
- Slow runner moves 1 position per step.
- Fast runner moves 2 positions per step.
- Both start at position 0.
After each step:
Step 0: S=0 F=0 distance = 0
Step 1: S=1 F=2 distance = 1
Step 2: S=2 F=4 distance = 2
Step 3: S=3 F=6 distance = 3
Step 4: S=4 F=8 distance = 4
Step 5: S=5 F=0 distance = 5
Step 6: S=6 F=2 distance = 6
Step 7: S=7 F=4 distance = 7
Step 8: S=8 F=6 distance = 8
Step 9: S=9 F=8 distance = 9
Step 10: S=0 F=0 distance = 0 ← caughtPythonThe important point is that distance is circular. The fast runner gains exactly 1 position per step because:
fast speed = 2
slow speed = 1
fast speed - slow speed
relative speed = 2 - 1 = 1PythonOnce both pointers are inside the cycle, think of the cycle as a circular track. The fast pointer gains one node per iteration over the slow pointer. Because the track is circular, the distance between them keeps changing until it becomes zero. At that point, both pointers are at the same node.
So regardless of how far apart they initially are, the fast runner keeps reducing the circular distance by 1 each step.
Eventually, that distance becomes 0, meaning both pointers are at the same node.
That’s why cycle detection works.
Floyd’s Cycle Detection Algorithm
A. Cycle detection
The fast pointer moves twice as fast as the slow pointer. If they meet, a cycle exists.
# Phase 1: Detect cycle
def cycle_detect(head):
slow = head
fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
return FalsePythonCode logic
while fast and fast.next:
fast = fast.next.nextPython- fast → check that fast itself is not None
- fast.next → check that there is at least one node after fast, so fast can safely move two steps
- fast.next.next is NOT checked because it is allowed to be None. If the first two conditions(fast and fast.next) are valid, then fast.next.next gives None, and that is handled by the next iteration by condition fast.
Why does this work?
Initially, both pointers start at the same node.
- slow moves 1 node at a time.
- fast moves 2 nodes at a time.
If there is no cycle, fast will eventually reach None.
If there is a cycle, both pointers enter the cycle. Since fast is moving faster, the distance between them keeps changing until fast catches slow.
B. Finding the start of the cycle/cycle entry point
After slow and fast meet:
- Keep the slow pointer at the meeting point.
- Start a new pointer from the head.
- Move both one step at a time.
They will meet exactly at the start of the cycle.
start = head
while start != slow: #slow pointer starts at meeting point
start = start.next
slow = slow.next
return start PythonC. Length of the cycle

Let:
- a = distance from head to the start of the cycle
- b = distance from cycle start to the meeting point
- c = remaining distance in the cycle
So the cycle length is b + c
length = 1
current = start.next #start = cycle_start
while current != start:
current = current.next
length += 1
return lengthPythonNotes
In Two Pointer problems, we use both for and while loops. The choice depends on whether the pointer movement follows a predictable pattern or continues until a condition is met.
Use a for loop when you know the number of iterations or when one pointer moves through a fixed range.
for fast in range(len(numbers)):
PythonHere, the loop has a predictable upper bound of len(numbers) iterations.
Use a while loop when you want to continue until a condition becomes false, especially when both pointers move dynamically.
Remember, both loops can often solve the same problem. Choose the one that expresses the stopping condition most clearly.
Tier 1 — Anchor Problems
LeetCode 167: Two Sum II – input array is sorted
Problem: Given a 1-indexed sorted array numbers and a target, find two numbers that add up to target and return their (1-indexed) positions.
Input: numbers = [2,7,11,15], target = 9
Output: [1,2]PythonSolution:
The first solution that comes to mind is to use two loops, get all the element pairs, and check each one against the target.
Take the first element, 2, and select other elements using an inner loop soon.
Clearly, it will be O(n2) complexity
Now, another solution that will have complexity O(n): we have a sorted array; pick one element from the start and one from the end.
If the sum of these two equals the target, we are good to go. Got the answer
What if sum(a+b) > target?
Then we need to reduce one of the numbers before ‘a’ is already checked, so we are left with only one number that can be reduced: ‘b’; take an element before b
Similarly, what if sum(a+b) < target?
We have to increase either ‘a’ or ‘b’
The element after b is already checked; the only remaining option is to take an element after ‘a’
def twoSum(self, numbers, target):
left, right = 0, len(numbers) - 1
while left < right:
current_sum = numbers[left] + numbers[right]
if current_sum == target:
return [left + 1, right + 1] # 1-indexed
elif current_sum < target:
left += 1
else:
right -= 1
Python- Time Complexity: O(n)
- Space Complexity: O(1)
- Opposite-direction pointer
LeetCode 15: 3Sum
def threeSum(self, nums: List[int]) -> List[List[int]]:
nums.sort()
n = len(nums)
res = []
for i in range(n - 2):
# skip duplicate fixed element
if i > 0 and nums[i] == nums[i - 1]:
continue
l, r = i + 1, n - 1
while l < r:
s = nums[i] + nums[l] + nums[r]
if s == 0:
res.append([nums[i], nums[l], nums[r]])
# move both pointers
l += 1
r -= 1
# skip duplicates
while l < r and nums[l] == nums[l - 1]:
l += 1
while l < r and nums[r] == nums[r + 1]:
r -= 1
elif s < 0:
l += 1
else:
r -= 1
return resPythonLeetCode 125: Valid Palindrome
Problem: Given a string s, determine if it’s a palindrome after converting all uppercase letters to lowercase and removing all non-alphanumeric characters.
Input: s = "A man, a plan, a canal: Panama"
Output: true
Input: s = "race a car"
Output: falsePythonSolution
class Solution:
def isPalindrome(self, s: str) -> bool:
left, right = 0, len(s) - 1
while left < right:
# skip non-alphanumeric
while left < right and not s[left].isalnum():
left += 1
while left < right and not s[right].isalnum():
right -= 1
if s[left].lower() != s[right].lower():
return False
left += 1
right -= 1
return TruePythonLeetCode 344: Reverse String
The input is given as an array of characters s. You must do this in place by modifying the input array, using O(1) extra memory. Nothing needs to be returned.
Input: s = ["h","e","l","l","o"]
Output: ["o","l","l","e","h"]
Input: s = ["H","a","n","n","a","h"]
Output: ["h","a","n","n","a","H"]PythonSolution
class Solution:
def reverseString(self, s: List[str]) -> None:
"""
Do not return anything, modify s in-place instead.
"""
left, right = 0, len(s) - 1
while left < right:
s[left], s[right] = s[right], s[left]
left += 1
right -= 1PythonLeetCode 141: Linked List Cycle
def hasCycle(head):
slow = head
fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
return FalsePythonLeetCode 26 — Remove Duplicates from Sorted Array
Given a sorted array nums, remove duplicates in-place so each unique element appears once (keeping relative order), and return k, the count of unique elements. The first k elements of nums must hold the unique values in order; anything after index k-1 doesn’t matter.
Input: nums = [1,1,2]
Output: 2, nums = [1,2,]
Input: nums = [0,0,1,1,1,2,2,3,3,4]
Output: 5, nums = [0,1,2,3,4,,,,,]PythonSolution
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
slow = 0
for fast in range(1, len(nums)):
if nums[slow] != nums[fast]:
slow += 1
nums[slow] = nums[fast]
return slow + 1PythonLeetCode 3: Longest Substring Without Repeating Characters
"abcabcbb" → "abc" → length 3
"bbbbb" → "b" → length 1
"pwwkew" → "wke" → length 3
PythonSliding Window with HashMap/Set
def lengthOfLongestSubstring(s):
seen = set()
left = 0
max_len = 0
for right in range(len(s)):
while s[right] in seen:
seen.remove(s[left])
left += 1
seen.add(s[right])
max_len = max(max_len, right - left + 1)
return max_lenPython- Time Complexity = O(n)
- Space Complexity = O(min(n, charset))
Time complexity O(n), not O(n²). That means even though the while loop is inside the for loop, it doesn’t restart from scratch for every iteration — instead, it just continues moving left forward.
Space Complexity: At first glance, we’re storing characters in a set or HashMap. In the worst case, we could store all characters of the string → O(n).
Optimised with HashMap (Skip Moves)
Instead of moving left one step at a time, we can jump directly using a dictionary that stores the last seen index of each character.
def lengthOfLongestSubstring(s: str) -> int:
char_index = {}
left = 0
max_len = 0
for right, ch in enumerate(s):
if ch in char_index and char_index[ch] >= left:
left = char_index[ch] + 1 # jump left pointer
char_index[ch] = right
max_len = max(max_len, right - left + 1)
return max_lenPythonTier 2 — Variation Problems
LeetCode 643. Maximum Average Subarray I
class Solution:
def findMaxAverage(self, nums: list[int], k: int) -> float:
# Step 1: Compute sum of first window
window_sum = sum(nums[:k]) #first result from first window
max_sum = window_sum
# Step 2: Slide the window
for i in range(k, len(nums)):
window_sum += nums[i] - nums[i - k] # following results from following windows
max_sum = max(max_sum, window_sum)
# Step 3: Return max average
return max_sum / kPythonLeetCode 219: Contains Duplicate II
Way 1 Sliding Window + HashSet
class Solution:
def containsNearbyDuplicate(self, nums, k):
window = set()
for i, num in enumerate(nums):
if num in window:
return True
window.add(num)
if len(window) > k:
window.remove(nums[i - k])
return False
Way 2 : HashMap
class Solution:
def containsNearbyDuplicate(self, nums: list[int], k: int) -> bool:
seen = {}
for i, num in enumerate(nums):
if num in seen and i - seen[num] <= k:
return True
seen[num] = i
return False
Way 3 Array
class Solution:
def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
if k == 0 or len(nums) == len(set(nums)):
return False
for i in range(len(nums)):
if nums[i] in nums[i + 1 : i + k + 1]:
return True
return FalsePythonLeetCode 876: Middle of the Linked List
Given the head of a singly linked list, return the middle node of the linked list.
If there are two middle nodes, return the second middle node.
Input: head = [1,2,3,4,5]
Output: [3,4,5]
Explanation: The middle node of the list is node 3.PythonSolution
class Solution:
def middleNode(self, head: Optional[ListNode]) -> Optional[ListNode]:
slow = head
fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slowPythonLeetCode 19: Remove Nth Node From End of List
Given the head of a linked list, remove the nth node from the end of the list and return its head.
Input: head = [1,2,3,4,5], n = 2
Output: [1,2,3,5]PythonSolution
Simplest way to use two loops: one loop to get the length of the linked list and 2nd loop to remove the node
Optimized version
def removeNthFromEnd(self, head: ListNode | None, n: int) -> ListNode | None:
dummy = ListNode(0, head)
slow = fast = dummy
for _ in range(n):
fast = fast.next
while fast.next:
slow = slow.next
fast = fast.next
slow.next = slow.next.next
return dummy.nextPythonWhat dummy = ListNode(0, head) ?
ListNode(0, head): This will create a new node pointing to head with a value of zero
Why do we need that?
It handles removing the head itself, such as when n equals the list length, without requiring a separate special case.
Tier 3 — Hard/Edge Problems
LeetCode 633: Sum of Square Numbers
By Two pointer
class Solution:
def judgeSquareSum(self, c: int) -> bool:
a = 0
b = int(c ** 0.5)
while a <= b:
s = a*a + b*b
if s == c:
return True
elif s < c:
a += 1
else:
b -= 1
return False
'''
why b = int(c**0.5)
b*b = c-a*a
b = sqrt(c-a*a)
let suppose a = b =sqrt(c)
then a*a+b*b ==> 2c
even if one of a or b is greater than sqrt(c) will in valid a*a+b*b =c
example
b greater than sqrt(c)
a*a + b*b> c , Hence both should be less than sqrt(c)
'''PythonBy binary search
class Solution:
def judgeSquareSum(self, c: int) -> bool:
import math
limit = int(math.isqrt(c)) # ⌊√c⌋ safely
for a in range(limit + 1):
target = c - a*a
if self.binary_search(0, limit, target):
return True
return False
def binary_search(self, left: int, right: int, target: int) -> bool:
while left <= right:
mid = (left + right) // 2
sq = mid * mid
if sq == target:
return True
elif sq < target:
left = mid + 1
else:
right = mid - 1
return False
'''
a² + b² = c
⇒ b² = c − a²
Key points:
a ranges from 0 to ⌊√c⌋
For each a, search b in range [0, ⌊√c⌋]
Never take sqrt of negative numbers
'''PythonTwo pointers are optimal, but binary search is often asked in interviews.
By Hash set
class Solution:
def judgeSquareSum(self, c: int) -> bool:
squares = set()
i = 0
while i * i <= c:
squares.add(i * i)
i += 1
for s in squares:
if c - s in squares:
return True
return False
Python| Approach | Time | Space |
|---|---|---|
| Two Pointers | O(√c) | O(1) |
| Binary Search | O(√c log c) | O(1) |
| Hash Set | O(√c) | O(√c) |
LeetCode 11: Container With Most Water
You are given an integer array height of length n. There are n vertical lines drawn such that the two endpoints of the ith line are (i, 0) and (i, height[i]).
Find two lines that, together with the x-axis, form a container such that the container contains the most water.
Return the maximum amount of water a container can store.
Notice that you may not slant the container.

Example 1:
Input: height = [1,8,6,2,5,4,8,3,7]
Output: 49
Example 2:
Input: height = [1,1]
Output: 1
Solution
That initial instinct is completely natural—when a problem asks to maximise Area, your first reflex is to look ahead and pick the move that gives the bigger Area.
Failed attempt
class Solution:
def maxArea(self, height: List[int]) -> int:
length = len(height)
left = 0
right = length-1
def area(left,right):
h = min(height[left],height[right])
d = right-left
return h*d
result = 0
while left<right:
a = area(left,right)
if result<a:
result = a
b = area(left+1,right)
if b>=a:
left+=1
else:
right-=1
return result
PythonIt works for many cases, but for cases like height = [2, 1, 10, 10], it fails
[2 ,1, 10, 10]
| | area = 2*3 = 6
[2 ,1, 10, 10]
| | area = 1*2 = 2
[2 ,1, 10, 10]
| | area = 1*1 =1
Result = 6, which is wrong
PythonIt’s stuck in a local optimum
Different approach
Which pointer to move left or right based on the area failed. One more approach we can try is based on height[left] and height[right]
Concept
Distance is guaranteed to reduce by 1 on every single step, no matter which pointer you move.
Area = height of shorter line * distancePythonSo distance is always reduced, so try to optimise height
class Solution:
def maxArea(self, height: List[int]) -> int:
length = len(height)
left = 0
right = length-1
result = 0
while left<right:
area = min(height[left],height[right]) * (right-left)
if result<area:
result = area
if height[left] >height[right]:
right-=1
else:
left+=1
return result PythonLeetCode 1004: Max Consecutive Ones III
Given a binary array nums and an integer k, return the maximum number of consecutive 1‘s in the array if you can flip at most k 0‘s.
Input: nums = [1,1,1,0,0,0,1,1,1,1,0], k = 2
Output: 6
Explanation: [1,1,1,0,0,1,1,1,1,1,1]
Bolded numbers were flipped from 0 to 1. The longest subarray is underlined.PythonSolution
class Solution:
def longestOnes(self, nums: List[int], k: int) -> int:
slow = 0
zeros = 0
result = 0
for fast in range(len(nums)):
if nums[fast] == 0:
zeros += 1
if zeros > k:
if nums[slow] == 0:
zeros -= 1
slow += 1
result = max(result, fast - slow + 1)
return resultPythonExtra
More to practice
- Trapping Rain Water
- Longest Substring Without Repeating Characters
- Minimum Window Substring (sliding window variation)
- Merge Two Sorted Linked Lists
- LeetCode 18: 4 Sum
- 42. Trapping Rain Water
- 3. Longest Substring Without Repeating Characters
- 209. Minimum Size Subarray Sum
- 76. Minimum Window Substring
- 159. Longest Substring with At Most Two Distinct Characters
- 142. Linked List Cycle II
- 19. Remove Nth Node From End of List
- 27. Remove Element
- 283. Move Zeroes
- 80. Remove Duplicates from Sorted Array II
- 977. Squares of a Sorted Array
- 16. 3Sum Closest
- 30. Substring with Concatenation of All Words
- 992. Subarrays with K Different Integers
- 287. Find the Duplicate Number
- 202. Happy Number
- 457. Circular Array Loop
- 143. Reorder List
Conclusion
The two-pointer technique is a must-know for anyone preparing for coding interviews or competitive programming. From searching pairs in sorted arrays to detecting cycles in linked lists, it significantly simplifies complex problems.