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.
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 problem | Which pattern | Why |
|---|---|---|
| "subarray", "substring", "consecutive elements" | Sliding window algorithm | Adjacency is part of the answer, so the answer is a range and ranges need maintained state |
| "sorted array" + "find a pair" | Two pointers, converging | Ordering licenses the elimination; the elements between the pair are never inspected |
| "longest/shortest ... such that ..." over a subarray | Sliding window algorithm, variable | The constraint decides the length, so both edges must move and the aggregate must be maintained |
| "triplet", "quadruplet" summing to a target | Two pointers, inside a loop | Fix one element, converge on the rest — a pair problem wearing a bigger hat |
| "in place", "O(1) extra space", partition, dedupe | Two pointers, same-direction | A 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, fixed | The length is given; only the aggregate has to move |
| "count the subarrays where ..." | Sliding window algorithm | Counting ranges is a range problem; right - left + 1 counts every valid one ending at right |
| Palindrome check, reverse in place, mirrored compare | Two pointers, converging | Mirrored positions retire together and nothing between them is aggregated |
| Linked list cycle, middle node, k-th from the end | Two pointers, fast & slow | A speed or index gap is a measurement between two positions, not a range |
| "at most K distinct/odd/zeros" | Sliding window algorithm | At-most constraints are downward-closed, which is what makes shrinking from the left legal |
| Unsorted array, "find a pair summing to target" | Neither — hash map | Converging needs order; a window needs contiguity. This problem offers neither |
| Negatives present, "subarray sums to exactly k" | Neither — prefix sums + hash map | A 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.
| Axis | Two pointers (converging) | Sliding window algorithm |
|---|---|---|
| What the pair means | Two candidate positions | The two ends of one contiguous range |
| Maintained state | None | Sum, frequency map, distinct count, deque |
| Precondition | Sorted input (or positional progress) | Contiguity matters; validity downward-closed |
| Movement | Inward, one end per comparison | right always forward, left forward to fix |
| Time argument | ≤ n − 1 iterations, direct | 2n state updates, amortised |
| Typical extra space | O(1) | O(min(n, Σ)) for the state |
| Breaks when | Input is unsorted | The 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.
| Approach | Result | What it actually found |
|---|---|---|
| Sliding window algorithm | 4 | [5, 1, 1, 5] — the only contiguous run reaching 10 |
| Two pointers after sort | 2 | The 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
- 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.
- 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.
- 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.