On This Page

What Are They?

Two pointers is a technique where you use two index variables to traverse a data structure โ€” usually an array or string โ€” in a way that avoids checking every possible pair. Instead of the O(nยฒ) nested loop approach ("for each element, check every other element"), the pointers move strategically so each element is visited a small constant number of times.

The technique isn't a specific algorithm โ€” it's more of a pattern that shows up across dozens of different problems. What unites them is the core insight: by being clever about which direction to move each pointer, you can skip work that wouldn't lead to a better answer.

Where the Name Comes From

The "sliding window" name has roots in networking. TCP (Transmission Control Protocol, the protocol that makes reliable internet communication possible) uses a sliding window for flow control โ€” a range of sequence numbers representing packets that can be "in flight." The sender's window slides forward as acknowledgments arrive. The algorithmic technique borrows both the name and the mental model: a window of elements that slides through the data.

The Two Main Flavors

The sliding window variant deserves its own name because it's that common. The idea: maintain a window [left, right] that expands by advancing right and contracts by advancing left. You grow the window when you need more elements to satisfy a condition, and shrink it when the window violates a constraint.

A Third Flavor: Fast and Slow

There's a related technique where two pointers move at different speeds through a linked list. The slow pointer advances one node at a time; the fast pointer advances two nodes. If there's a cycle, the fast pointer laps the slow pointer and they meet. If there's no cycle, the fast pointer reaches the end. This is Floyd's Cycle Detection Algorithm (also called the tortoise and hare algorithm), published by Robert Floyd in 1967. It also finds the middle of a linked list in one pass โ€” when fast reaches the end, slow is at the midpoint.

How They Work

Converging Two Pointers

Start with left = 0, right = n-1. Check some condition involving arr[left] and arr[right]. Based on the result, move one pointer inward:

This works because the array is sorted. Moving left can only increase the left value. Moving right can only decrease the right value. You never need to revisit a discarded pointer position, so the total work is O(n).

Why can't we skip solutions? Consider two sum on a sorted array. If arr[left] + arr[right] < target, every pair (left, right'), where right' < right, also sums to less than the target (since arr[right'] โ‰ค arr[right]). So moving left forward is safe โ€” we haven't skipped any valid answer. A symmetric argument works for the too-large case.

Sliding Window โ€” Fixed Size

For "max sum of subarray of size k": start with a window covering the first k elements. Compute its sum. Slide the window one step right โ€” add the new element entering from the right edge, subtract the element leaving from the left edge. Each slide is O(1) work, so the whole thing is O(n).

The fixed window approach works whenever the window size is given (or can be determined) ahead of time. You're essentially computing a rolling aggregate โ€” sum, average, max, min, or count.

Sliding Window โ€” Variable Size

For "smallest subarray with sum โ‰ฅ target": expand right until the window satisfies the condition (sum โ‰ฅ target). Then shrink from left while the condition still holds, recording the minimum length along the way. When the condition breaks, expand right again.

The key insight people miss: left never moves backward, and right never moves backward. Each pointer traverses the array at most once. Even though there's an inner while loop for shrinking, the total number of left-advances across the entire algorithm is at most n. So the total work is O(n), not O(nยฒ).

The variable window is trickier because you need to decide when to expand and when to contract. The general template:

  1. Move right to expand the window
  2. Update your window state (add the new element to your count/sum/set)
  3. While the window violates a constraint, shrink from the left (update state, advance left)
  4. Record the answer if the current window is valid

Choosing the Right Direction

How do you know whether to use converging or sliding?

Interactive Visualization

Toggle between Two Pointers mode (finding a pair that sums to a target) and Sliding Window mode (finding the maximum sum subarray of a given size).

Two Pointers / Sliding Window
Mode: Two Pointers | Target Sum: -- | Status: Ready

Common Mistakes

โš ๏ธ Mistake #1: Using "if" instead of "while" for the shrink step In variable-size sliding window, you contract the window with a while loop, not an if. The window might need to shrink by multiple elements before the constraint is restored. Writing if only shrinks once and can leave the window in an invalid state.
โš ๏ธ Mistake #2: Forgetting to sort before using converging two pointers Converging two pointers only works on sorted data. If the problem gives unsorted input and asks for a pair with a target sum, either sort first (O(n log n) time, O(1) extra space) or use a hash map (O(n) time, O(n) space). Two pointers on unsorted data gives wrong answers.
โš ๏ธ Mistake #3: Not updating window state correctly during shrink When you remove the leftmost element from your window, you need to undo its effect. If you're tracking character frequencies, decrement the count. If you're tracking the number of distinct characters, check whether the count drops to zero. Forgetting the undo step makes your window state inconsistent.
โš ๏ธ Mistake #4: Off-by-one in window size calculations A window from index left to index right (inclusive) has right - left + 1 elements, not right - left. This off-by-one error is incredibly common and leads to answers that are consistently off by 1.
โš ๏ธ Mistake #5: Trying two pointers when the problem needs a hash map Two pointers find one pair efficiently. If the problem asks for all pairs, or the data isn't sortable, or you need O(1) lookup for seen values, a hash map is the right tool. Don't force two pointers where they don't fit.

