The Two Pointer Technique: A Complete Guide

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.

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).

two pointer

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    
Python

L 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).

Floyd's Cycle Detection

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
↑                                           ↓
└───────────────────────────────────────────┘
Python

Suppose:

  • 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  ← caught
Python

The 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 = 1
Python

Once 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 False
Python

Code logic

while fast and fast.next:
    fast = fast.next.next
Python

  • 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 
Python

C. Length of the cycle

slow and fast pointer

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 length
Python

Notes

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)):
Python

Here, 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]
Python

Solution:

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 res
Python

LeetCode 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: false
Python

Solution

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 True
Python

LeetCode 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"]
Python

Solution

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 -= 1
Python

LeetCode 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 False
Python

LeetCode 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,,,,,]
Python

Solution

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 + 1
Python

LeetCode 3: Longest Substring Without Repeating Characters

"abcabcbb" → "abc" → length 3
"bbbbb" → "b" → length 1
"pwwkew" → "wke" → length 3    
Python

Sliding 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_len
Python

  • 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_len
Python

Tier 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 / k
Python

LeetCode 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 False
Python

LeetCode 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.
Python

Solution


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 slow
Python

LeetCode 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]
Python

Solution

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.next
Python

What 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)
'''
Python

Read about √n

By 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
'''
Python

Two 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

ApproachTimeSpace
Two PointersO(√c)O(1)
Binary SearchO(√c log c)O(1)
Hash SetO(√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.

Two pointer

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
                
Python

It 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     
   
Python

It’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 * distance
Python

So 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        
Python

LeetCode 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.
Python

Solution

 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 result
Python

Extra

More to practice

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.

Resource


About Puneet Verma

Puneet Verma is a software developer specialising in backend architecture, Dynamic Programming, and SaaS solutions. He focuses on building optimised, scalable applications and sharing deep-dive technical tutorials to help developers master complex algorithmic patterns.

Leave a Comment