Back to Blog
Coding PatternsArraysDSAInterview Tips

Two Pointers vs Sliding Window: How to Choose

Amit Singh

Amit Singh

Author

August 11, 2026
12 min read

Ask one question: does the answer have to be a contiguous run of elements whose aggregate you can update as the range grows and shrinks? If yes, you want the sliding window algorithm. If the answer is a pair of positions and everything between them is irrelevant, you want two pointers proper. Sorted input pushes toward the pair; the word contiguous pushes toward the window.

That is the whole decision. The rest of this post is why the rule works, the phrases that trigger each side, the complexity argument that differs even though both come out linear, and one problem worked both ways where only one answer is correct.

The full breakdown of either pattern — every variant, template and edge case — lives in its own post: two pointers and the sliding window algorithm. This one is only the choice between them.

The structural difference, stated precisely

Most write-ups say the sliding window algorithm "uses two pointers" and stop there, which is true and useless. The precise relationship:

The sliding window algorithm is the same-direction two-pointer case in which the pair bounds a contiguous range and you maintain state describing that range incrementally. Two pointers proper is about a pair of positions — usually converging from opposite ends — with no state maintained about anything between them.

Two things follow, and both are worth saying out loud in an interview.

Direction is not the discriminator. The in-place partition — a write index trailing a read index for dedup, Move Zeroes, or Sort Colors — moves both indices the same way and is not a window. Nothing about the region between write and read is tracked; it is scratch space. What makes something a window is that the enclosed range is the object under study and its aggregate survives from one step to the next.

State is the discriminator. A window carries a running sum, a frequency map, a distinct-count, a deque of candidates, and updating that state at the two edges instead of recomputing it is what buys the linear time. Converging pointers carry nothing: nums[lo] + nums[hi] is recomputed every iteration and costs O(1) because it touches two cells.

So the two questions are: is the answer a range or a pair, and is there anything about the range worth remembering. The same distinction as code, with the bodies abstracted so the shapes are visible:

// Two pointers proper: a PAIR. Nothing between lo and hi is remembered.
while (lo < hi) {
    int probe = combine(nums[lo], nums[hi]);            // depends on the two ends only
    if (tooSmall(probe)) lo++; else hi--;               // one end retires per comparison
}

// Sliding window algorithm: a RANGE, plus state describing its contents.
for (int right = 0; right < n; right++) {
    state.admit(nums[right]);                           // exactly one element in
    while (invalid(state)) state.evict(nums[left++]);   // zero or more elements out
    best = Math.max(best, right - left + 1);            // the range itself is the answer
}

The state object in the second skeleton has no counterpart in the first. That absence is the entire difference.

Two walks, one array
nums = [2, 3, 1, 2, 4, 3] — the same six values on both sidesOnly the pointer behaviour differs: a pair of positions on the left, a contiguous range on the right.Converging pairlo and hi start at the two ends and walk inwardlohi231243lohia[lo]=2 · a[hi]=3Only a[lo] and a[hi] are read — the interior is skipped231243lohia[lo]=3 · a[hi]=4Both ends step inward; still just two cells in play231243lohia[lo]=1 · a[hi]=2They meet and the scan ends — at most n − 1 stepsPointers move inward; nothing between them is tracked.State carried: none. The answer is a pair of positions.Sliding windowleft and right both start at 0 — shortest run with sum ≥ 7leftright231243leftrightsum = 6Grow: right++ admits a value, sum < 7 so keep going231243leftrightsum = 8Valid: sum ≥ 7, so record this run — length 4231243leftrightevictedsum = 6Shrink: left++ evicts a[0]=2 and the sum drops to 6Pointers move rightward; the shaded span is the answer.State carried: a running sum, updated at the two edges.
The same six values on both sides. On the left the pointers close in on each other and only the two cells under them are ever compared — the span between them carries no state. On the right both pointers travel rightward and the shaded span between them — together with its running sum — is the thing being computed.

Watch what the interiors do. On the left the two pointers move toward each other and nothing between them is tracked — every cell gets compared as a pointer passes over it, but no state about the span survives the step, and the answer is the two cells the pointers land on. On the right they move the same direction and the shaded span between them is the answer, carrying an aggregate that persists from step to step. Same two indices walking the same array; entirely different object under study.

Recognition: the phrase in the problem decides it