Complexity Analysis

Both techniques achieve O(n) by ensuring each pointer only moves forward โ€” never backward. You're visiting each element at most twice (once by each pointer).

PatternTimeSpaceWhen To Use
Two Sum (sorted)O(n)O(1)Pair finding on sorted data
Three SumO(nยฒ)O(1)Sort + nested two pointers
Fixed-size windowO(n)O(1)Max/min sum of size k
Variable-size windowO(n)O(1)*Smallest subarray with sum โ‰ฅ S
Window + hash mapO(n)O(k)Distinct chars, frequency constraints
Remove duplicatesO(n)O(1)In-place array manipulation
Fast/slow pointersO(n)O(1)Cycle detection, linked list middle

* Some sliding window problems need a hash map or counter inside the window, adding O(k) space where k is the alphabet/value size.

Implementation

Two Sum II (Sorted Array)

Python

def two_sum_sorted(numbers, target):
    """Find two numbers in a sorted array that sum to target.
    Returns 1-indexed positions. O(n) time, O(1) space."""
    left, right = 0, len(numbers) - 1

    while left < right:
        current = numbers[left] + numbers[right]

        if current == target:
            return [left + 1, right + 1]  # 1-indexed result
        elif current < target:
            left += 1   # Sum too small โ€” need a bigger left value
        else:
            right -= 1  # Sum too large โ€” need a smaller right value

    return []  # No pair found
      

Container With Most Water (Converging)

Python

def max_area(height):
    """Find two lines that form a container holding the most water.
    The greedy insight: always move the shorter side inward.
    Moving the taller side can only decrease width without 
    increasing the limiting height, so it can never help."""
    left, right = 0, len(height) - 1
    best = 0

    while left < right:
        # Area = width ร— min(left_height, right_height)
        w = right - left
        h = min(height[left], height[right])
        best = max(best, w * h)

        # Move the shorter side โ€” it's the bottleneck
        if height[left] < height[right]:
            left += 1
        else:
            right -= 1

    return best
      

Max Sum Subarray of Size K (Fixed Window)

Python

def max_sum_subarray(arr, k):
    """Find the maximum sum of any contiguous subarray of size k.
    O(n) time โ€” each element is added once and removed once."""
    # Build the first window
    window_sum = sum(arr[:k])
    max_sum = window_sum

    # Slide: add the new right element, drop the old left element
    for i in range(k, len(arr)):
        window_sum += arr[i] - arr[i - k]
        max_sum = max(max_sum, window_sum)

    return max_sum
      

Minimum Size Subarray Sum (Variable Window)

Python

def min_subarray_len(target, nums):
    """Find the shortest subarray with sum >= target.
    Variable-size sliding window. O(n) time."""
    left = 0
    current_sum = 0
    min_len = float('inf')

    for right in range(len(nums)):
        current_sum += nums[right]  # Expand window

        # Shrink from left while condition holds
        while current_sum >= target:
            min_len = min(min_len, right - left + 1)
            current_sum -= nums[left]
            left += 1  # Contract window

    return min_len if min_len != float('inf') else 0
      

Longest Substring Without Repeating Characters (Variable Window + Hash Map)

Python

def length_of_longest_substring(s):
    """Find the longest substring with all unique characters.
    Window tracks the current duplicate-free range.
    Hash map remembers each character's most recent index."""
    seen = {}       # char โ†’ most recent index
    left = 0
    max_len = 0

    for right, char in enumerate(s):
        # If char was seen and is inside our current window, shrink
        if char in seen and seen[char] >= left:
            left = seen[char] + 1  # Jump left past the duplicate

        seen[char] = right
        max_len = max(max_len, right - left + 1)

    return max_len
      
JavaScript

function lengthOfLongestSubstring(s) {
  const seen = new Map();
  let left = 0, maxLen = 0;

  for (let right = 0; right < s.length; right++) {
    const char = s[right];
    // Shrink window if duplicate found inside it
    if (seen.has(char) && seen.get(char) >= left) {
      left = seen.get(char) + 1;
    }
    seen.set(char, right);
    maxLen = Math.max(maxLen, right - left + 1);
  }

  return maxLen;
}
      

