Chapter 3: Two Pointers & Sliding Window
Master the two pointer technique for scanning sequences from both ends or in tandem, then learn sliding window patterns that maintain a moving subarray to solve range-based problems in linear time. These patterns eliminate nested loops and are among the most frequently tested in interviews.
- Topics
- 8
- Demos
- 5
- Videos
- 3
- Min read
- 34
Chapter Overview
Two pointers and sliding window are arguably the highest-value patterns in all of DSA interviewing. They transform problems that seem to require O(n^2) brute force into elegant O(n) solutions by maintaining state as pointers sweep through the data.
Two pointers come in two flavors. Same-direction pointers (often called "slow and fast") walk through the array together, with the fast pointer exploring ahead while the slow pointer marks a boundary. Opposite-direction pointers start at both ends and move inward, exploiting sorted order or symmetry to skip unnecessary comparisons.
Sliding window extends the two-pointer idea to subarrays and substrings. A fixed-size window slides across the array, updating its aggregate in O(1) per step. A variable-size window expands to include new elements and shrinks from the left when a constraint is violated, solving "longest/shortest subarray with property X" problems efficiently.
Finally, we cover two closely related techniques: prefix sums, which precompute cumulative totals so any range sum can be answered in O(1), and Floyd's cycle detection, which uses fast and slow pointers on linked structures to detect loops in O(1) space.
This chapter covers:
- Same & opposite direction pointers: Core two-pointer strategies
- Classic two-pointer problems: Remove duplicates, two sum sorted, valid palindrome
- Fixed-size sliding window: Maximum sum subarray of size k
- Variable-size sliding window: Longest and shortest windows with constraints
- Prefix sums: O(1) range queries after O(n) preprocessing
- Cycle detection: Floyd's tortoise and hare algorithm
Chapter Roadmap
Click any topic to jump in
Two Pointers
Two indices walking an array together — the building block for in-place transforms, pair search, and sliding windows.
Same-direction pointers for in-place rewriting, opposite-direction for pair search.
In-Place Rewriting
Same-direction pointers (read/write) compact arrays in $O(n)$ time and $O(1)$ extra memory.
Two Sum / Three Sum
Opposite-direction pointers find target pairs on sorted arrays in linear time — the basis of $k$-sum reductions.
Palindrome Checks
Opposite-direction pointers compare mirrored positions — extensions handle filtering and tolerated mismatches.
Sliding windows generalise two pointers to maintain state over a contiguous range.
Fixed Window
A window of exactly $k$ elements — amortised $O(1)$ per step using incremental updates.
Variable Window
Expand-then-shrink windows — each index is visited at most twice, keeping the total $O(n)$.
Prefix sums answer range queries; tortoise & hare uses two pointers on linked structures.
Prefix Sums
Cumulative sums turn range-sum queries into two array reads — combine with hash maps for subarray-sum problems.
Floyd's Tortoise & Hare
Two pointers moving at different speeds detect cycles in $O(n)$ time and $O(1)$ space.
Topics
8 topics in this chapter
Suppose you must check whether any two numbers in a list of 100,000 values add up to a target, or strip every zero out of an array without allocating a second one. The obvious approach compares every pair with nested loops, about five billion comparisons for 100,000 elements. That is far too slow, and most of those comparisons are wasted because their outcome was predictable from ones already made.
The previous chapter, Binary Search, showed how ordered data lets one comparison discard half of the candidates. Two pointers apply a similar idea to a linear sweep: two indices move through the data, and each comparison lets one of them advance past an element that can never matter again. Since each pointer moves at most n times, the total work is O(n).
This topic introduces the two basic layouts. Same-direction pointers, a fast reader and a slow writer, compact or partition an array in place. Opposite-direction pointers start at both ends and close in, using sorted order or symmetry to decide which end to move. Every later topic in this chapter builds on one of these two shapes.
Definition
The two-pointer technique scans a sequence with two indices whose positions encode an invariant. Same-direction pointers both move left to right, a fast read index ahead of a slow write index. Opposite-direction pointers start at the two ends and move inward. Because neither pointer ever moves backwards, the scan finishes in O(n) time with O(1) extra space.
In this topic
- 1Same-Direction (Fast & Slow) Pointers
- 2Opposite-Direction Pointers
Same-Direction (Fast & Slow) Pointers
In-place problems forbid a second array, yet you still need to separate the elements you keep from the ones you drop. Same-direction pointers do this with two indices that both start at 0. The fast index reads every element exactly once; the slow index marks where the next kept element should be written. The invariant is that nums[0..slow-1] holds exactly the kept elements, in their original order. When nums[fast] passes the test, write or swap it into position slow and increment slow; otherwise only fast advances. A common bug is incrementing slow unconditionally, which keeps everything. This one pattern powers removal, deduplication, and stable partitioning.
Two indices (fast, read) and (slow, write) both advance from . The invariant is that already holds the finalised output. Because each pointer moves only forward and at most times, the total work is time and extra space — strictly better than any solution that allocates a new array.
Given an array [0, 1, 0, 3, 12], move all zeros to the end while maintaining the relative order of non-zero elements. Do this in-place.
Opposite-Direction Pointers
One pointer starts at the beginning, the other at the end. They move toward each other based on a comparison or condition. This is ideal for sorted arrays (where the sum of endpoints guides which pointer to move), palindrome checking (comparing characters from both ends), and container problems (where width decreases as pointers converge). The loop runs while left < right, visiting each element at most once for O(n) time.
Two indices (from the left) and (from the right) close in on each other. At each step one of them moves inward, so the combined distance decreases by at least per iteration. Starting from , the loop runs at most times — . The invariant depends on the problem (e.g., 'the answer, if any, involves some and ').
Given a sorted array [-4, -1, 0, 3, 10], return a new array of each element squared, sorted in non-decreasing order.
Related Problems on PixelBank
A sensor log arrives sorted by value, full of repeated readings, and a downstream job needs just the distinct values stored in the same array, because the device has no memory to spare for a copy. Converting to a set would work, but it allocates O(n) extra space and, in many languages, loses the sorted order along the way.
The previous topic, Same & Opposite Direction Pointers, introduced the fast reader and the slow writer. Removing duplicates from a sorted array is the cleanest application of that layout. Sorting guarantees that equal values sit next to each other, so a value is new exactly when it differs from the last value written. The slow pointer marks the end of the unique prefix, the fast pointer scans ahead, and each new value is copied forward. The array is compacted in one O(n) pass with O(1) extra space.
We first build the basic deduplication loop and its invariant. Then we generalize it to allow each value up to k times, where the only change is which earlier element the fast pointer is compared against.
Definition
In-place deduplication of a sorted array rewrites its prefix so that nums[0..k-1] contains each distinct value exactly once, in sorted order, and returns k. Elements beyond index k-1 are left unspecified. A read pointer and a write pointer achieve this in a single pass, using O(n) time and O(1) extra space.
In this topic
- 1In-Place Deduplication with Two Pointers
- 2Allow At Most K Duplicates
In-Place Deduplication with Two Pointers
Because the array is sorted, duplicates form contiguous runs, so deciding whether nums[fast] is new needs only one comparison, against the last kept value nums[slow]. Keep slow on the last element of the unique prefix, starting at index 0, and let fast scan from index 1. When nums[fast] differs from nums[slow], increment slow and copy nums[fast] there; otherwise skip it. The invariant is that nums[0..slow] holds every distinct value seen so far, once each, so the answer is slow + 1. The method fails silently on unsorted input: [1, 2, 1] keeps all three values, because the two equal elements are not adjacent.
Maintain the invariant is the deduplicated prefix. Read pointer scans the array; whenever , copy to and increment . Each index is touched times so total work is . Space is because writing happens in-place — the overwritten cells are never revisited by .
Remove duplicates in-place from sorted array [0, 0, 1, 1, 1, 2, 2, 3, 3, 4] and return the new length.
Allow At Most K Duplicates
A generalization of the deduplication problem: allow each value to appear at most k times instead of just once. The trick is to compare the fast pointer not with slow, but with the element k positions before slow. If nums[fast] != nums[slow - k + 1], the element can be added. For k=2, compare with nums[slow - 1]. This elegant modification handles any duplicate threshold with the same O(n) time and O(1) space.
Generalise the invariant: contains each distinct element at most times. The decision 'should be kept?' reduces to comparing with : if different (or ), keep it. This preserves time and space, and reduces to the standard dedup when .
Remove duplicates from sorted array [1, 1, 1, 2, 2, 3] such that each element appears at most twice. Return the new length.
Related Problems on PixelBank
An online store wants to suggest two gift items whose prices add up exactly to a customer's gift card balance, and its catalog is already sorted by price. Checking every pair of items costs O(n^2) comparisons. A hash map brings that down to O(n) time, but it needs O(n) extra memory and ignores the fact that the data is already ordered.
The previous topic, Remove Duplicates, used same-direction pointers to rewrite an array. Here we switch to opposite-direction pointers, which exploit sorted order. Put one pointer on the cheapest item and one on the most expensive. If their sum is too small, the cheap item cannot pair with anything, since it is already paired with the largest partner available, so the left pointer moves right. If the sum is too large, the expensive item is useless for the mirror reason, so the right pointer moves left. Every step rules out one element for good, which gives O(n) time with O(1) extra space.
We first prove why discarding an element is safe, then extend the idea to Three Sum by fixing one element and running two-sum on the rest, the general k-sum reduction.
Definition
Two Sum Sorted asks for indices i < j in a sorted array such that nums[i] + nums[j] equals a target. Opposite-direction pointers start at i = 0 and j = n - 1, moving i right when the sum is too small and j left when it is too large. The method runs in O(n) time with O(1) extra space and never skips a valid pair.
In this topic
- 1Two Sum with Opposite Pointers
- 2Three Sum Reduction
Two Sum with Opposite Pointers
Place one pointer at the start and one at the end of the sorted array. Compute their sum. If the sum equals the target, you found the pair. If the sum is too small, move the left pointer right to increase it. If the sum is too large, move the right pointer left to decrease it. Each step eliminates one pointer position, guaranteeing termination in at most n steps. This gives O(n) time and O(1) space.
On a sorted array, define . If , increase (only way to grow ); if , decrease . The key claim: pointers only move inward, and any pair discarded by a move cannot achieve the target. This gives time — strictly better than the of sort-then-hash for the sorted case.
Given sorted array [2, 7, 11, 15] and target 9, return the indices of the two numbers that add up to the target.
Three Sum Reduction
The three-sum problem (find three numbers that sum to zero) reduces to two-sum sorted. Sort the array, then for each element nums[i], use two pointers on the remaining subarray to find a pair summing to -nums[i]. Skip duplicate values of i to avoid duplicate triplets. The total time is O(n^2): O(n) for the outer loop times O(n) for each two-pointer scan. This is optimal since there can be O(n^2) valid triplets.
3-sum reduces to calls of 2-sum: fix , solve 2-sum for target on . With sorting () and linear passes (), total is . Lower bound: 3-sum is conjectured to require time, so is believed optimal. The same pattern extends -sum to .
Given [-1, 0, 1, 2, -1, -4], find all unique triplets that sum to 0.
Related Problems on PixelBank
A form asks users to type a phrase, and you must decide whether it reads the same backwards, ignoring spaces, punctuation, and capital letters, so that "A man, a plan, a canal: Panama" counts. Building a cleaned, reversed copy and comparing the two works, but it allocates new strings and always touches every character, even when the very first and last letters already differ.
The previous topic, Two Sum Sorted, moved opposite-direction pointers according to a sum. Palindrome checking uses the same layout for symmetry instead of order: position i must match position n-1-i, so a left pointer and a right pointer compare mirrored characters and step inward together. The first mismatch ends the search, and a match on every pair proves the string is a palindrome, all in O(n) time and O(1) extra space.
We first handle the practical version that skips non-alphanumeric characters and ignores case without building a copy. Then we allow at most one deletion, where a single mismatch branches into exactly two possible repairs, and each branch is checked with the same simple loop.
Definition
A string s of length n is a palindrome if s[i] equals s[n-1-i] for every index i < n/2. The two-pointer check compares s[left] with s[right], starting from the two ends, and moves both pointers inward after each match. It stops at the first mismatch, giving O(n) time in the worst case and O(1) extra space.
In this topic
- 1Basic Palindrome Check with Two Pointers
- 2Valid Palindrome with At Most One Removal
Basic Palindrome Check with Two Pointers
Place a left pointer at the start and a right pointer at the end of the string. Compare characters at both positions. If they match, move both pointers inward. If they differ, the string is not a palindrome. For case-insensitive checks, convert both characters to lowercase before comparing. To skip non-alphanumeric characters, advance the pointer past them before each comparison. The loop runs while left < right, giving O(n) time and O(1) space.
A string is a palindrome iff for all . Two pointers compare in parallel, advancing inward when matched, failing on mismatch. Time is in the worst case, space , and early termination makes the best case .
Determine if "A man, a plan, a canal: Panama" is a valid palindrome, considering only alphanumeric characters and ignoring case.
Valid Palindrome with At Most One Removal
Some inputs are one typo away from a palindrome, and the question becomes whether deleting at most one character fixes them. Run the normal inward scan. At the first mismatch, at positions left and right, one of those two characters must be the deleted one, because every pair outside them already matches. So test the two remaining ranges, s[left+1..right] and s[left..right-1], with a plain palindrome check that allows no further deletions. If either passes, the answer is True. Each check is one linear scan, so the total stays O(n). Allowing unlimited deletions would instead branch at every mismatch and grow exponentially.
On mismatch at , the answer is 'yes' iff or is a palindrome — exactly one of the two characters must be the odd one out. This is a constant number of recursive palindrome checks, each , so the total remains . The bound 'at most one removal' prevents an exponential branching tree.
Can "abca" become a palindrome by removing at most one character?
Related Problems on PixelBank
A fitness tracker records steps every minute, and the app must report the busiest 60-minute stretch of the day. Summing each 60-minute block from scratch means 60 additions for each of about 1,380 starting minutes, and the cost, O(nk) in general, grows as windows get longer or the data gets finer.
The previous topic, Valid Palindrome, moved two pointers toward each other. A sliding window moves two pointers in the same direction, always exactly k apart, and treats the elements between them as one unit. The key observation is that two consecutive windows share k - 1 elements: sliding one step adds a single new element on the right and drops a single old one on the left. Updating a running aggregate with that pair of changes costs O(1), so all n - k + 1 windows cost O(n) in total.
We first apply this to the classic maximum-sum window with a running sum. Then we extend it to aggregates that cannot be undone by subtraction, such as distinct counts and window maxima, by pairing the window with a hash map or a monotonic deque.
Definition
A fixed-size sliding window is a contiguous range nums[i..i+k-1] of exactly k elements that advances one index at a time across an array of length n. Its aggregate is updated incrementally, adding the entering element and removing the leaving one, so all n - k + 1 windows are processed in O(n) total instead of O(nk).
In this topic
- 1Maximum Sum Subarray of Size K
- 2Sliding Window with Auxiliary Data Structures
Maximum Sum Subarray of Size K
Recomputing every window's sum from scratch costs k additions per window, O(nk) overall. Instead, compute the first window's sum once, then slide: the sum of the window starting at i + 1 equals the previous sum plus nums[i + k] minus nums[i]. Track the largest sum seen. Each slide is O(1), so the whole scan is O(n) with O(1) extra space. The subtraction step is the crux. It works for sums, counts, and averages, which can be undone, but not for a maximum, because removing the old maximum leaves no record of the runner-up. Also guard against k > n, where no window exists.
Compute , then for each step add and subtract — an update. Total time and space. This is a telescoping sum: instead of recomputing terms, cache the running total and apply a delta of size per shift.
Find the maximum sum of any subarray of size 3 in [2, 1, 5, 1, 3, 2].
Sliding Window with Auxiliary Data Structures
Some fixed-window problems require more than a running sum. For example, counting distinct elements in each window of size k needs a hash map (frequency counter). When an element enters the window, increment its count; when one leaves, decrement it (and remove if count hits zero). The number of keys in the map gives the distinct count. Similarly, finding the maximum in each window can use a deque (monotonic queue) for O(n) amortized time.
To track properties like 'distinct count' or 'max in window' under updates, pair the window with a hash map or a monotonic deque. Hash map: incrementing a counter on add and decrementing on remove gives amortised per step. Monotonic deque: maintains window maxima in total because each element is pushed and popped at most once.
Count the number of distinct elements in each window of size 3 for array [1, 2, 1, 3, 4, 2, 3].
Related Problems on PixelBank
A streaming service wants the longest stretch of a user's watch history that involves at most two genres, or the shortest run of consecutive purchases whose total reaches a reward threshold. Neither question fixes the window length in advance, so the fixed-size trick of keeping the pointers exactly k apart no longer applies, and checking every start and end pair means examining O(n^2) windows.
The previous topic, Fixed-Size Sliding Window, kept the two pointers a constant distance apart. Here the gap changes: the right pointer grows the window by one element each step, and the left pointer shrinks it only as far as the constraint requires. The pattern works whenever validity is monotonic, meaning a shrunken valid window stays valid, or a grown satisfying window keeps satisfying the goal. Under that condition each pointer only moves forward, at most n times, so the nested-looking loop runs in O(n).
We first build the longest-window template, which records the answer after restoring validity. Then we flip it into the shortest-window template, which records while shrinking a window that already meets the goal.
Definition
A variable-size sliding window keeps a range [left, right] whose length adapts to a constraint. The right pointer extends it one element per step, and the left pointer advances only to restore or exploit validity. When validity is monotonic in the window's extent, both pointers move at most n times, so the longest or shortest qualifying subarray is found in O(n).
In this topic
- 1Longest Window Pattern (Expand Until Invalid)
- 2Shortest Window Pattern (Shrink After Valid)
Longest Window Pattern (Expand Until Invalid)
For 'find the longest subarray with at most K distinct elements' type problems, expand the right pointer one element at a time, updating window state. When the constraint is violated (e.g., more than K distinct elements), shrink from the left until the constraint is restored. The answer is the maximum value of (right - left + 1) seen during the process. The key insight: each pointer moves at most n times total, so despite the nested loops, the total time is O(n).
Grow greedily; when the window becomes invalid, shrink until it is valid again. The total is because each index is touched at most twice (once by , once by ). Correctness: the optimal longest valid window must end at some , and for each the smallest valid is monotone non-decreasing — so a single-pass two-pointer sweep finds the optimum.
Find the length of the longest substring with at most 2 distinct characters in 'eceba'.
Shortest Window Pattern (Shrink After Valid)
For 'find the shortest subarray whose sum is at least target' type problems, expand right to grow the window sum. Once the sum meets or exceeds the target, the window is valid --- record its length and shrink from the left to try to find a shorter valid window. Keep shrinking while the window remains valid. The answer is the minimum length recorded. This pattern also handles 'minimum window substring' problems where you shrink after satisfying all required characters.
Expand until the window is valid, then shrink as long as it remains valid, recording the min length. Same bound. The key invariant is that any shorter valid window contained in must start at , so moving only ever discards longer (suboptimal) candidates.
Find the length of the shortest subarray with sum >= 7 in [2, 3, 1, 2, 4, 3].
A dashboard shows daily sales for the last five years, and analysts fire thousands of queries like "total revenue between March 3 and June 18". Summing each range on demand costs up to n additions per query, so q queries cost O(nq), and a busy dashboard grinds to a halt even though the underlying data never changes.
The previous topic, Longest & Shortest Window, answered range questions with a window that slides once across the data. That works when the ranges arrive in left-to-right order, but arbitrary queries jump around. A prefix sum array fixes this by paying O(n) once up front: P[i] stores the total of the first i elements, so any range total is the difference of two stored values and each query costs O(1).
We first build the prefix array with its leading zero and derive the range formula, including the off-by-one that trips up most people. Then we combine prefix sums with a hash map to count subarrays that sum to exactly k, a case where sliding windows fail because negative numbers break their monotonicity.
Definition
The prefix sum array of nums has length n + 1, with P[0] = 0 and P[i] = nums[0] + ... + nums[i-1]. The sum of the inclusive range nums[l..r] then equals P[r+1] - P[l]. Building P takes O(n) time, and every range-sum query afterwards takes O(1) time.
In this topic
- 1Building and Using Prefix Sums
- 2Subarray Sum Equals K with Prefix Sum + Hash Map
Building and Using Prefix Sums
Answering many range-sum queries with a loop is wasteful, because overlapping ranges repeat the same additions. Build an array P of length n + 1 with P[0] = 0 and P[i + 1] = P[i] + nums[i], so P[i] is the total of the first i elements. The sum of nums[l..r] inclusive is P[r + 1] - P[l]: P[r + 1] covers indices 0 through r, and subtracting P[l] cancels indices 0 through l - 1. The leading zero lets ranges starting at index 0 use the same formula. The classic bug, P[r] - P[l], drops nums[r]. Frequent updates call for a Fenwick tree instead.
Define with . Then in . Preprocessing is and each query is , so queries cost versus naïvely — a huge win when is large.
Given nums = [1, 2, 3, 4, 5], answer these range sum queries: sum(0..2), sum(1..3), sum(2..4).
Subarray Sum Equals K with Prefix Sum + Hash Map
To count subarrays with sum exactly k, combine prefix sums with a hash map. As you compute prefix sums left to right, a subarray ending at index j with sum k means prefix[j+1] - prefix[i] = k for some i, which means prefix[i] = prefix[j+1] - k. So at each step, check how many previous prefix sums equal (current_prefix - k). Store prefix sum frequencies in a hash map for O(1) lookup. This solves the problem in O(n) time and O(n) space.
The number of subarrays with sum equals . Rewriting as and maintaining a hash map from prefix sums to counts lets you count contributing 's for each in . Total time , space — linear versus brute force.
Count the number of subarrays with sum equal to 3 in [1, 1, 1, 2, -1, 2].
Related Problems on PixelBank
A linked list is supposed to end in null, but a bug made its last node point back into the middle, and now every loop over it runs forever. You need to detect this, ideally without extra memory: storing every visited node in a hash set works, but it costs O(n) space, which matters for huge lists and for memory-constrained systems.
The previous topic, Prefix Sum, closed the array half of this chapter. Floyd's algorithm brings the two-pointer idea to linked structures and to any sequence defined by a function, x, f(x), f(f(x)), and so on. A slow pointer moves one step at a time and a fast pointer moves two. With no cycle, fast reaches the end. With a cycle, both pointers eventually circle inside it, and because fast gains one step per move, it must land on slow within one lap. That gives O(n) time and O(1) space.
We first see why the pointers meet and how a second phase, restarting one pointer from the head, finds the exact node where the cycle begins. Then we apply the same algorithm to an array to find a duplicate number without modifying the array or allocating memory.
Definition
Floyd's cycle detection, also called the tortoise and hare algorithm, walks a sequence with a slow pointer taking one step and a fast pointer taking two. The pointers meet if and only if the sequence has a cycle. Restarting one pointer from the start and moving both one step at a time then finds the first node of the cycle, in O(1) space.
In this topic
- 1Tortoise and Hare Algorithm
- 2Finding Duplicates with Floyd's Algorithm
Tortoise and Hare Algorithm
Start slow and fast at the head and move them one and two nodes per step. If fast reaches null, there is no cycle. Otherwise both enter the cycle, where fast gains one node per step, so they meet within one lap. For the cycle's start, let d be the head-to-entry distance, L the cycle length, and m the meeting point's distance past the entry. Fast walked twice as far as slow, so d + m is a multiple of L, and d more steps from the meeting point land on the entry. Reset one pointer to the head and step both singly: they meet at the entry.
Let advance by step and by . If the list has a cycle of length starting at position , they meet after at most steps — because once both are inside the cycle, the relative speed is and fast catches slow in at most iterations. Total time , space — no hash set needed.
Detect if a linked list has a cycle and find the node where the cycle begins.
Finding Duplicates with Floyd's Algorithm
Given an array of n+1 integers where each integer is between 1 and n, there must be at least one duplicate. Treat the array as a linked list where index i points to index nums[i]. A duplicate value means two indices point to the same 'node,' creating a cycle. Use Floyd's algorithm to find where the cycle begins --- that value is the duplicate number. This solves the problem in O(n) time and O(1) space without modifying the array.
Treat the array as a function . Because there are values in , has a cycle by the pigeonhole principle, and the duplicate is the cycle's entry point. Applying Floyd's algorithm twice (meet, then restart one pointer) finds the entry in time and space — a striking improvement over time with space for the hash-set method.
Find the duplicate number in [1, 3, 4, 2, 2] using Floyd's algorithm (O(1) space, no array modification).