The phrase in the problemWhich patternWhy
"subarray", "substring", "consecutive elements"Sliding window algorithmAdjacency is part of the answer, so the answer is a range and ranges need maintained state
"sorted array" + "find a pair"Two pointers, convergingOrdering licenses the elimination; the elements between the pair are never inspected
"longest/shortest ... such that ..." over a subarraySliding window algorithm, variableThe constraint decides the length, so both edges must move and the aggregate must be maintained
"triplet", "quadruplet" summing to a targetTwo pointers, inside a loopFix one element, converge on the rest — a pair problem wearing a bigger hat
"in place", "O(1) extra space", partition, dedupeTwo pointers, same-directionA write index trails a read index; the region between them is scratch, not a window
"every window of size k", "each block of k"Sliding window algorithm, fixedThe length is given; only the aggregate has to move
"count the subarrays where ..."Sliding window algorithmCounting ranges is a range problem; right - left + 1 counts every valid one ending at right
Palindrome check, reverse in place, mirrored compareTwo pointers, convergingMirrored positions retire together and nothing between them is aggregated
Linked list cycle, middle node, k-th from the endTwo pointers, fast & slowA speed or index gap is a measurement between two positions, not a range
"at most K distinct/odd/zeros"Sliding window algorithmAt-most constraints are downward-closed, which is what makes shrinking from the left legal
Unsorted array, "find a pair summing to target"Neither — hash mapConverging needs order; a window needs contiguity. This problem offers neither
Negatives present, "subarray sums to exactly k"Neither — prefix sums + hash mapA larger range can hold a smaller sum, so there is no direction to move

The last two rows are the ones that separate people who understand the patterns from people who have collected them. Both look like two-index problems and neither is.

Both are linear, for different reasons

This is the complexity question that catches people: "O(n)" is the right answer for both, and the justification is not the same one.

Converging two pointers is O(n) directly. On a sorted array with lo < hi, if nums[lo] + nums[hi] < target then nums[lo] cannot reach the target with any index still in play, because nums[hi] is the largest partner it has left. One comparison retires that element along with every pair it still had. Each iteration shrinks the interval by exactly one, so the loop body runs at most n − 1 times. Nothing is amortised; the bound is a direct count of iterations.

The sliding window algorithm is O(n) amortised. The outer loop admits each element exactly once, which is n steps. The inner shrink loop looks like it makes this quadratic, and does not, because it is bounded globally rather than per iteration: left starts at 0, never moves backwards, and cannot exceed n, so every execution of the shrink body across the entire run is one of at most n total advances. Each element enters once and leaves at most once — about 2n state updates for the whole scan.

The practical difference: the window's bound assumes constant-time state updates. Track a scalar — a running sum, a missing counter, a map size — and you keep O(n). Compare two frequency maps in full on every step and you have quietly bought an alphabet-sized factor, O(n·Σ), with code that still looks linear. Converging pointers have no such trap because they carry no state to update.

And one number to keep honest: 3Sum is O(n²), not O(n). It fixes one element and runs the converging pair scan inside a loop over that element, plus an O(n log n) sort the quadratic term absorbs. "Two pointers, so linear" is true of the pair scan and false of the algorithm you actually wrote. Quote the loop you typed.

AxisTwo pointers (converging)Sliding window algorithm
What the pair meansTwo candidate positionsThe two ends of one contiguous range
Maintained stateNoneSum, frequency map, distinct count, deque
PreconditionSorted input (or positional progress)Contiguity matters; validity downward-closed
MovementInward, one end per comparisonright always forward, left forward to fix
Time argument≤ n − 1 iterations, direct2n state updates, amortised
Typical extra spaceO(1)O(min(n, Σ)) for the state
Breaks whenInput is unsortedThe answer need not be contiguous

One problem, both ways

Minimum Size Subarray Sum. Given positive integers and a target, return the length of the shortest contiguous subarray whose sum is at least the target, or 0 if none exists.

The word contiguous and the word shortest together say: range, with a constraint deciding the length. Variable-size window.

def min_subarray_len(target, nums):          # nums are positive
    left = total = 0
    best = float("inf")
    for right, x in enumerate(nums):
        total += x                           # admit the entering element
        while total >= target:               # valid — record, then keep tightening
            best = min(best, right - left + 1)
            total -= nums[left]
            left += 1
    return 0 if best == float("inf") else best
static int minSubarrayLen(int target, int[] nums) {
    int left = 0, best = Integer.MAX_VALUE;
    long total = 0;                                   // long: the running sum can overflow
    for (int right = 0; right < nums.length; right++) {
        total += nums[right];
        while (total >= target) {
            best = Math.min(best, right - left + 1);
            total -= nums[left++];
        }
    }
    return best == Integer.MAX_VALUE ? 0 : best;
}

