Arrays & Strings — Two Pointers / Sliding Window
Topic 1 of 7, with 3 concept checks. Recognizing when to use two pointers or a sliding window
Shrink the search space with moving boundaries
Array boundaries
Define what each pointer or window boundary means, state the invariant it preserves, and justify every movement so array and string scans remain linear and explainable.
Core lesson 01
Reach for two pointers when the array is sorted (or can be sorted) and you need pairs/triples satisfying a condition, or when comparing from both ends (palindromes) - turns an O(n^2) nested loop into O(n).
The two-pointer pattern maintains two indices moving toward each other (or in the same direction at different speeds) based on a comparison, eliminating the need to check every pair explicitly. It applies whenever moving one pointer safely rules out a whole range of possibilities - most commonly on sorted arrays (Two Sum II, 3Sum) or symmetric checks (palindromes).
def two_sum_sorted(nums, target):
left, right = 0, len(nums) - 1
while left < right:
s = nums[left] + nums[right]
if s == target:
return [left, right]
elif s < target:
left += 1
else:
right -= 1
return []What to remember
How do you recognize a two-pointer problem?
Common footguns
- Trying two pointers on an unsorted array without sorting first (or realizing sorting changes required output, like original indices) - the pattern relies on monotonic movement being meaningful.
Core lesson 02
Sliding window applies to contiguous subarray/substring problems (longest, shortest, count matching a condition) - grow from the right, shrink from the left whenever the constraint is violated, tracking the best result.
A brute-force approach checks every subarray, O(n^2) or worse. A sliding window maintains a valid (or trackable) range [left, right], expanding right by one each step and updating running state (sum, character counts); when that state violates the constraint, it shrinks from the left until valid again. Each pointer moves forward at most n times total, giving O(n).
def longest_unique_substring(s):
seen = set()
left = 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 bestWhat to remember
How do you recognize a sliding window problem, and what's the core template?
Common footguns
- Shrinking the window with an if instead of a while when multiple violations can stack up - an if only fixes one step of invalidity, a while fully restores validity.
Core lesson 03
Use a hash map when the array is unsorted and you can't (or shouldn't) sort it - e.g. when the answer needs original indices - trading O(n) space for avoiding the O(n log n) sort two pointers would otherwise require.
Two Sum is the canonical example: on a sorted array, two pointers solves it in O(n) after an O(n log n) sort - but if you need the ORIGINAL indices of the two numbers, sorting destroys that information, so a hash map (value -> original index) solves it in a single O(n) pass without ever sorting.
def two_sum_indices(nums, target):
seen = {} # value -> index
for i, num in enumerate(nums):
complement = target - num
if complement in seen:
return [seen[complement], i]
seen[num] = i
return []What to remember
When should you reach for a hash map instead of two pointers, even on similar-looking problems?
Common footguns
- Sorting an array to use two pointers, then returning the sorted positions instead of the original indices the problem actually asked for.
Python lab
Browser Python lab
Runtime · idle
Python loads on your first run. Your code stays in this browser.
Best practices
- Two pointers: sorted data, pair/triple sums, palindromes, removing duplicates in place.
- Sliding window: contiguous subarray/substring with a size, sum, or character-set constraint.
- Hash map: unsorted data, need O(1) lookups, or must preserve original order/indices.
- Always ask: does sorting lose information the answer needs (like original indices)?
Apply the concept in Interview practice
Two SumeasyLeetCode #1 · O(n) time
Hash map of value to index, single pass, checking for the complement before inserting.
Open problem3SummediumLeetCode #15 · O(n^2) time
Sort the array, fix one number, then two-pointer the rest for pairs summing to its negation, skipping duplicates at every level.
Open problemContainer With Most WatermediumLeetCode #11 · O(n) time
Two pointers from both ends; always move the pointer at the shorter line inward, since moving the taller one can never increase the area.
Open problemLongest Substring Without Repeating CharactersmediumLeetCode #3 · O(n) time
Sliding window with a set/dict of characters currently in the window; shrink from the left whenever a repeat is found.
Open problemMinimum Window SubstringhardLeetCode #76 · O(n) time
Sliding window with a character-count dict of the target; expand right until the window contains all required characters, then shrink left while still valid, tracking the smallest valid window.
Open problemTrapping Rain WaterhardLeetCode #42 · O(n) time, O(1) space
Two pointers from both ends tracking the max height seen so far on each side; water at each position is bounded by the smaller of the two maxes.
Open problemConcept checks
How do you recognize a two-pointer problem?
Hint
Look for a sorted array, or a need to compare elements from both ends.
Common signal words: pair, sum, palindrome, sorted, remove duplicates in place.
Answer
Reach for two pointers when the array is sorted (or can be sorted) and you need pairs/triples satisfying a condition, or when comparing from both ends (palindromes) - turns an O(n^2) nested loop into O(n).
The two-pointer pattern maintains two indices moving toward each other (or in the same direction at different speeds) based on a comparison, eliminating the need to check every pair explicitly. It applies whenever moving one pointer safely rules out a whole range of possibilities - most commonly on sorted arrays (Two Sum II, 3Sum) or symmetric checks (palindromes).
def two_sum_sorted(nums, target):
left, right = 0, len(nums) - 1
while left < right:
s = nums[left] + nums[right]
if s == target:
return [left, right]
elif s < target:
left += 1
else:
right -= 1
return []Watch out
- Trying two pointers on an unsorted array without sorting first (or realizing sorting changes required output, like original indices) - the pattern relies on monotonic movement being meaningful.
How do you recognize a sliding window problem, and what's the core template?
Hint
Look for 'contiguous subarray/substring' with a size or sum/condition constraint.
Expand the right edge, and shrink the left edge when the window becomes invalid.
Answer
Sliding window applies to contiguous subarray/substring problems (longest, shortest, count matching a condition) - grow from the right, shrink from the left whenever the constraint is violated, tracking the best result.
A brute-force approach checks every subarray, O(n^2) or worse. A sliding window maintains a valid (or trackable) range [left, right], expanding right by one each step and updating running state (sum, character counts); when that state violates the constraint, it shrinks from the left until valid again. Each pointer moves forward at most n times total, giving O(n).
def longest_unique_substring(s):
seen = set()
left = 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 bestWatch out
- Shrinking the window with an if instead of a while when multiple violations can stack up - an if only fixes one step of invalidity, a while fully restores validity.
When should you reach for a hash map instead of two pointers, even on similar-looking problems?
Hint
Two pointers usually needs sorted (or sortable) data; a hash map doesn't care about order.
If you need to preserve original indices/order, sorting may not be an option.
Answer
Use a hash map when the array is unsorted and you can't (or shouldn't) sort it - e.g. when the answer needs original indices - trading O(n) space for avoiding the O(n log n) sort two pointers would otherwise require.
Two Sum is the canonical example: on a sorted array, two pointers solves it in O(n) after an O(n log n) sort - but if you need the ORIGINAL indices of the two numbers, sorting destroys that information, so a hash map (value -> original index) solves it in a single O(n) pass without ever sorting.
def two_sum_indices(nums, target):
seen = {} # value -> index
for i, num in enumerate(nums):
complement = target - num
if complement in seen:
return [seen[complement], i]
seen[num] = i
return []Watch out
- Sorting an array to use two pointers, then returning the sorted positions instead of the original indices the problem actually asked for.