Two-Pointer and Sliding Window Techniques for Array Problems
A surprising number of array and string problems that look like they need nested loops can be solved in a single pass by moving two indices toward each other or sliding a window across the data.
Published July 6, 2026Both techniques exist to avoid the same trap: a nested loop that recomputes work it already did. If you find yourself writing for i in range(n): for j in range(n): to check every pair or every subarray, it's worth asking whether the inner loop's starting point could instead be tracked with a single variable that only moves forward.
Two pointers on a sorted array
The classic setup: given a sorted array, find two numbers that add up to a target. The brute-force approach checks every pair, which is O(n squared). With two pointers — one at the start, one at the end — you can solve it in one pass:
def two_sum_sorted(nums, target):
left, right = 0, len(nums) - 1
while left < right:
current = nums[left] + nums[right]
if current == target:
return (left, right)
elif current < target:
left += 1
else:
right -= 1
return None
two_sum_sorted([1, 3, 5, 8, 11, 15], 19) # (3, 4) -> 8 + 11
The reasoning is what matters, not just the code: because the array is sorted, if the current sum is too small, moving the left pointer right is the only way to increase it — moving right left could only decrease the sum further. Each pointer moves at most n times total, so the whole thing is O(n) instead of O(n squared), with O(1) extra space.
The same pattern solves "remove duplicates from a sorted array in place" and "reverse an array in place" — anywhere you need to compare or swap elements from opposite ends without allocating a new array.
Fast and slow pointers
A variant moves both pointers in the same direction at different speeds. The canonical use is cycle detection in a linked list: advance a slow pointer one step and a fast pointer two steps per iteration. If there's a cycle, the fast pointer eventually laps the slow one; if there isn't, it hits the end first.
def has_cycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow is fast:
return True
return False
Sliding window: fixed size
Sliding window is really two pointers that define the edges of a contiguous range, expanding and shrinking as you scan. The simplest version keeps the window a fixed size — useful for "find the maximum sum of any k consecutive elements":
def max_sum_fixed_window(nums, k):
window_sum = sum(nums[:k])
best = window_sum
for i in range(k, len(nums)):
window_sum += nums[i] - nums[i - k]
best = max(best, window_sum)
return best
max_sum_fixed_window([2, 1, 5, 1, 3, 2], 3) # 9 -> [5, 1, 3]
The key insight: instead of resumming all k elements every time the window moves, you subtract the element that just left and add the element that just entered. That turns an O(n*k) brute-force scan into O(n).
Sliding window: variable size
The more common interview-style problem has a window that grows and shrinks based on a condition, like "find the length of the longest substring without repeating characters":
def longest_unique_substring(s):
seen = {}
left = 0
best = 0
for right, char in enumerate(s):
if char in seen and seen[char] >= left:
left = seen[char] + 1
seen[char] = right
best = max(best, right - left + 1)
return best
longest_unique_substring("abcabcbb") # 3 -> "abc"
Here the right pointer always advances (the outer loop), and the left pointer only jumps forward when a repeat is found. Because left never moves backward, each character is visited by each pointer at most once, giving O(n) total despite the nested-looking logic.
How to recognize which one applies
Two pointers from opposite ends usually fits problems on sorted data where you're searching for a pair or partitioning around a pivot. Fast/slow pointers fit linked structures and cycle or midpoint problems. Sliding window fits contiguous-subarray or substring problems where you're optimizing a sum, count, or set of distinct elements within a range. If the problem statement includes the word "contiguous" or "subarray," that's usually the strongest signal to reach for a window before anything else.
None of these techniques change the asymptotic lower bound of the problem — you still have to look at every element at least once. What they eliminate is redundant re-examination of elements you've already accounted for, which is exactly the gap between O(n) and O(n squared) in practice.