Now the two-pointer attempt. There is only one way to get converging pointers onto this problem — sort the array so the elimination argument becomes available — and that step is where it dies:

def min_subarray_len_wrong(target, nums):
    nums = sorted(nums)                      # adjacency is destroyed on this line
    lo, hi = 0, len(nums) - 1
    while lo < hi:
        if nums[lo] + nums[hi] >= target:
            return 2                         # "two elements reach the target"
        lo += 1                              # sum too small -> retire the small end
    return 0

One detail before the results, because it looks like a bug and isn't. Only lo moves in that second function. That is correct for this predicate: we are asking "does any pair reach the target", and nums[hi] is always the largest partner available, so when nums[lo] + nums[hi] falls short, lo can never reach it with anything and retires. Nothing forces hi down. The full two-sided convergence appears when you search for an exact sum — then an overshoot retires hi too, which is the version the elimination argument above describes. Half the loop moving is a property of the question, not a shortcut.

Which is the real tell, and it is worth pausing on: because hi never moves and the array is sorted, nums[lo] + nums[hi] only grows as lo advances, so the entire loop is equivalent to the single test nums[n-2] + nums[n-1] >= target. It is an O(n) scan performing an O(1) check. If only one pointer ever moves, the pair framing is decorative — a sign you have reached for the pattern rather than the question.

Run both on nums = [5, 1, 1, 5], target = 10.

ApproachResultWhat it actually found
Sliding window algorithm4[5, 1, 1, 5] — the only contiguous run reaching 10
Two pointers after sort2The two 5s, which sit at indices 0 and 3 and are not neighbours

Every contiguous subarray of length 2 here sums to 6, 2 or 6. The pair the sorted scan found is a perfectly good pair and a nonexistent subarray. This is the failure mode to hold onto: it does not crash, it does not loop forever, it returns a small plausible integer, and it answers a question nobody asked.

Now the mirror image. Two Sum II — a sorted array, return the indices of the two values that sum to the target. The answer is a pair, so try a window on it and see what happens:

def two_sum_sorted_wrong(nums, target):
    left = total = 0
    for right, x in enumerate(nums):
        total += x
        while total > target:
            total -= nums[left]
            left += 1
        if total == target:
            return [left, right]             # a RANGE, not a pair
    return []

On nums = [1, 3, 5, 8], target = 9 this returns [0, 2] — the range 1 + 3 + 5. The answer is indices 0 and 3, the pair 1 + 8. A window cannot name a non-adjacent pair at all: the only thing it can report is the two ends of a range whose entire contents were summed. It found something, so nothing looks wrong.

Both failures are silent, which is the argument for choosing deliberately rather than by reflex. The correct converging solution to this one is a half-dozen lines, in the two pointers post.

The two failure modes worth memorising

Reaching for a window when the answer is not contiguous. The tell is that you find yourself wanting to skip an element inside the window. You cannot — a window has no notion of "keep the ends and drop the middle", and any problem that needs that is a pair problem, a hash-map problem, or dynamic programming. Sanity check before you commit: is the object I must return a run of neighbours, or a set of positions?

Reaching for converging pointers on unsorted input. The elimination argument is built entirely on ordering; without it, moving a pointer tells you nothing about what you ruled out. The code runs, terminates, and produces a wrong answer that looks right on the sample input. If the array is unsorted and you need a pair, the answer is a hash map at O(n) time and O(n) space — and if you sort first to reach for pointers, you have paid O(n log n), lost the original indices, and, on any subarray question, lost the problem.

Both of these are the same defect underneath: applying a pattern whose precondition you never checked. Two pointers converging needs order. A window needs contiguity to be part of the answer.

Where both patterns stop: negative numbers

The variable-size window rests on a precondition almost nobody states. Validity must be downward-closed — if a range is valid, every range inside it must also be valid. That is what licenses "the window went invalid, so evicting from the left is the only move that can help."

With non-negative values and a sum constraint, this holds for a simple reason: the sum is monotone in window length. Growing the range can only raise the sum, shrinking it can only lower the sum, so "shrink until valid again" is a well-defined instruction. Introduce a single negative number and that collapses. Evicting a −5 makes the sum go up. A range being invalid now tells you nothing about which direction to move, and the shrink step is not merely inefficient, it is unsound.

The right tool then is a running prefix sum plus a hash map of the prefixes you have already seen:

