Fun with Learning Technology
LearnCoursesQuestionsTracksToolsNewsExplorePractice
Fun with Learning Technology

A new problem, explained clearly, every day.

Subscribe
Learn
  • Lessons
  • Topics
  • News
  • Tools
  • Courses
  • Career tracks
  • Everything
Site
  • About
  • Contact
  • Support
  • Privacy
  • Terms
Get the daily one

One email per new problem. No spam.

Request a tutorial

Requests shape what gets made next.

© 2026 Fun with Learning TechnologyRSS
Home›Courses›Python›Python Coding Patterns — Two Pointers

Interview Prep

Python Coding Patterns — Two Pointers

The two-pointer pattern involves maintaining two references that traverse a data structure at different speeds or from different directions to solve problems efficiently. This approach is fundamental because it transforms many brute-force quadratic time complexity solutions into linear time complexity solutions by eliminating redundant comparisons. You should reach for this technique whenever you encounter sorted arrays, linked lists, or strings where you need to find pairs, subarrays, or specific targets based on positional relationships.

The Opposite-Ends Strategy

The opposite-ends strategy is the most intuitive implementation of the two-pointer technique, where you place one pointer at the start of a structure and the other at the end. As the pointers move toward the middle, you incrementally reduce the problem space. This works exceptionally well for sorted arrays because the ordering allows you to make logical decisions about which pointer to move based on the current sum or condition. If your target is larger than the sum of the pointers, you must increase the left value; if smaller, you must decrease the right value. Because each element is visited at most once, this strategy achieves linear time complexity. The underlying reasoning relies on the fact that once a pointer passes an element that cannot possibly contribute to a valid solution due to the sorted nature, that element can be permanently discarded, effectively pruning the search space without missing potential answers.

def find_target_sum(arr, target):
    # Initialize pointers at both ends of the list
    left, right = 0, len(arr) - 1
    while left < right:
        current_sum = arr[left] + arr[right]
        if current_sum == target:
            return [left, right]
        # If sum is too low, increase left pointer to increase total
        elif current_sum < target:
            left += 1
        # If sum is too high, decrease right pointer to reduce total
        else:
            right -= 1
    return None

Slow and Fast Pointers

The slow and fast pointer pattern, often referred to as the tortoise and the hare, is uniquely suited for detecting cycles or identifying specific nodes in linear data structures like linked lists. The reasoning here is that if a cycle exists, the faster pointer will eventually lap the slower pointer, confirming the presence of a loop. Because the fast pointer moves at twice the speed of the slow one, the gap between them increases by one at each step, ensuring they eventually meet inside the cycle. This logic is far superior to using extra memory, like a hash set, to track visited nodes. By managing the relative speeds of these references, we can solve problems involving middle-element finding or cycle detection in constant space. This pattern exploits the property of predictable movement, where the mathematical relationship between the velocities of the two pointers guarantees intersection at a specific point determined by the structure of the underlying data.

class Node:
    def __init__(self, val): self.val = val; self.next = None

