CS Core & Software Engineering · Data Structures & Algorithms
Two Pointers and Sliding Window
Two pointer traversal and sliding window techniques for subarray and substring problems.
Eight concepts on two pointers and sliding windows — opposite-end scans on sorted arrays, fixed and variable windows, the classic longest-unique-substring walk, and fast/slow cycle detection. The shared win is O(n) time with O(1) extra memory when the pointers only move forward (or inward) once.
- CS Core & Software Engineering
- Medium level
- 8 concepts
- 5 practice questions
1Two indices instead of nested loops
Two-pointer techniques maintain two indices into a sequence and move them according to a rule — toward each other, or both forward at possibly different speeds. The goal is to solve pair, partition, or window problems in O(n) time instead of an O(n^2) double loop that re-checks the same pairs.
The method is not free magic: opposite-end pair search needs sorted order (or a sort that costs O(n \log n) first). Sliding windows need a constraint you can maintain by growing and shrinking a contiguous span. When those ingredients are present, each element is touched a constant number of times.
Figure. Two indices replace a nested scan when the array's order lets you advance exactly one side per step.
Why it beats O(n^2)
- One pass familyPointers only advance (or only retreat); they never restart from the beginning for each i.
- Amortized linearAcross the whole run, left and right each move at most n steps → O(n) total work.
- SpaceOften O(1) extra — just the indices (plus a small frequency map for some windows).
A two-pointer scan where each index only moves forward across an n-element array is
- O(n) time, because each pointer contributes at most n steps
- O(n^2) time, because two indices always mean a nested loop
- O(1) time, because only two integers are stored
Two indices are O(1) space, not O(1) time. Forward-only movement caps total steps at O(n); nested loops are the pattern this replaces.
2Opposite-end pointers on a sorted array
On a sorted array, place left at the start and right at the end. Compare the sum (or other monotone combination) to the target: if too small, advance left to increase the sum; if too large, move right leftward to decrease it; if equal, you have the pair. Each step discards one index permanently.
Unsorted data must be sorted first — O(n \log n) — or you switch to a hash map that spends O(n) extra memory. Opposite-end exploits sortedness specifically to keep extra memory at O(1).
Figure. Start with L on 1 and R on 11. Sums that are too large shrink R; sums that are too small advance L until 4 + 5 = 9.
Pair sum inward
- Initializeleft = 0, right = n − 1 on a sorted array.
- Compare sumIf nums[left] + nums[right] < target, left++; if greater, right−−; if equal, return the pair.
- StopPointers meet ⇒ no pair. Total movements ≤ n.
Two Sum on a Sorted Array
Given a sorted array [1, 3, 4, 5, 7, 11] and target 9, find two elements that sum to the target.
- left = 0, right = 5: 1 + 11 = 12 > 9 → right−−right = 4
- 1 + 7 = 8 < 9 → left++left = 1
- 3 + 7 = 10 > 9 → right−−; then 3 + 5 = 8 < 9 → left++left = 2, right = 3
- 4 + 5 = 9pair (4, 5) in O(n) time, O(1) space
Pro tip. Opposite-end two pointers exploit sortedness to avoid the O(n) extra space a hash map would use.
On a sorted array, if nums[left] + nums[right] is less than the target, you should
- Advance left, because the sum needs to grow
- Advance right, because a larger right index always increases the sum
- Reset both pointers to the middle and binary search
Sorted ascending: moving left toward larger values increases the sum; moving right would decrease it further. Binary search of the whole array each time would rebuild an O(n \log n) nested pattern.
3Sliding window: grow right, shrink left
A sliding window maintains a contiguous subarray or substring [\mathrm{left}, \mathrm{right}]. Expand by advancing right to include new elements; when a constraint breaks, advance left to drop elements from the front until the window is valid again. Track a best answer (length, sum, count) at each valid state.
Because left and right only move forward, each index enters and leaves the window at most once — total work O(n) even though a nested description sounds quadratic.
Figure. A contiguous window on "abcabcbb". Grow R to include; when a duplicate enters, advance L until the window is unique again.
Variable window skeleton
- Expandright++ and update the window's summary (sum, set, counts).
- Shrink while invalidWhile the constraint fails, remove s[left] from the summary and left++.
- RecordUpdate the best answer from the current valid window, then continue expanding.
In a variable sliding window on a string of length n, total pointer movement is
- O(n), because left and right each advance at most n times
- O(n^2), because every right step restarts left at 0
- O(1), because the window is a single object
Left never moves backward; right never moves backward. At most 2n advances total. Restarting left each time would destroy the amortization.
4Fixed-size vs variable-size windows
A fixed window of length k always covers k elements: slide by dropping the outgoing element and adding the incoming one, usually in O(1) work per step after the first window is built. Use it for "best subarray of length exactly k".
A variable window grows and shrinks to satisfy a predicate — longest substring with all unique characters, shortest subarray with sum at least S. The length is the answer (or part of it), not an input. Mixing the two usually means you hardcoded k on a problem that asked for an extremal length.
Same strip shape as Sliding window: grow right, shrink left — fixed windows hold length k; variable windows let length be the answer while the constraint holds.
| Kind | Length | Typical ask |
|---|---|---|
| Fixed | Given k | Max/min aggregate among windows of size k |
| Variable | Discovered | Longest/shortest window meeting a constraint |
"Longest substring with all distinct characters" is
- A variable window — length is what you maximize under a constraint
- A fixed window — you are given k up front
- Not a window problem — only opposite-end pointers apply
No k is given; you expand/shrink until uniqueness holds and track the max length. Fixed windows need a stated size.
5Longest substring without repeating characters
Maintain window [\mathrm{left}, \mathrm{right}] and a set (or last-seen map) of characters inside it. Advance right; if s[\mathrm{right}] is already in the window, advance left — removing characters as you go — until that character is unique again. After each expansion, the window length is a candidate maximum.
On s = \texttt{abcabcbb} the answer is 3 ("abc"). The run is O(n) because each pointer only moves forward.
Track the window on the Sliding window: grow right, shrink left strip: grow while characters are new, shrink from the left on a repeat, and keep the best length seen.
Shrink on duplicate
- Expand rightInclude s[right] in the window structure.
- RepairWhile s[right] duplicates a window character, drop s[left] and left++.
- Track maxbest = max(best, right − left + 1).
Variable window with a set
def length_of_longest_substring(s):
seen = set()
left = 0
best = 0
for right, ch in enumerate(s):
while ch in seen:
seen.remove(s[left])
left += 1
seen.add(ch)
best = max(best, right - left + 1)
return bestLongest Substring Without Repeating Characters
Given s = "abcabcbb", find the length of the longest substring with all distinct characters.
- right on "abc": seen = {a,b,c}len = 3; best = 3
- next "a" ∈ seen → remove s[left]="a", left→1; then add "a"window "bca"; len = 3; best = 3
- at final "bb": shrink until one "b" remainsbest stays 3 (never exceeds "abc")
Pro tip. This variable-size window runs in O(n) because each pointer only moves forward across the string once.
Coding lab. Longest unique window in abcabcbb runs in the app, with checks on your output.
For s = "abcabcbb", the longest substring without repeating characters has length
- 3, e.g. "abc"
- 8, the whole string
- 2, because "bb" appears at the end
"abc" (and later "bca", "cab") are length 3; the full string repeats. The final "bb" is a short unique window of length 1 after shrinking.
6Fast and slow pointers
Fast/slow (Floyd) uses two pointers at different speeds on a linked structure: typically slow moves one step while fast moves two. If there is a cycle, fast eventually laps slow and they meet inside the loop. If fast hits null, there is no cycle.
The same idea finds a midpoint in one pass: when fast reaches the end, slow sits at the middle — useful before reversing the second half of a list. Speed difference, not sortedness, is the ingredient.
Figure. A→B→C→D→E with E linking back to C along the outside. Fast and slow start at A; they first meet at D in the ledger's three-tick walk.
Cycle detection
- Start togetherslow = fast = head.
- Advanceslow = slow.next; fast = fast.next.next (when safe).
- DecideMeet ⇒ cycle. fast becomes null ⇒ acyclic.
Meet inside a cycle
A list has nodes A→B→C→D→E→C (E links back to C). Slow starts at A and moves one step per tick; fast moves two. Do they meet, and where is the first meeting node among {C, D, E}?
- After tick 1: slow = B, fast = Cnot met
- Tick 2: slow = C, fast = Enot met
- Tick 3: slow = D, fast = D (E→C→D)meet at D
Pro tip. Meeting proves a cycle exists; a second phase (reset one pointer to head, step both by one) finds the cycle entrance if you need it.
In Floyd cycle detection, if the list has no cycle, fast
- Reaches null (or null.next) and the algorithm reports no cycle
- Loops forever because two pointers always meet
- Must jump to the head and start a binary search
On an acyclic list fast falls off the end. Meeting is the cycle signal; binary search does not apply to an unmarked linked list.
7Why windows are amortized O(n)
Fixed windows of size k: build the first window in O(k), then each slide removes one element and adds one — O(1) per step, O(n) over the array. Variable windows: every index is assigned to right once and to left at most once, so expand/shrink work sums to O(n) even if an inner while-loop looks scary in isolation.
Opposite-end two pointers share the same accounting: left only increases, right only decreases, at most n moves total. Sorting first, when required, dominates at O(n \log n).
Figure. Amortized O(n): every index is enqueued at most once and dequeued at most once across the whole scan.
Charge each move once
- Right advancesAt most n expansions.
- Left advancesAt most n shrinks — left never retreats.
- TotalO(n) updates of the window summary (plus O(n \log n) if you sorted first).
| Pattern | Time | Extra space |
|---|---|---|
| Opposite-end on sorted data | O(n) after sort | O(1) |
| Sort then opposite-end | O(n \log n) | O(1) or sort's stack |
| Variable / fixed window | O(n) | O(1) or O(k) for counts |
| Fast/slow on a list | O(n) | O(1) |
A variable window's inner while-loop that advances left can still be O(n) overall because
- Left only moves forward, so those iterations sum to ≤ n across the whole run
- The while-loop is ignored by Big-O notation
- Right resets to 0 after every shrink
Amortization counts total left moves, not the worst single while. Resetting right would destroy the bound.
8Which pointer pattern fits
Sorted pair or container problems → opposite-end. Contiguous extremum under a constraint → sliding window (fixed if k is given, variable if length is the answer). Cycle or midpoint on a list → fast/slow. Unsorted pair sum with O(n) memory OK → hash map from the hashing topic instead.
If you catch yourself restarting a second index from 0 for every i, ask whether a monotonic pointer or window would let that index only move forward.
Pick from the pattern table: sorted pair sum → opposite-end; contiguous extremum under a constraint → sliding window; list cycle or midpoint → fast/slow; unsorted pair with O(n) memory → hash map.
| Prompt shape | Pattern |
|---|---|
| Sorted array, pair with target sum | Opposite-end two pointers |
| Longest/shortest contiguous span with a property | Variable sliding window |
| Best aggregate among spans of length k | Fixed window |
| Linked-list cycle or midpoint | Fast / slow |
Detecting a cycle in a singly linked list with O(1) extra memory points to
- Fast/slow pointers
- Opposite-end pointers on a sorted array
- A fixed window of length k = 2
Speed difference on a list finds cycles without a hash set of nodes. Opposite-end needs random access and sorted order; a fixed window is for contiguous aggregates.
Notes
- Two Pointers: Use two indices moving toward each other (or in the same direction) to solve pair/partition problems in O(n) instead of O(n^2).
- Opposite-End Pattern: On a sorted array, move a left and right pointer inward based on whether the current sum is too small or too large (e.g., pair-sum, container problems).
- Sliding Window: Maintain a moving subarray/substring window, expanding the right pointer and shrinking the left to satisfy a constraint in a single pass.
- Fixed vs Variable Window: Fixed-size windows solve 'best window of length k'; variable-size windows solve 'longest/shortest window meeting a condition.'
- Fast/Slow Pointers: Two pointers at different speeds detect cycles or find a midpoint in one traversal.
Formulas
- Two-pointer scan: O(n) time, O(1) extra space.
- Sliding window: each element enters and leaves the window at most once, so total work is O(n).
- Fixed window of size k: slide by removing the outgoing element and adding the incoming one in O(1) per step.
- Sorting prerequisite: opposite-end two-pointer on unsorted data first needs an O(n \log n) sort.
- Variable window: expand right, and while the constraint breaks, advance left - amortized O(n).
Exam traps & shortcuts
- If the array is sorted and you need a pair/triplet meeting a sum condition, use opposite-end two pointers rather than nested loops.
- For 'longest/shortest substring with condition X,' reach for a variable-size sliding window with a frequency map.
- For a max/min sum of every length-k window, use a fixed sliding window and update in O(1) instead of recomputing.
- Detecting a cycle or the middle of a linked list is a fast/slow (tortoise and hare) two-pointer problem.
Reference tables
Restated from the concepts — scan sheet only.
| Pattern | Time | Extra space |
|---|---|---|
| Two-pointer scan (pointers monotone) | O(n) | O(1) |
| Opposite-end after sorting | O(n \log n) | O(1) |
| Sliding window (fixed or variable) | O(n) | O(1) or O(k) |
| Fast/slow cycle or midpoint | O(n) | O(1) |
Identities the examples keep using.
| Identity | Form |
|---|---|
| Window length | right − left + 1 |
| Amortized window work | each index enters/leaves ≤ once → O(n) |
| Opposite-end move | sum too small ⇒ left++; too big ⇒ right−− |
| Fast/slow step | slow +1, fast +2 per tick |
Recap
Night-before pegs for two pointers and windows.
- Core
- Two monotone indices beat O(n^2) nested loops when each element is charged once.
- Opposite-end
- Sorted array: grow sum with left++, shrink with right−−. O(1) extra space.
- Window
- Expand right; shrink left while invalid; track best. Contiguous only.
- Fixed vs variable
- Fixed: length k given. Variable: length is the answer under a constraint.
- Unique substring
- "abcabcbb" → 3; set/map + shrink on duplicate.
- Fast/slow
- Cycle if they meet; midpoint when fast hits the end.
- Cost
- Inner while is fine — left never retreats, total still O(n).
Practise Two Pointers and Sliding Window
Reading is free and needs no account. Practice, mocks and progress live in the app.
- 5 exam-style questions on this topic, with explanations
- A 6-question practice set that ends the chapter
- Timed mocks scored with the real marking scheme
- Readiness tracked per topic, kept on your device