DEV Community

Matteo
Matteo

Posted on

The Two-Pointer Technique: From Naive Loops to Elegant O(N) Solutions

1. The Hook: Two Friends on a Number Line

Imagine two friends walking along a line of numbers. Sometimes they walk together toward the same direction, sometimes they start at opposite ends and meet in the middle, and other times one runs faster than the other to check what’s ahead.

That’s the essence of the two-pointer technique, one of the simplest yet most powerful tools in algorithm design.
When we face problems involving pairs, ranges, or sequences (like substrings or subarrays), our first instinct is often brute force: two nested loops scanning every possible combination. It works, but it’s slow. The two-pointer approach is a smarter way to explore the same search space without redundancy, often turning an O(N²) problem into O(N).

Today’s post will guide you through the concept, show real-world algorithmic applications, and end with a quick review that helps you recognize when and how to use each variant.


2. The Naive Approach: Nested Loops Everywhere

Let’s start with the naive way we typically approach “pair” or “window” problems.
Take a simple example: finding the longest substring without repeating characters in a given string.

The straightforward way is to try all possible substrings and check each one for duplicates.

def longest_unique_substring(s):
    n = len(s)
    best = 0
    for i in range(n):
        seen = set()
        for j in range(i, n):
            ch = s[j]
            if ch in seen:
                break
            seen.add(ch)
            best = max(best, j - i + 1)
    return best
Enter fullscreen mode Exit fullscreen mode

This double loop checks every substring and re-examines most of the same characters multiple times.

The time complexity? O(N²). The space complexity? O(min(n, charset size)).
It works, but it’s inefficient for large inputs.


3. The Core Idea: What Are Two Pointers?

The two-pointer technique uses two indices (left and right, or i and j) to traverse a sequence in a coordinated way.
The idea is to maintain a relationship between them (a window or pair) and move one or both intelligently based on conditions.

There are three main ways we can use this pattern:

Think of it as teaching your loops to communicate.
Instead of two independent loops, you now have two synchronized variables that work together to minimize redundant work.


4. Same Direction: The Sliding Window Pattern

Let’s revisit our substring example, but this time we’ll use a sliding window, one of the most versatile two-pointer strategies.

Problem
Find the length of the longest substring without repeating characters.

Idea

  • We’ll use two pointers left and right to represent the current window of unique characters.
  • We expand right to include new characters and shrink left whe never we find a duplicate.

Implementation

def lengthOfLongestSubstring(s):
    if not s:
        return 0

    last = {}          # char -> last index
    left = 0
    best = 0

    for right, ch in enumerate(s):
        if ch in last and last[ch] >= left:
            left = last[ch] + 1
        last[ch] = right
        if right - left + 1 > best:
            best = right - left + 1

    return best
Enter fullscreen mode Exit fullscreen mode

How It Works

  • right moves forward exploring new characters.
  • On a duplicate that lies inside the current window, jump left to last[ch] + 1 instead of sliding one by one.
  • Update last[ch] = right, then recompute the window width.
  • Each index is processed once and left only moves forward

Complexity
Time: O(N)
Space: O(min(N, charset size))

Key Insight
The sliding window turns what used to be two nested loops into a single, where each index is processed once, and left only moves forward in jumps.


5. Opposite Directions: The Converging Pointer Pattern

Sometimes, the input array is sorted or can be treated as ordered. In those cases, we can exploit symmetry by starting from both ends and moving inward.

Problem
Find the container that holds the most water between vertical lines.
Given an array height[i] representing vertical lines, the area between two lines i and j is min(height[i], height[j]) * (j - i).

Naive Approach
Check every pair (i, j), that’s O(N²) again.

Optimized Two-Pointer Approach

def maxArea(height):
    left, right = 0, len(height) - 1
    best = 0

    while left < right:
        area = (right - left) * min(height[left], height[right])
        best = max(best, area)

        # Move the smaller line inward
        if height[left] < height[right]:
            left += 1
        else:
            right -= 1

    return best
Enter fullscreen mode Exit fullscreen mode

Why It Works

  • The area is limited by the shorter line.
  • Moving the taller one won’t help because the width shrinks but the limiting height stays the same.
  • By always moving the shorter pointer, we maximize the chance of finding a better area.

Complexity
Time: O(N)
Space: O(1)


6. Different Speeds: The Fast–Slow Pointer Pattern

Sometimes both pointers start at the same point but move at different speeds.
This approach is particularly powerful for linked list problems or anything involving cycles or midpoints.

Problem
Detect if a linked list contains a cycle.

Algorithm (Floyd’s Cycle Detection)

def hasCycle(head):
    slow = fast = head

    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow == fast:
            return True

    return False
Enter fullscreen mode Exit fullscreen mode

Intuition

  • The fast pointer moves two steps at a time, while slow moves one.
  • If there’s no cycle, fast will hit the end.
  • If there is a cycle, fast will eventually “lap” slow and they’ll meet.

Complexity
Time: O(N)
Space: O(1)


7. Quick Comprehension Check

Question:
You’re given a sorted array nums and a target t. How can you check if any two numbers sum up to t in O(N)? Return the indices of the two numbers, index1 and index2, added by one as an integer array [index1, index2] of length 2.

Hint:
Start one pointer at the beginning and one at the end. Move inward based on whether the sum is too large or too small.

Answer:

def twoSumSorted(nums, target):
    left, right = 0, len(nums) - 1
    while left < right:
        s = nums[left] + nums[right]
        if s == target:
            return [left + 1, right + 1]
        elif s < target:
            left += 1
        else:
            right -= 1
    return False
Enter fullscreen mode Exit fullscreen mode

8. Beyond the Basics: Variants and Edge Cases

Choosing the Right Strategy

  • When dealing with substrings or subarrays, use sliding windows.
  • When working with sorted data, use converging pointers.
  • When traversing linked lists or cycles, use fast–slow pointers.
  • When symmetry matters (like palindromes), use center expansion.

9. Final Review

The two-pointer technique is the Swiss Army knife of algorithmic problem-solving.
It simplifies a vast class of problems by controlling how two indices move through data.

In summary:

What makes this pattern elegant is that the logic stays simple, you move pointers intelligently based on the problem’s constraints, ensuring each element is visited only as often as necessary.


10. The Hook for Tomorrow

We’ve mastered how two pointers traverse data efficiently.
Next, we’ll explore how algorithms search — not by brute force, but by systematically narrowing the space of possibilities.

In the next chapter, we’ll begin a three-part deep dive into searching algorithms:

  • Searching in arrays and numbers: from binary search and search on answer patterns to bitonic arrays and other specialized strategies.
  • Searching in strings: exploring Z-algorithm, KMP, Rabin–Karp, and hybrid methods that combine pattern matching with binary search.
  • Searching in trees and graphs: covering BFS, DFS, and how these foundational traversals become the backbone of pathfinding and state-space exploration.

By the end of this trilogy, you’ll see how every kind of search (numeric, textual, or structural) is just a different way of asking the same question: where do I look next?

Top comments (0)