def has_cycle(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next      # Moves one step
        fast = fast.next.next # Moves two steps
        # If they meet, a cycle exists
        if slow == fast:
            return True
    return False

Partitioning with Pointers

Partitioning with two pointers involves managing elements based on a specific criteria, effectively segregating the data into two distinct groups within a single pass. The classic example is moving all occurrences of a specific value to one side while keeping other elements in relative order or simply rearranging them. You maintain one pointer as a boundary for the processed section and another to scan through the input. As the scanning pointer finds elements that meet the criteria, you swap them with the element at the boundary pointer and increment the boundary. This works because you are maintaining a formal invariant: every element before the boundary pointer satisfies the condition, while everything between the boundary and the current scanner does not. By strictly enforcing this state, you ensure that the transformation is completed in a single pass without requiring auxiliary data structures, maintaining high memory efficiency.

def move_zeros(nums):
    boundary = 0
    for i in range(len(nums)):
        # If we find a non-zero, swap it to the boundary
        if nums[i] != 0:
            nums[boundary], nums[i] = nums[i], nums[boundary]
            boundary += 1
    return nums

Sliding Window as Two Pointers

A sliding window is essentially a specialized two-pointer pattern where the pointers represent the start and end of a dynamic interval rather than static positions. This pattern is essential for problems involving subarrays or substrings. The logic works by expanding the 'right' pointer to include new elements into the window, and contracting the 'left' pointer once the current window exceeds a constraint or satisfies a goal. This is efficient because instead of recalculating the entire window state from scratch, you update the state incrementally as elements enter or leave. The reasoning is that since each element is added and removed at most once, the total number of operations remains linear. You use this when you need to maintain a running sum, count, or frequency map that represents the optimal slice of data, ensuring you only process each item the minimum number of times necessary to validate the condition.

def longest_substring_no_repeat(s):
    char_map = {}
    left = longest = 0
    for right in range(len(s)):
        # Contract window if repeat is found
        if s[right] in char_map and char_map[s[right]] >= left:
            left = char_map[s[right]] + 1
        char_map[s[right]] = right
        longest = max(longest, right - left + 1)
    return longest

Multi-Pointer Interleaving

Sometimes a problem requires comparing more than two elements or merging multiple sorted structures simultaneously, which leads to the multi-pointer pattern. While similar to the basic two-pointer approach, this involves keeping one pointer for each input structure to compare their current elements. By always selecting the smallest or largest element among the current pointer positions, you can effectively merge or compare multiple streams of data. The reasoning is that because each input is already sorted, the global minimum or maximum at any given time must be one of the elements currently pointed to. By advancing only the pointer that provided the 'winner' of the current comparison, you maintain the sorted order globally. This is the logic used in efficient merging algorithms, allowing you to combine multiple large inputs into a single organized output with linear time complexity while keeping space usage strictly managed and predictable.

def merge_sorted_lists(l1, l2):
    p1, p2 = 0, 0
    result = []
    # Compare and pick smallest until one list is exhausted
    while p1 < len(l1) and p2 < len(l2):
        if l1[p1] < l2[p2]:
            result.append(l1[p1])
            p1 += 1
        else:
            result.append(l2[p2])
            p2 += 1
    # Append remaining elements
    return result + l1[p1:] + l2[p2:]

Key points

  • Two-pointer techniques reduce time complexity by avoiding redundant nested loops.
  • The opposite-ends pattern is most effective when working with sorted data structures.
  • Fast and slow pointers provide a space-efficient way to detect cycles in linked lists.
  • Maintaining an invariant is the core logic that ensures the correctness of pointer movements.
  • Sliding window variations allow for efficient processing of dynamic subarrays and substrings.
  • Multi-pointer strategies allow merging multiple sorted streams into one in linear time.
  • Each pointer should only move forward to ensure the algorithm maintains a linear time complexity.
  • Choosing the right pointer strategy depends on whether the underlying data is sorted or contains cycles.

Common mistakes

  • Mistake: Modifying the collection while iterating. Why it's wrong: Changing list size during a two-pointer traversal causes indexing errors or skipped elements. Fix: Use a new list to store results or use indexing carefully to avoid removals.
  • Mistake: Miscalculating the loop termination condition. Why it's wrong: Using 'left <= right' when 'left < right' is required for problems like 'Pair Sum' leads to comparing an element with itself. Fix: Always verify if the pointers should meet or cross based on the problem logic.
  • Mistake: Failing to handle duplicate values. Why it's wrong: In problems like '3Sum', failing to skip duplicate elements leads to redundant calculations or incorrect output. Fix: Add an additional while loop to increment/decrement pointers until a unique value is found.
  • Mistake: Assuming pointers must move at the same speed. Why it's wrong: Some problems require 'fast and slow' pointers or conditional movement, not just simple bidirectional steps. Fix: Explicitly define the logic for when each pointer moves.
  • Mistake: Neglecting sorted order requirements. Why it's wrong: Two pointers often rely on the data being sorted to make greedy decisions; applying it to unsorted lists returns incorrect results. Fix: Sort the list first or ensure the algorithm accounts for unsorted input.

Interview questions

What is the core intuition behind the Two Pointers pattern in Python, and when should you consider using it?

The Two Pointers pattern is a technique used to iterate through a data structure, typically a list, using two variables that move toward each other or in the same direction to reduce time complexity. You should consider this pattern whenever you are working with sorted arrays or linked lists where you need to find pairs, subarrays, or subsets that satisfy specific criteria. By maintaining two pointers, we often avoid nested loops, which would result in O(n²) time complexity, allowing us to solve the problem in O(n) linear time instead. For instance, in Python, if you need to check if a sorted list contains a pair that sums to a target value, you can place one pointer at the start and one at the end, moving them inward based on the current sum compared to the target, which is far more efficient than checking every possible combination.

How does the 'Two Pointers' approach differ from a 'Nested Loops' approach when solving the 'Two Sum' problem in a sorted list?

When solving the Two Sum problem on a sorted list, a nested loops approach compares every single element with every other element, resulting in an O(n²) time complexity. This is inefficient because it ignores the sorted property of the input. Conversely, the Two Pointers approach utilizes the sorting: by placing a pointer at index 0 and another at index n-1, if the sum is too small, we increment the left pointer to increase the sum, and if it is too large, we decrement the right pointer. This allows us to find the pair in a single pass, achieving O(n) linear time complexity. The Two Pointers method is significantly more scalable for large datasets in Python, as it minimizes redundant comparisons by leveraging the inherent structure of the data.

Can you explain how to use Two Pointers to remove duplicates from a sorted Python list in-place?

To remove duplicates in-place, we use a 'slow' pointer and a 'fast' pointer. The slow pointer tracks the position of the last unique element found, while the fast pointer iterates through the list. In Python, you initialize the slow pointer at index 0. As the fast pointer moves, whenever you find a value that is not equal to the value at the slow pointer, you increment the slow pointer and update its position with the new unique value. This approach is powerful because it modifies the list without needing auxiliary space, resulting in O(1) space complexity and O(n) time complexity. Code-wise: 'if nums[fast] != nums[slow]: slow += 1; nums[slow] = nums[fast]' effectively collapses the list into its unique version.

How would you approach the 'Container With Most Water' problem using the Two Pointers pattern in Python?

The 'Container With Most Water' problem requires finding two lines that, together with the x-axis, form a container that holds the most water. The area is defined by the distance between pointers multiplied by the minimum of the two heights. We start with pointers at both ends of the list. We calculate the area, then move the pointer pointing to the shorter line inward, hoping to find a taller line that might compensate for the reduced width. By always discarding the shorter side, we systematically explore the most promising containers. This Python strategy is efficient because we only visit each element once, maintaining O(n) time complexity, whereas a brute-force approach would require checking every pair, leading to an unacceptable performance bottleneck for large input sizes.

In the context of the '3Sum' problem, why is sorting the array and using a fixed pointer combined with two pointers more effective than a simple brute-force approach?

The 3Sum problem asks for all unique triplets that sum to zero. Brute force would require three nested loops, giving us O(n³) complexity, which is prohibitive. By sorting the list first, we can iterate through the list using a fixed pointer 'i' and then treat the remaining subarray as a 2Sum problem using two pointers (left and right). As we iterate with 'i', we skip duplicate values to ensure unique triplets. This strategy reduces the complexity to O(n²). The effectiveness comes from the fact that once the list is sorted, the two-pointer step becomes a simple linear scan, and we can easily skip over duplicates in Python using a simple 'while' loop, which is much faster than managing a hash set of results.

How do you handle 'sliding window' problems where the window size is variable, and how does this relate to the Two Pointers pattern?

Variable sliding window problems are a specific application of the Two Pointers pattern where the 'left' and 'right' pointers define the boundaries of a window. We expand the window by moving the right pointer and contract it by moving the left pointer when a condition is violated (e.g., the sum exceeds a target). In Python, this is often implemented with a 'while' loop inside a 'for' loop. For example, to find the longest subarray with a sum less than K, we add elements to the window until the sum is too large, then subtract elements from the left until it fits again. This maintains O(n) time complexity because each element is added and removed from the window at most once, providing an optimal solution for dynamic window constraints.

All Python interview questions →

Check yourself

1. In a sorted array, if the sum of elements at the left and right pointers is less than the target value, what should you do?

  • A.Decrement the right pointer
  • B.Increment the left pointer
  • C.Increment both pointers
  • D.Move both pointers to the center
Show answer

B. Increment the left pointer
Incrementing the left pointer increases the total sum because the array is sorted. Decrementing the right pointer would decrease the sum, making it further from the target. Moving both or decrementing the right is illogical in a sorted search.

2. Why is the Two Pointer pattern generally more efficient than a nested loop approach for finding a pair sum?

  • A.It uses less memory than nested loops
  • B.It allows for O(n log n) sorting, while nested loops are always slower
  • C.It reduces the time complexity from O(n^2) to O(n)
  • D.It is always O(1) space complexity regardless of input
Show answer

C. It reduces the time complexity from O(n^2) to O(n)
Two pointers allow us to scan the array in a single pass after sorting, resulting in linear time complexity. Nested loops are quadratic O(n^2). While it does use O(1) space, that is not the primary reason for its efficiency relative to nested loops.

3. When using two pointers to remove duplicates from a sorted array in-place, what is the role of the 'slow' pointer?

  • A.It marks the position of the last unique element found
  • B.It iterates through the entire list to find new values
  • C.It skips over elements that are equal to the target
  • D.It keeps track of the total count of duplicates
Show answer

A. It marks the position of the last unique element found
The slow pointer maintains the boundary of the 'processed' unique list. The fast pointer searches for new values. Option B describes the fast pointer, while C and D are not standard functions of the slow pointer in this specific pattern.

4. What is the primary condition required to use the bidirectional two-pointer approach for finding pairs?

  • A.The list must contain only positive integers
  • B.The list must be sorted in ascending order
  • C.The list must be converted to a dictionary first
  • D.The list must have an even number of elements
Show answer

B. The list must be sorted in ascending order
The bidirectional approach relies on the property that moving a pointer inward predictably changes the sum (increasing from left, decreasing from right). This only holds if the list is sorted. Positive integers, dictionaries, and even lengths are not requirements.

5. If you are asked to 'square a sorted array' and return the result in sorted order, why is two-pointer the best approach?

  • A.It avoids the O(n log n) cost of sorting the squared values
  • B.Squaring makes the array unsorted, so pointers are needed to re-sort
  • C.It is faster than just squaring and calling .sort()
  • D.It prevents index out of bounds errors
Show answer

A. It avoids the O(n log n) cost of sorting the squared values
Squaring a sorted list (e.g., [-4, -1, 0, 3, 10]) results in values where the largest numbers are at the ends. Two pointers allow you to pick the largest squares from the ends and place them into a new array backwards in O(n) time, avoiding a full re-sort.

Take the full Python quiz →

← PreviousPython Coding Patterns — Sliding WindowNext →Python Coding Patterns — Recursion and Backtracking

Python

78 lessons, free to read.

All lessons →

Track your progress

Sign in to mark lessons done, score quizzes and keep notes.

Open in the app