from collections import defaultdict

def subarray_sum_equals_k(nums, k):          # negatives allowed
    seen = defaultdict(int)
    seen[0] = 1                              # the empty prefix, before any element
    running = count = 0
    for x in nums:
        running += x
        count += seen[running - k]           # every earlier prefix that closes a k-sum here
        seen[running] += 1
    return count

The subarray (i, j] sums to k exactly when prefix[j] − prefix[i] = k, so at each j you ask how many earlier prefixes equal running − k. O(n) time, O(n) space, and it never needed monotonicity — which is precisely why it survives the negatives that kill the window. Saying "this is not a window problem, because with negatives the shrink step loses its justification" is worth more in an interview than solving the non-negative version quickly.

The 20-second decision procedure

  1. What must I return? A subarray, substring, length, or count of ranges → window. A pair or triplet of positions or values, a boolean, or a rearranged array → two pointers.
  2. Is there state worth carrying? A sum, a frequency map, a distinct count → window. If the answer depends only on the two cells the pointers currently touch, there is nothing to carry, and you are on the pair side.
  3. Is the precondition actually there? Pairs need sorted input. Windows need the aggregate to move monotonically with the range — check for negatives before you write the shrink loop.

Answer those three out loud before typing. The naming is itself part of what is being assessed: a candidate who says "this says contiguous and asks for the shortest, so it is a variable-size window recorded inside the shrink loop" has already shown more than one who silently produces working code.

Both patterns are among the seventeen shapes in the complete DSA patterns guide, which has the recognition signal for the other fifteen. For the depth on each — every variant, the edge cases, the dedup discipline — go to the two pointers breakdown and the sliding window breakdown. If you would rather be corrected in real time than read about it, that is what the live DSA course is for: every session ends with you defending the choice you made, not the code you wrote.

More interview patterns: Binary Search · Dynamic Programming · Graph Algorithms

One email when the next pattern breakdown lands, and nothing else — subscribe here.

Written by Amit Singh, Senior SDE at Amazon and a Claude Certified Architect. He teaches the DSA cohort at AlgoEngineer.

Frequently asked questions

Is the sliding window algorithm just a special case of two pointers?
Structurally yes, and the containment is worth stating precisely. A window is the same-direction case in which the two indices bound a contiguous range and you maintain state describing that range incrementally. What makes it a window is not the direction of travel — the in-place read/write partition also moves both indices forward and is not a window — it is that the enclosed range is the object you are reasoning about and its state survives from step to step.
Are two pointers and the sliding window algorithm linear for the same reason?
No, and interviewers do ask. Converging two pointers is O(n) because each comparison retires one endpoint together with every partner it had left, so the loop can run at most n − 1 times — a direct bound, not an amortised one. The window is O(n) amortised: the left index never moves backwards and cannot exceed n, so across the whole scan every element is admitted once and evicted at most once. Same exponent, two different arguments.
What tells me the answer has to be contiguous?
The output, not the input. If the thing you must return is a subarray, a substring, a length, or a count of ranges, adjacency is part of the answer and you need a window. If the output is a pair of indices, a boolean, a rearranged array, or a triplet of values, the elements between the two positions are irrelevant and you are in two-pointer territory. The words to look for are "subarray", "substring", "window" and "consecutive".
Can I sort the input and then use two pointers on a subarray problem?
No — sorting destroys adjacency, which is exactly the property a subarray question is asking about. After a sort, the two elements your pointers land on may have been at opposite ends of the original array, so any length or range you report describes a sequence that never existed. Sorting is free for pair and triplet problems because those never claimed the elements were neighbours; it is fatal the moment the answer must be a contiguous run.
Which pattern handles negative numbers in a subarray-sum problem?
Neither, and the reason differs for each. The shrink step of a window is only sound if the sum falls when you evict from the left, which needs non-negative values. Converging pointers handle negatives perfectly well on a sorted array — on [-5, -2, 1, 4] targeting -1, the pair scan finds -5 and 4 without difficulty — but a subarray problem forbids sorting, because reordering destroys the contiguity the question is about. So it is the contiguity requirement that rules out two pointers here, not the negatives. For "subarray sums to exactly k" with negatives present, the correct tool is a running prefix sum with a hash map of previously seen prefixes, which is O(n) time and O(n) space.

Ready to Ace Your Interviews?

Live, cohort-based interview prep taught by a working FAANG engineer — weekly mock interviews, lifetime access, and a 7-day money-back guarantee.