Two Pointers
Scope — The two-pointer family on arrays and strings — opposite-ends convergence, fast/slow, expand-from-centre and the read/write partition, with one canonical template for each; window problems that grow and shrink on a condition live elsewhere. See also — deep dives split out of this file: 2_pointers_examples.md — the worked LC catalogue, one canonical solution per problem; quickselect.md — QuickSelect / partition-based Kth-element selection, which is a selection algorithm rather than a two-pointer scan. Neighbouring sheets: sliding_window.md — variable-size windows driven by a condition; 2_pointers_linkedlist.md — the same idea on nodes instead of indices; n_sum.md — the sorted-array k-sum specialisation; palindrome.md — expand-from-centre in depth.
LeetCode Problem Lists
Overview
Two pointers keeps two indices into the same sequence (or one into each of two sequences) and advances them under a rule, replacing a nested loop with a single linear pass.
Key Properties
- Complexity: O(N) for a single scan, O(N log N) when the input must be sorted first, O(N^2) for expand-from-centre over every index; O(1) extra space — the pointers are the only state
- Core Idea: every iteration advances at least one pointer and never revisits an index, so total work is bounded by the number of pointer moves
- When to Use: sorted input, in-place rewriting, palindromes, merging two sorted sequences, subsequence checks, cycle detection
- Key Techniques: fast/slow (same direction), left/right (converging), expand-from-centre, one-pointer-always-moves
References
Recognition
| Signal in the statement | Constraint cue | Reach for | Not this if |
|---|---|---|---|
| “remove … in-place”, “return k, the first k elements”, “O(1) extra memory” | n ≤ 3·10⁴, sorted or a fixed val |
Template 1 fast/slow compaction — LC 26, LC 27, LC 80, LC 283 | the order of the kept elements does not matter and writes are costly → Template 2’s bidirectional LC 27 |
| sorted array, “a pair whose sum …”, “max area between two lines”, “palindrome” | n ≤ 10⁵ → O(n), or O(n log n) with a sort first | Template 2 converging pointers — LC 11, LC 125, LC 167, LC 344 | unsorted and the answer is indices → hash map complement lookup (hash_map.md) |
| “longest / count palindromic substring” | n ≤ 1000 → O(n²) is fine | Template 3 expand from centre — LC 5, LC 647 | palindromic subsequence → interval DP (LC 516) |
| “is s a subsequence of t”, “formed by deleting some characters” | len(t) ≤ 10⁴ | Template 4 one pointer always moves — LC 392, LC 524, LC 1023, LC 809 | contiguous substring match → KMP / window; longest common subsequence → DP (LC 1143) |
| “merge nums2 into nums1”, nums1 has trailing empty slots | m + n ≤ 200, O(1) space asked | Template 5 fill from the back — LC 88, LC 977, LC 986 | two linked lists → merge forward with a dummy head (LC 21) |
| “sort an array of 0, 1, 2 in one pass”, “partition by value into three groups” | O(1) space, one pass | Template 6 Dutch national flag — LC 75, LC 905 | the relative order must be kept → a stable list partition (LC 86) is linked_list.md’s two dummies |
| “does the linked list have a cycle”, “where does it begin”, “find the duplicate without modifying” | O(1) memory | Template 7 Floyd fast/slow — LC 141, LC 142, LC 287 | O(n) memory is allowed → a visited set is simpler |
| window that grows on one condition, shrinks on another | n ≤ 10⁵ | Template 8 grow right, shrink left — LC 567, LC 209 | any real window problem → sliding_window.md, which owns the family |
| two inputs compared token by token — “compare version numbers”, “one edit apart”, “equal after backspaces” | O(n + m) time, O(1) space | Template 9 two sequences in lockstep — LC 165, LC 161, LC 844, LC 986 | tokens may be skipped or reordered → subsequence (Template 4) or DP |
| kth largest / k closest (select, do not sort) | n ≤ 10⁵, average O(n) | quickselect.md — LC 215, LC 973 | k is tiny or the input streams → heap |
Constraint cues come from the ruler in complexity_cheatsheet.md. Everything else — the long tail of worked problems — lives in 2_pointers_examples.md.
Problem Categories
Pointer Types
-
Pointer types
-
Fast - Slow pointers- fast, slow pointers from
same start point
- fast, slow pointers from
-
Left- Right pointers- left, right pointers from
idx = 0, idx = len(n) - 1respectively - Usually set
- left pointer = 0
- right pointer = len(nums)
- binary search
- array reverse
- 2 sum
- sliding window
- left, right pointers from
-
-
Expandfrom center (and Deal withodd, evencases)- LC 680
- LC 647
- LC 005
-
Merge Sorted Array
- LC 88
-
Minimum Swaps to Group All 1’s Together
- LC 1151 (check sliding_window.md)
-
Boats to Save People
- LC 881
-
Sort + Fix-One + Two-Pointer(closest / smaller sum)- Fix
i, squeezel/rinward; track closest by|sum - target| - LC 16 (3Sum Closest)
- LC 259 (3Sum Smaller)
- Fix
-
move
right pointer first, then moveleft pointper condition- LC 567
- LC 209 (see
sliding window cheatsheet)
-
Subsequence Matchingwith character type constraints- One pointer always moves, one conditionally moves
- Extra validation on non-matching characters
- LC 392 (Is Subsequence)
- LC 1023 (Camelcase Matching - with uppercase/lowercase constraint)
-
Group-by-Group String Comparison- Both pointers advance by group (run of same char), not by single char
- Validate each aligned group: size must allow extension (>= 3) if counts differ
- LC 809 (Expressive Words)
-
Find-Pivot + Find-Successor + Reverse-Suffix(Next Permutation)- Scan right-to-left to find first ascending pair (pivot), then smallest-greater successor
- Swap pivot and successor, then reverse the descending suffix → ascending
- LC 31 (Next Permutation), LC 556 (Next Greater Element III)
-
Converging Low/High pointers to build a permutation(greedy)- Consume smallest available value on one signal, largest on the other
low/highwalk inward over the range[0, n]; the survivor fills the last slot- LC 942 (DI String Match)
-
Last-occurrence + expanding right boundary(greedy partition)- Pre-compute each char’s last index; cut when
i == end - LC 763 (Partition Labels)
- Pre-compute each char’s last index; cut when
-
Staircase pointers on a sorted 2D matrix- Start at the
top-rightsaddle: too big →col--, too small →row++ - LC 240 (Search a 2D Matrix II)
- Start at the
-
Two pointers over TWO strings (lockstep / chunk-wise)- Each pointer consumes one chunk from its own string per round
- Exhausted side → implicit
0(LC 165) or prefix rule (LC 953) - LC 165 (Compare Version Numbers), LC 953 (Verifying an Alien Dictionary)
-
3-phase pointer scan over sorted intervals- before → merge-overlap → after
- LC 57 (Insert Interval)
-
Algorithm
- binary search
- sliding window
- for loop + “expand
left,rightfrom center”
-
Data structure
- Array
- Linked list
Templates & Algorithms
Template 1: Fast/Slow Read-Write Compaction — LC 26 Priority 5 of 5 — Must know — expect it in almost every loop
Recognise — “remove the duplicates / every val in-place and return k, the number of
elements kept”, “do not allocate extra space”, “the first k elements must hold the result”.
n ≤ 3·10⁴ → one O(n) pass.
Key point — nums[0..slow] always holds the kept elements, in order. fast reads every
index once; slow advances only when a value is kept, so the write slot never overtakes the
read slot and nothing unread is overwritten.
# python
# LC 26 - Remove Duplicates from Sorted Array
# IDEA: fast reads every index; slow is the last kept slot, advanced only for a new value
# time = O(n), space = O(1)
def removeDuplicates(nums):
if not nums:
return 0
slow = 0 # nums[0..slow] = the unique values seen so far
for fast in range(1, len(nums)):
if nums[fast] != nums[slow]: # a value not kept yet
slow += 1 # open the next write slot
nums[slow] = nums[fast] # copy it forward
# equal: a duplicate, fast moves on and slow stays
return slow + 1 # slow is an index; the count is index + 1
// java
// LC 26 - Remove Duplicates from Sorted Array
// IDEA: fast reads every index; slow is the last kept slot, advanced only for a new value
// time = O(n), space = O(1)
public int removeDuplicates(int[] nums) {
if (nums.length == 0) {
return 0;
}
int slow = 0; // nums[0..slow] = the unique values seen so far
for (int fast = 1; fast < nums.length; fast++) {
if (nums[fast] != nums[slow]) {
slow++; // open the next write slot
nums[slow] = nums[fast]; // overwrite with the new unique value
}
}
return slow + 1;
}
Complexity
- Time O(n): the single
for fastloop; each index is read once and written at most once. - Space O(1): the two indices are the only state — the result is written into
numsitself.
Dry run
nums = [0,0,1,1,2]
fast=1 0 == nums[0] skip slow=0
fast=2 1 != 0 slow=1, nums[1]=1 [0,1,1,1,2]
fast=3 1 == nums[1] skip slow=1
fast=4 2 != 1 slow=2, nums[2]=2 [0,1,2,1,2]
return 3 -> nums[:3] = [0,1,2]
Traps
[]: returning barereturngivesNonein Python, not0. Guard it explicitly.- Allowing k copies (LC 80): compare against the written prefix,
nums[fast] != nums[write - k], never against the original neighbournums[fast - k]. On[1,1,1,2,2,3], k = 2, thefast - kversion reads a slot already overwritten and returns[1,1,2,3], losing a2. - Swapping instead of overwriting (
nums[slow], nums[fast] = nums[fast], nums[slow]) is also correct and keeps every value somewhere in the array; overwriting is simpler because the tail is never read.
Not this if — the order of the kept elements does not matter and writes are expensive: the
bidirectional LC 27 under Template 2
copies from the right end and touches fewer slots when val is rare.
Drill — LC 26 ⭐ · LC 27 (keep when != val; slow is the next free slot) · LC 283
(swap with the write slot so the zeros drift right) · LC 80 (at most two copies: compare with
nums[write - 2]) · LC 83 / LC 82 (the same rule on a linked list, see
2_pointers_linkedlist.md)
Allowing at most K copies
Generalise the comparison to nums[fast] != nums[write - k], with write the next free slot and
the first k elements always kept: k = 1 is LC 26, k = 2 is LC 80. Worked in
2_pointers_examples.md.
Remove Element — LC 27
Key point — the same compaction with a different keep test: keep
nums[fast]whenever it is notval. Hereslowis the next free slot rather than the last kept one, so the answer isslowitself.
Reduces to: Template 1 — only the keep test changes.
# python
# LC 27 - Remove Element
# IDEA: copy every element that is not val down to the write slot
# time = O(n), space = O(1)
def removeElement(nums, val):
slow = 0 # next free write slot
for fast in range(len(nums)):
if nums[fast] != val:
nums[slow] = nums[fast]
slow += 1
return slow
// java
// LC 27 - Remove Element
// IDEA: copy every element that is not val down to the write slot
// time = O(n), space = O(1)
public int removeElement(int[] nums, int val) {
int slow = 0; // next free write slot
for (int fast = 0; fast < nums.length; fast++) {
if (nums[fast] != val) {
nums[slow] = nums[fast];
slow++;
}
}
return slow;
}
Template 2: Converging Pointers from Both Ends — LC 11 Priority 5 of 5 — Must know — expect it in almost every loop
Recognise — a sorted array and “a pair whose sum is …”, “two lines that hold the most water”, “is it a palindrome”, “reverse in place”. n ≤ 10⁵ → O(n) after an optional O(n log n) sort.
Key point — every step discards one end for good, and only pairs that cannot beat the current one are discarded. For LC 11, the shorter wall caps every pair it is part of, and every remaining partner is closer, so no pair using the shorter wall can beat the one just measured.
# python
# LC 11 - Container With Most Water
# IDEA: measure the widest pair, then drop the shorter wall: it cannot do better with a narrower partner
# time = O(n), space = O(1)
def maxArea(height):
left = 0
right = len(height) - 1
best = 0
while left < right:
width = right - left
best = max(best, min(height[left], height[right]) * width)
if height[left] < height[right]: # left is the bottleneck: discard it
left += 1
else: # right is (or ties): discard it
right -= 1
return best
// java
// LC 11 - Container With Most Water
// IDEA: measure the widest pair, then drop the shorter wall: it cannot do better with a narrower partner
// time = O(n), space = O(1)
public int maxArea(int[] height) {
int left = 0;
int right = height.length - 1;
int best = 0;
while (left < right) {
int width = right - left;
best = Math.max(best, Math.min(height[left], height[right]) * width);
if (height[left] < height[right]) {
left++; // left is the bottleneck: discard it
} else {
right--;
}
}
return best;
}
Complexity
- Time O(n): each pass of
while left < rightmoves exactly one pointer inward, so the loop runs n − 1 times. - Space O(1): two indices and the running best.
Traps
- Moving the taller wall:
[1,8,6,2,5,4,8,3,7]returns 8 instead of 49 — the shorter wall stays and keeps capping every pair. while left <= rightpairs an index with itself. Harmless for LC 11 (width 0), wrong for LC 167:numbers = [2,3,4], target 4 returns[1,1], using the2twice.- An unsorted input for a pair-sum: the “too small → move left” rule needs sorted order. Sort first, or use a hash map when the original indices are the answer.
Not this if — unsorted input with indices as the answer → complement lookup in hash_map.md (LC 1); a contiguous range with a condition → sliding_window.md.
Drill — LC 11 ⭐ · LC 344 (reverse: swap, then move both) · LC 125 ⭐ (skip non-alphanumerics
before comparing) · LC 167 (sum too small → left++) · LC 15 (fix one, converge the rest — see
n_sum.md) · LC 42 (move the side with the lower max)
Valid Palindrome — LC 125 is this card with skips: each side first steps past the characters that do not count, then the pair is compared. It is worked as string.md § Template 1; one deletion allowed (LC 680) is the same scan branching twice at the first mismatch, in palindrome.md.
Remove Element, Bidirectional Variant — LC 27
Key point — a
valatlis overwritten bynums[r]andrshrinks, butlstays put: the value just copied in may itself beval. Whenl > r,lis the count of kept elements.
Reduces to: Template 2 — converging ends, used as a partition instead of a pair search.
Key difference from the fast-slow (Template 1) approach:
- Fast-slow overwrites sequentially → preserves relative order
- Bidirectional replaces
nums[l]withnums[r]→ does NOT preserve order, but may do fewer writes (good whenvalis rare)
// java
// LC 27 Remove Element - Bidirectional variant
// IDEA: a val at l is overwritten by the rightmost element; l only advances past a kept element
// time = O(n), space = O(1)
/**
* Key Idea:
* - l starts at 0, r starts at nums.length - 1
* - If nums[l] == val, OVERWRITE it with nums[r] and shrink r
* (do NOT advance l yet — the new nums[l] might also be val)
* - If nums[l] != val, it is a "good" element → advance l
* - When l > r, l equals the count of valid elements
*
* //--------------------
* Example 1
* //--------------------
* nums = [3,2,2,3], val = 3
*
* [3,2,2,3] nums[l]=3==val, nums[l]=nums[r]=3, r--
* l r
*
* [3,2,2,3] nums[l]=3==val, nums[l]=nums[r]=2, r--
* l r
*
* [2,2,2,3] nums[l]=2!=val, l++
* l r
*
* [2,2,2,3] nums[l]=2!=val, l++
* lr
*
* l(2) > r(1), return l = 2
*
* //--------------------
* Example 2
* //--------------------
* nums = [0,1,2,2,3,0,4,2], val = 2
*
* [0,1,2,2,3,0,4,2] nums[l]=0!=val, l++
* l r
*
* [0,1,2,2,3,0,4,2] nums[l]=1!=val, l++
* l r
*
* [0,1,2,2,3,0,4,2] nums[l]=2==val, nums[l]=nums[r]=2, r--
* l r
*
* [0,1,2,2,3,0,4,2] nums[l]=2==val, nums[l]=nums[r]=4, r--
* l r
*
* [0,1,4,2,3,0,4,2] nums[l]=4!=val, l++
* l r
*
* [0,1,4,2,3,0,4,2] nums[l]=2==val, nums[l]=nums[r]=0, r--
* l r
*
* [0,1,4,0,3,0,4,2] nums[l]=0!=val, l++
* l r
*
* [0,1,4,0,3,0,4,2] nums[l]=3!=val, l++
* l r
*
* l(5) > r(4), return l = 5
*/
public int removeElement(int[] nums, int val) {
int l = 0;
int r = nums.length - 1;
while (l <= r) {
if (nums[l] == val) {
// Overwrite with rightmost element, shrink right boundary
// do NOT advance l — the new nums[l] might also be val
nums[l] = nums[r];
r--;
} else {
// Good element confirmed, advance left
l++;
}
}
// l is exactly the count of non-val elements
return l;
}
Comparison: Fast-Slow vs Bidirectional
| Aspect | Fast-Slow (Template 1) | Bidirectional (this) |
|---|---|---|
| Order | Preserves relative order | Does NOT preserve order |
| Writes | One write per valid element | Fewer writes when val is rare |
| Loop style | for loop (fast advances always) |
while (l <= r) |
| When to use | Order matters | Order doesn’t matter, minimal writes |
Similar Problems:
- LC 27 Remove Element (this pattern)
- LC 905 Sort Array By Parity — move evens left, odds right (same bidirectional shrink idea)
- LC 75 Sort Colors (Dutch National Flag) — three-way bidirectional partition
- LC 283 Move Zeroes — order matters, use fast-slow instead
- LC 26 Remove Duplicates from Sorted Array — order matters, use fast-slow instead
- LC 80 Remove Duplicates from Sorted Array II — order matters, use fast-slow instead
Reverse an Array
The bare skeleton of the template: no pair to evaluate, just swap and move both.
// java
// IDEA: swap the two ends, move both inward until they meet
// time = O(n), space = O(1)
void reverse(int[] nums) {
int left = 0;
int right = nums.length - 1;
while (left < right) {
int tmp = nums[left];
nums[left] = nums[right];
nums[right] = tmp;
left++;
right--;
}
}
Template 3: Expand from Centre — LC 5 Priority 4 of 5 — High value — a gap here costs you rounds
Recognise — “longest palindromic substring”, “count the palindromic substrings”. n ≤ 1000 → O(n²) is the intended bound.
Key point — every palindrome is the expansion of its centre, and there are only 2n − 1 centres: n characters (odd length) and n − 1 gaps (even length). Expanding each centre while the two ends match visits every palindromic substring exactly once.
# python
# LC 5 - Longest Palindromic Substring
# IDEA: try all 2n - 1 centres (each char, each gap); expand while the ends match; keep the widest
# time = O(n^2), space = O(1)
def longestPalindrome(s):
def expand(left, right):
# grow while s[left..right] is a palindrome
while left >= 0 and right < len(s) and s[left] == s[right]:
left -= 1
right += 1
# the loop stopped one step too far on each side
return left + 1, right # half-open [start, end)
best_start, best_end = 0, 0
for center in range(len(s)):
for left, right in ((center, center), (center, center + 1)): # odd, then even
start, end = expand(left, right)
if end - start > best_end - best_start:
best_start, best_end = start, end
return s[best_start:best_end]
// java
// LC 5 - Longest Palindromic Substring
// IDEA: try all 2n - 1 centres (each char, each gap); expand while the ends match; keep the widest
// time = O(n^2), space = O(1)
public String longestPalindrome(String s) {
int bestStart = 0;
int bestEnd = 0; // half-open [bestStart, bestEnd)
for (int center = 0; center < s.length(); center++) {
int[] odd = expand(s, center, center);
int[] even = expand(s, center, center + 1);
if (odd[1] - odd[0] > bestEnd - bestStart) {
bestStart = odd[0];
bestEnd = odd[1];
}
if (even[1] - even[0] > bestEnd - bestStart) {
bestStart = even[0];
bestEnd = even[1];
}
}
return s.substring(bestStart, bestEnd);
}
private int[] expand(String s, int left, int right) {
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
left--;
right++;
}
return new int[]{left + 1, right}; // undo the last failed step
}
Complexity
- Time O(n²): the
for centerloop runs 2n − 1 expansions, and eachwhileinsideexpandcan run up to n/2 steps (all-equal input"aaaa"). - Space O(1): only indices are kept; the substring is sliced once at the end.
Dry run
s = "cbbd"
center 0 odd "c" even c|b stop best "c"
center 1 odd "b" even b|b -> "bb", then c|d stop best "bb"
center 2 odd "b" even b|d stop
center 3 odd "d"
return "bb"
Traps
- Odd centres only:
"cbbd"returns"c"— the even centre between the twobs is never tried. - The
whileexits one step past the palindrome on each side; readings[left:right + 1]after it includes two characters that do not match. Useleft + 1andright(half-open). - Slicing a new string on every successful step turns O(n²) into O(n³); compare lengths, slice once.
Not this if — palindromic subsequence (characters may be skipped) → interval DP (LC 516); n ≤ 10⁵ needs Manacher’s O(n). The palindrome family in depth is palindrome.md.
Drill — LC 5 ⭐ · LC 647 ⭐ (count: add 1 on every successful expand step) · LC 680 (converging pointers with one skip — Template 2) · LC 516 (subsequence → DP; the look-alike)
Full solutions for LC 5 and LC 647 are in 2_pointers_examples.md.
Template 4: Subsequence Matching, One Pointer Always Moves — LC 392 Priority 4 of 5 — High value — a gap here costs you rounds
Recognise — “is s a subsequence of t”, “can be formed by deleting some characters
without changing the order”, “does the query match the pattern”. len(t) ≤ 10⁴ → one O(|t|) scan.
Key point — matching s[i] with the earliest equal t[j] is never worse than a later one: it
leaves the longest possible suffix of t for s[i+1:]. So j moves every step and i moves only on
a match, and i == len(s) at the end means every character was placed in order.
# python
# LC 392 - Is Subsequence
# IDEA: j scans t every step; i advances only when s[i] matches (greedy earliest match)
# time = O(|t|), space = O(1)
def isSubsequence(s, t):
i = 0 # next character of s to place
for j in range(len(t)): # t's pointer always moves
if i < len(s) and s[i] == t[j]:
i += 1 # matched: move s's pointer too
return i == len(s)
// java
// LC 392 - Is Subsequence
// IDEA: j scans t every step; i advances only when s[i] matches (greedy earliest match)
// time = O(|t|), space = O(1)
public boolean isSubsequence(String s, String t) {
int i = 0; // next character of s to place
for (int j = 0; j < t.length(); j++) { // t's pointer always moves
if (i < s.length() && s.charAt(i) == t.charAt(j)) {
i++;
}
}
return i == s.length();
}
Complexity
- Time O(|t|): the
for jloop visits each character oftonce;ionly follows. - Space O(1): two indices.
Dry run
s = "abc", t = "ahbgdc"
j=0 'a' == s[0] i=1
j=1 'h' i=1
j=2 'b' == s[1] i=2
j=3 'g' j=4 'd' i=2
j=5 'c' == s[2] i=3 == len(s) -> true
Traps
- Reading
s[i]withouti < len(s):s = "a",t = "aa"indexes past the end ofson the seconda. - Returning
j == len(t)instead ofi == len(s):jalways reaches the end, so"axc"in"ahbgdc"would return true. s = ""is a subsequence of anything, including""; the loop must not require a match to return true.- Many queries against one
t(LC 392 follow-up, LC 792): rescanningtper query is O(k·|t|). Precompute each character’s sorted index list and binary-search the next position.
Not this if — a contiguous match (substring) → KMP or a window; the longest common subsequence of two strings → DP (LC 1143), because there the greedy choice is no longer safe.
Drill — LC 392 ⭐ · LC 524 (test each dictionary word with this scan) · LC 1023 (an unmatched
t character must be lowercase) · LC 809 (advance by groups of equal characters) · LC 792 (many
queries: index lists + binary search)
Template 5: Merge Two Sorted Arrays, Fill from the Back — LC 88 Priority 5 of 5 — Must know — expect it in almost every loop
Recognise — “merge nums2 into nums1 in-place”, “nums1 has length m + n, the last n are
0 and should be ignored”. m + n ≤ 200, but the point is O(1) extra space.
Key point — write the largest remaining value into the last free slot. The write index
write = p1 + p2 + 1 is always ≥ p1, so a slot is only written after it has been read (or was
empty): merging backward needs no buffer, merging forward would overwrite unread nums1 values.
# python
# LC 88 - Merge Sorted Array
# IDEA: two read pointers at the ends of the valid parts, one write pointer at the very end;
# always write the larger tail. Loop on p2 only: leftover nums1 is already in place.
# time = O(m + n), space = O(1)
def merge(nums1, m, nums2, n):
p1 = m - 1 # read nums1 from the end of its valid part
p2 = n - 1 # read nums2 from its end
write = m + n - 1 # write from the very end of nums1
while p2 >= 0:
if p1 >= 0 and nums1[p1] > nums2[p2]:
nums1[write] = nums1[p1]
p1 -= 1
else: # nums2's tail is larger or equal, or nums1 is used up
nums1[write] = nums2[p2]
p2 -= 1
write -= 1
// java
// LC 88 - Merge Sorted Array
// IDEA: write the larger tail into the last free slot; loop until nums2 is used up
// time = O(m + n), space = O(1)
public void merge(int[] nums1, int m, int[] nums2, int n) {
int p1 = m - 1; // read nums1
int p2 = n - 1; // read nums2
int write = m + n - 1; // write slot
while (p2 >= 0) {
if (p1 >= 0 && nums1[p1] > nums2[p2]) {
nums1[write] = nums1[p1];
p1--;
} else {
nums1[write] = nums2[p2];
p2--;
}
write--;
}
}
Complexity
- Time O(m + n): each pass of
while p2 >= 0writes one slot and moveswriteleft; at most m + n slots exist. - Space O(1): the merge happens in
nums1’s own empty tail.
Dry run
nums1 = [1, 2, 3, 0, 0, 0], m = 3
nums2 = [2, 5, 6], n = 3
| Step | p1 | p2 | write | Compare | Action | nums1 |
|------|----|----|-------|-------------------------|---------|----------------|
| init | 2 | 2 | 5 | — | setup | [1,2,3,0,0,0] |
| 1 | 2 | 2 | 5 | nums1[2]=3 < nums2[2]=6 | write 6 | [1,2,3,0,0,6] |
| 2 | 2 | 1 | 4 | nums1[2]=3 < nums2[1]=5 | write 5 | [1,2,3,0,5,6] |
| 3 | 2 | 0 | 3 | nums1[2]=3 > nums2[0]=2 | write 3 | [1,2,3,3,5,6] |
| 4 | 1 | 0 | 2 | nums1[1]=2 == nums2[0]=2| write 2 | [1,2,2,3,5,6] |
| 5 | 1 | -1 | 1 | p2 < 0 | STOP | [1,2,2,3,5,6] |
nums1[0..1] = [1,2] is already in place -> no flush step
Traps
- Merging left → right overwrites unread values:
nums1 = [4,5,6,0,0,0],nums2 = [1,2,3]writes1over the4before reading it. while p1 >= 0 and p2 >= 0with no flush:nums1 = [0], m = 0,nums2 = [1]never copies the1. Loop onp2alone, or addnums1[:p2 + 1] = nums2[:p2 + 1]afterwards.- Reading
nums1[p1]withoutp1 >= 0: in Pythonnums1[-1]silently reads the last slot instead of failing. nums1 = sorted(nums1 + nums2)rebinds the local name; the caller’s array is unchanged.- Starting
writeatm - 1:nums1has m + n slots; the first write must bem + n - 1.
Not this if — two linked lists: there is no empty tail to fill, so merge forward with a dummy head (LC 21); k sorted inputs → a heap (LC 23).
Drill — LC 88 ⭐ · LC 977 (largest square is at one of the two ends: fill a result backward) · LC 350 (two sorted pointers, keep the common values) · LC 986 (advance the interval that ends first) · LC 4 (the merge only conceptually — binary search the partition instead)
Template 6: Three-Way Partition, Dutch National Flag — LC 75 Priority 4 of 5 — High value — a gap here costs you rounds
Recognise — “sort an array of 0s, 1s and 2s in one pass, constant space”, “partition around a pivot into less / equal / greater”. O(1) space, a single pass.
Key point — four regions are kept true at every step: nums[0..low-1] = 0,
nums[low..mid-1] = 1, nums[mid..high] = unseen, nums[high+1..] = 2. Each step shrinks the
unseen region by one, so the loop ends when mid > high.
# python
# LC 75 - Sort Colors
# IDEA: 0 -> swap into the low region, 2 -> swap into the high region, 1 -> leave in place
# time = O(n), space = O(1)
def sortColors(nums):
low, mid, high = 0, 0, len(nums) - 1
while mid <= high: # nums[mid..high] is still unseen
if nums[mid] == 0:
nums[low], nums[mid] = nums[mid], nums[low]
low += 1
mid += 1 # what came from low is a 1 (or mid itself): already seen
elif nums[mid] == 1:
mid += 1
else:
nums[mid], nums[high] = nums[high], nums[mid]
high -= 1 # do NOT move mid: the value from high is unseen
// java
// LC 75 - Sort Colors
// IDEA: 0 -> swap into the low region, 2 -> swap into the high region, 1 -> leave in place
// time = O(n), space = O(1)
public void sortColors(int[] nums) {
int low = 0; // next slot for a 0
int mid = 0; // current element
int high = nums.length - 1; // next slot for a 2
while (mid <= high) {
if (nums[mid] == 0) {
swap(nums, low, mid);
low++;
mid++;
} else if (nums[mid] == 1) {
mid++;
} else {
swap(nums, mid, high);
high--; // the swapped-in value is unseen: check it next round
}
}
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
Complexity
- Time O(n): every pass of
while mid <= higheither movesmidright orhighleft, so the unseen region shrinks by one each time. - Space O(1): three indices; the swaps are in place.
Dry run
nums = [2,0,2,1,1,0] low mid high
mid=0 is 2 -> swap with high [0,0,2,1,1,2] 0 0 4
mid=0 is 0 -> swap with low [0,0,2,1,1,2] 1 1 4
mid=1 is 0 -> swap with low [0,0,2,1,1,2] 2 2 4
mid=2 is 2 -> swap with high [0,0,1,1,2,2] 2 2 3
mid=2 is 1 2 3 3
mid=3 is 1 2 4 3 mid > high: done
Traps
- Advancing
midafter the swap withhigh:[1,2,0]ends as[1,0,2]— the0that arrived from the right is never examined. while mid < high:[1,0]stops withmid == highunexamined and stays[1,0].
Not this if — the relative order must be kept (a stable partition): a stable list partition (LC 86) is linked_list.md’s two dummies, and an array keeps order with Template 1’s fast/slow (LC 283).
Drill — LC 75 ⭐ · LC 905 (two groups: one swap pointer is enough) · LC 2161 (stable — needs a second pass or an extra array) · LC 215 (the same 3-way partition inside quickselect, see quickselect.md)
Template 7: Fast/Slow Cycle Detection — LC 141 Priority 4 of 5 — High value — a gap here costs you rounds
Recognise — “does the linked list have a cycle”, “return the node where the cycle begins”, “find the duplicate without modifying the array, O(1) extra space”. O(1) memory rules out a visited set.
Key point — fast moves two steps, slow one. Once both are inside a cycle of length c, the gap
between them shrinks by one per step, so they meet within c steps; with no cycle fast reaches None.
For the entry (LC 142): head-to-entry distance ≡ meet-to-entry distance (mod c), so a pointer from
head and one from the meeting point, both moving one step, meet exactly at the entry.
# python
# LC 141 - Linked List Cycle; LC 142 - Linked List Cycle II (the entry node)
# IDEA: slow 1 step, fast 2 steps -> they meet iff there is a cycle; then head and meet point
# walk 1 step each and meet at the entry
# time = O(n), space = O(1)
def hasCycle(head):
slow = fast = head
while fast is not None and fast.next is not None:
slow = slow.next
fast = fast.next.next
if slow is fast: # identity, not value
return True
return False
def detectCycle(head):
slow = fast = head
while fast is not None and fast.next is not None:
slow = slow.next
fast = fast.next.next
if slow is fast: # inside the cycle: now find where it starts
entry = head
while entry is not slow:
entry = entry.next
slow = slow.next
return entry
return None # fast fell off the end: no cycle
// java
// LC 141 - Linked List Cycle; LC 142 - Linked List Cycle II (the entry node)
// IDEA: slow 1 step, fast 2 steps -> they meet iff there is a cycle; then head and meet point
// walk 1 step each and meet at the entry
// time = O(n), space = O(1)
public boolean hasCycle(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
return true;
}
}
return false;
}
public ListNode detectCycle(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
ListNode entry = head;
while (entry != slow) {
entry = entry.next;
slow = slow.next;
}
return entry;
}
}
return null; // no cycle: the guard above is what prevents an NPE
}
Complexity
- Time O(n): before the meet,
slowtakes at most n steps (tail length + one lap); the entry walk is at most another n. - Space O(1): two or three node references.
Traps
- Checking only
fast.next: an acyclic[1,2]makesfastNoneafter one jump, andfast.nextthrows. Testfastfirst, thenfast.next. - LC 142 on an acyclic list must return
None; running the entry walk unconditionally (no meet happened) dereferencesnull. - Starting
fast = head.nextchanges the distances: on[3,2,0,-4](tail to index 1) the head/meet walk never reaches the entry and loops forever. - Comparing
slow.val == fast.valreports a cycle on the acyclic[1,1,1]. Compare the nodes.
Not this if — O(n) memory is allowed and clarity matters → a visited set; the node-based family (middle node, nth from end, reorder, palindrome list) is 2_pointers_linkedlist.md.
Drill — LC 141 ⭐ · LC 876 (stop when fast ends: slow is the middle) · LC 202 (an implicit
list: n → sum of squared digits) · LC 142 (the entry walk) · LC 287 (the array as a list
i → nums[i]; the duplicate is the entry)
Template 8: Grow Right, Then Shrink Left — LC 567
Recognise — “does s2 contain a permutation of s1”, “a window that grows on one condition and
shrinks on another”. n ≤ 10⁴. This is the sliding-window skeleton, kept here as the two-pointer view
of it; sliding_window.md owns the family.
Key point — right only grows and left only grows, so each index enters and leaves the window
once. After the while the window is never longer than s1, so comparing counts there tests every
length-len(s1) window exactly once.
# python
# LC 567 - Permutation in String
# IDEA: grow right; shrink left until the window is len(s1) long; equal counts = a permutation
# time = O(26 * n), space = O(26)
def checkInclusion(s1, s2):
need = [0] * 26
for ch in s1:
need[ord(ch) - ord('a')] += 1
window = [0] * 26
left = 0
for right in range(len(s2)):
window[ord(s2[right]) - ord('a')] += 1 # grow
while right - left + 1 > len(s1): # too long -> shrink
window[ord(s2[left]) - ord('a')] -= 1
left += 1
if window == need:
return True
return False
// java
// LC 567 - Permutation in String
// IDEA: grow right; shrink left until the window is len(s1) long; equal counts = a permutation
// time = O(26 * n), space = O(26)
public boolean checkInclusion(String s1, String s2) {
int[] need = new int[26];
for (int i = 0; i < s1.length(); i++) {
need[s1.charAt(i) - 'a']++;
}
int[] window = new int[26];
int left = 0;
for (int right = 0; right < s2.length(); right++) {
window[s2.charAt(right) - 'a']++; // grow
while (right - left + 1 > s1.length()) { // too long -> shrink
window[s2.charAt(left) - 'a']--;
left++;
}
if (Arrays.equals(window, need)) {
return true;
}
}
return false;
}
Complexity
- Time O(26·n):
rightandlefteach crosss2once (the innerwhileis amortised O(1)); the 26-slot comparison runs once perright. - Space O(26): the two count arrays.
Traps
len(s1) > len(s2)("abc","ab"): the loop never reaches a full window and correctly returns false — do not slices2[:len(s1)]up front and assume it exists.- Testing
window == needbefore the shrink compares a window one character too long.
Not this if — the window size is not fixed and the condition is a sum or a count → the variable-window cards in sliding_window.md; negative values in a sum → prefix sum.
Drill — LC 567 ⭐ · LC 438 (collect every start instead of returning) · LC 209 (shrink while the sum is still ≥ target) · LC 76 (have/need counter — sliding_window’s card)
Template 9: Two Sequences in Lockstep — LC 165 Priority 4 of 5 — High value — a gap here costs you rounds
Recognise — “compare version1 and version2”, “are these two strings one edit apart”, “compare after applying backspaces”, “intersect two sorted lists of intervals”. Two inputs, each read once from its own pointer, one token per round: a revision number, an effective character, an interval. O(n + m) time and O(1) space asked.
Key point — each round consumes exactly one token from each side and compares them; the first
unequal pair decides the answer, so nothing before it is ever revisited. Looping while either
side has input left and letting an exhausted side yield a neutral token (0, an empty string) is
what makes unequal lengths a non-case.
# python
# LC 165 - Compare Version Numbers
# IDEA: i walks version1, j walks version2; each round parses one revision from each side
# (an exhausted side yields 0) and the first unequal pair decides
# time = O(n1 + n2), space = O(1)
def compareVersion(version1, version2):
i, j = 0, 0
n1, n2 = len(version1), len(version2)
while i < n1 or j < n2: # OR: the shorter side keeps yielding 0
a = 0
while i < n1 and version1[i] != ".":
a = a * 10 + int(version1[i]) # leading zeros vanish here
i += 1
b = 0
while j < n2 and version2[j] != ".":
b = b * 10 + int(version2[j])
j += 1
if a != b:
return -1 if a < b else 1
i += 1 # step over the '.' (harmless past the end)
j += 1
return 0
// java
// LC 165 - Compare Version Numbers
// IDEA: i walks version1, j walks version2; each round parses one revision from each side
// (an exhausted side yields 0) and the first unequal pair decides
// time = O(n1 + n2), space = O(1)
public int compareVersion(String version1, String version2) {
int i = 0, j = 0;
int n1 = version1.length(), n2 = version2.length();
while (i < n1 || j < n2) {
int a = 0;
while (i < n1 && version1.charAt(i) != '.') {
a = a * 10 + (version1.charAt(i) - '0');
i++;
}
int b = 0;
while (j < n2 && version2.charAt(j) != '.') {
b = b * 10 + (version2.charAt(j) - '0');
j++;
}
if (a != b) {
return Integer.compare(a, b);
}
i++;
j++;
}
return 0;
}
Complexity
- Time O(n1 + n2): the two inner
whileloops moveiandjforward only, and the outer loop ends when both have passed their string. - Space O(1): two pointers and two parsed integers; no
split.
Dry run — version1 = "1.01", version2 = "1.001.1"
round a (from version1) b (from version2) result
1 1 1 equal, continue
2 "01" -> 1 "001" -> 1 equal, continue
3 exhausted -> 0 1 0 < 1 -> return -1
Traps
while i < n1 and j < n2(AND) stops at the shorter string:"1.0.1"vs"1"returns 0 instead of 1.- Comparing revisions as strings:
"1.10"vs"1.2"—"10" < "2"as text. Parse to integers, which also makes"01"equal"001". split(".")plus zero-padding is shorter in Python and fine to mention, but it is O(n) extra space; the pointer form is the O(1) answer.
Not this if — the two sequences may be matched out of order or skipped arbitrarily → a subsequence check (Template 4) or LCS DP; both inputs sorted and the goal is one merged output → Template 5.
Drill — LC 165 ⭐ · LC 844 (the token is the next character that survives the backspaces — walk both strings from the back) · LC 161 (stop at the first mismatch, compare the suffixes) · LC 953 (adjacent words, a rank map — hash_map.md) · LC 986 (intervals: advance the one that ends first — intervals.md)
Drill Path
| Stage | Problems (easy → hard) | What it adds |
|---|---|---|
| Core ⭐ | LC 125 no verdict · LC 141 ok · LC 11 ok · LC 5 again · LC 647 again | converging with skips, Floyd, the shorter-wall argument, both centre kinds, counting by expansion |
| Practice | LC 26 no verdict · LC 88 again · LC 392 ok · LC 167 ok · LC 75 ok · LC 142 ok · LC 15 ok | compaction, backward merge, greedy subsequence, sorted pair, three regions, the cycle entry, fix-one + converge (n_sum.md) |
| Stretch | LC 80 again · LC 881 no verdict · LC 287 ok · LC 42 no verdict | at most k copies, greedy pairing after a sort, Floyd on an array, two running maxima |
Summary & Quick Reference
Pattern Selection Table
Templates 1–9 are routed by the Recognition table and drilled in the order of the Drill Path. The patterns below are two-pointer problems that live in another sheet:
| Pattern | When to Use | Example Problems |
|---|---|---|
| Sliding Window | Subarray/substring problems | LC 3, LC 76, LC 209 — sliding_window.md |
| Fixed + Two Pointers (exact / closest) | Sum == target, or nearest to it | LC 15, LC 18, LC 16, LC 259 — n_sum.md |
| Palindrome with Deletion | Allow k changes | LC 680, LC 1216 — palindrome.md |
| Longest Palindromic Prefix | Find longest palindromic prefix, prepend reversed suffix | LC 214, LC 336 — palindrome.md |
| Last-Occurrence + Expanding End | Greedy partition; cut when i == end |
LC 763 — string.md |
| Length-Prefixed (Encode/Decode) | Parse len#word blocks; i jumps by declared length |
LC 271, LC 297 — 2_pointers_examples.md |
| Converging Low/High (build permutation) | Greedy: consume smallest/largest available per signal | LC 942 — 2_pointers_examples.md |
| Staircase (sorted 2D matrix) | Search grid sorted by row AND column | LC 240 — 2_pointers_examples.md |
| 3-Phase Interval Scan | Insert/merge one interval into a sorted list | LC 57, LC 56 — intervals.md |
Classic Problems by Difficulty
Easy
- LC 26 Remove Duplicates from Sorted Array
- LC 27 Remove Element
- LC 125 Valid Palindrome
- LC 283 Move Zeroes
- LC 344 Reverse String
- LC 345 Reverse Vowels of a String
- LC 349 Intersection of Two Arrays
- LC 350 Intersection of Two Arrays II
- LC 392 Is Subsequence
- LC 680 Valid Palindrome II
- LC 844 Backspace String Compare
- LC 942 DI String Match
- LC 953 Verifying an Alien Dictionary
- LC 977 Squares of a Sorted Array
- LC 14 Longest Common Prefix (lockstep char scan across all strings)
Medium
- LC 3 Longest Substring Without Repeating Characters (Sliding Window)
- LC 5 Longest Palindromic Substring
- LC 11 Container With Most Water
- LC 15 3Sum
- LC 16 3Sum Closest
- LC 18 4Sum
- LC 75 Sort Colors (Dutch National Flag)
- LC 80 Remove Duplicates from Sorted Array II
- LC 86 Partition List
- LC 88 Merge Sorted Array
- LC 142 Linked List Cycle II
- LC 165 Compare Version Numbers
- LC 167 Two Sum II - Input Array Is Sorted
- LC 240 Search a 2D Matrix II (staircase two pointers)
- LC 57 Insert Interval (3-phase pointer scan)
- LC 763 Partition Labels
- LC 209 Minimum Size Subarray Sum (Sliding Window)
- LC 287 Find the Duplicate Number
- LC 567 Permutation in String (Sliding Window)
- LC 647 Palindromic Substrings
- LC 713 Subarray Product Less Than K
- LC 881 Boats to Save People
- LC 986 Interval List Intersections
- LC 1023 Camelcase Matching
Hard
- LC 42 Trapping Rain Water
- LC 76 Minimum Window Substring (Sliding Window)
- LC 214 Shortest Palindrome
- LC 828 Count Unique Characters of All Substrings
Interview Tips
| Signal | Pattern |
|---|---|
| “sort + find pair” | Left-right pointers after sorting |
| “in-place remove/deduplicate” | Slow-fast write pointer |
| “cycle in linked list” | Tortoise and hare |
| “partition into 3 groups” | Dutch national flag |
| “merge sorted in-place” | Fill from back |
| “palindrome check” | Pointers from both ends |
| “maximum area / container” | Shrink shorter side |
Related Sheets
- binary_search.md — left/right pointers halving a range instead of scanning it
- sliding_window.md — the condition-driven window
- n_sum.md — fixed element + converging pointers (LC 15, LC 16, LC 18)
- 2_pointers_examples.md — worked LC catalogue
- quickselect.md — Kth element by partition