Floyd's Cycle Detection (Fast/Slow Pointers)

Python

def has_cycle(head):
    """Detect if a linked list has a cycle using Floyd's algorithm.
    Slow pointer moves 1 step, fast moves 2 steps.
    If they meet, there's a cycle. O(n) time, O(1) space."""
    slow = fast = head

    while fast and fast.next:
        slow = slow.next          # 1 step
        fast = fast.next.next     # 2 steps
        if slow is fast:
            return True           # They met โ€” cycle exists

    return False  # fast hit the end โ€” no cycle

def find_cycle_start(head):
    """Find the node where the cycle begins.
    After detection, reset one pointer to head and advance both
    at speed 1. They meet at the cycle start. (Math: look up
    Floyd's algorithm proof for why this works.)"""
    slow = fast = head

    # Phase 1: detect cycle
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow is fast:
            break
    else:
        return None  # No cycle

    # Phase 2: find entry point
    slow = head
    while slow is not fast:
        slow = slow.next
        fast = fast.next  # Both move at speed 1 now
    return slow
      

Real-World Example: Rate Limiter

APIs often limit requests to "at most N requests per T seconds." This is a sliding window problem. You maintain a window of timestamps for recent requests and check whether adding a new one would exceed the limit.

Python

from collections import deque
import time

class SlidingWindowRateLimiter:
    """Allow at most max_requests in window_seconds.
    Uses a deque (double-ended queue) as a sliding window
    of request timestamps."""

    def __init__(self, max_requests, window_seconds):
        self.max_requests = max_requests
        self.window_seconds = window_seconds
        self.timestamps = deque()  # Sorted by time (oldest first)

    def allow_request(self):
        now = time.time()

        # Shrink window: remove timestamps outside the window
        # This is the "slide left" operation
        while self.timestamps and now - self.timestamps[0] > self.window_seconds:
            self.timestamps.popleft()

        if len(self.timestamps) < self.max_requests:
            self.timestamps.append(now)  # Expand window: add new request
            return True
        return False  # Rate limited

# Usage: 5 requests per 10 seconds
limiter = SlidingWindowRateLimiter(max_requests=5, window_seconds=10)

for i in range(8):
    allowed = limiter.allow_request()
    print(f"Request {i+1}: {'โœ“ Allowed' if allowed else 'โœ— Rate limited'}")
    # First 5 pass, next 3 are blocked (until 10 seconds elapse)
      

The deque acts as the sliding window. Old timestamps "fall off" the left as they age out of the window. New timestamps enter from the right. At any moment, the deque length tells you how many requests are in the current window. This is exactly the same pattern as a variable-size sliding window on an array โ€” just applied to timestamps instead of indices.

Interview Patterns

๐Ÿ’ก The "shrink from left" instinct In variable-size sliding window problems, the hardest part is knowing when to shrink. The template: expand right unconditionally, then while the window violates a constraint, increment left. Don't overthink โ€” the "while" loop handles it. If you're writing "if" instead of "while" for the shrink step, you probably have a bug.
๐Ÿ’ก Two pointers need sorted input Converging two-pointer solutions almost always require sorted data. If the array isn't sorted and the problem asks for pairs, you probably want a hash map instead (O(n) time, O(n) space). Two pointers trade space for the sorting step: O(n log n) time, O(1) extra space.
๐Ÿ’ก The "at most K" trick "Subarrays with exactly K distinct elements" is hard directly. But "at most K" minus "at most K-1" gives you "exactly K." Use this decomposition: solve the "at most" version with sliding window, then subtract. This trick comes up in problems like "Subarrays with K Different Integers."
๐Ÿ’ก Fast/slow pointers for linked lists Two pointers moving at different speeds: one moves 1 step, the other moves 2 steps. Floyd's cycle detection uses this โ€” if there's a cycle, they meet. Also useful for finding the middle of a linked list in one pass (when fast reaches the end, slow is at the midpoint) and detecting palindromes in linked lists.
๐Ÿ’ก Three Sum = Sort + Two Pointers Fix one element (loop through the array). For each fixed element, use converging two pointers on the remaining portion to find pairs that sum to (target - fixed). Sort the array first. Skip duplicates by checking if the current element equals the previous. This gives O(nยฒ) โ€” much better than the O(nยณ) brute force.
๐Ÿ’ก Sliding window with a monotonic deque "Maximum of each window of size k" seems to need O(k) per window, but a monotonic deque (a double-ended queue where elements are always in decreasing order) gives O(1) amortized per element. Each element enters and leaves the deque at most once. The front always holds the current window's maximum. This turns an O(nk) brute force into O(n).

Practice Problems