The first time someone showed me the sliding window technique, my reaction was mild annoyance. I'd been solving a substring problem with nested loops, feeling clever, and they collapsed it into a single pass with two variables. My O(n²) turned into O(n) and I felt slightly robbed.
That's how two pointers and sliding window work. They look like tricks until they click, and then they feel obvious. They're the answer to a huge slice of array and string problems, especially the ones where a hash map would burn memory you don't need to spend.
This is part three of the Senior Engineer Handbook series. We've covered interview strategy and the hashing patterns. Now the two techniques I reach for whenever the input is sorted or the problem mentions a "substring" or "subarray."
Two pointers: walking from both ends
The setup: a sorted array, and you want a pair that satisfies some condition. The naive move is to check every pair, which is O(n²). Two pointers uses the sortedness to skip almost all of that work.
Put one pointer at the start, one at the end. Look at the sum. Too big? The largest element is too heavy, so move the right pointer left. Too small? Move the left pointer right. Every step eliminates a whole set of pairs you no longer need to check.
function twoSumSorted(nums: number[], target: number): [number, number] | null {
let left = 0;
let right = nums.length - 1;
while (left < right) {
const sum = nums[left] + nums[right];
if (sum === target) return [left, right];
if (sum < target) left++; // need a bigger sum, move up from the small end
else right--; // need a smaller sum, move down from the big end
}
return null;
}
Notice this uses zero extra memory. On a sorted array this beats the hash-map version from the last article: same O(n) time, but O(1) space instead of O(n). This is exactly the "the input's sorted, so I'll use two pointers instead of a map" move I mentioned earlier. Saying that in an interview shows you're choosing tools, not reaching for the same one every time.
Two pointers isn't only start-and-end, though. Sometimes both pointers move the same direction at different speeds. That's how you remove duplicates from a sorted array in place, or detect a cycle in a linked list. The unifying idea is: two indices, each moving under its own rule, replacing a nested loop.
Sliding window: the expanding and shrinking frame
Now the one I love. Sliding window is for problems about a contiguous stretch: the longest substring with some property, the smallest subarray that hits a sum, the max of every window of size k.
Picture a frame over the array. You grow it from the right, and when it violates a constraint, you shrink it from the left. The window slides forward, never backtracking, so the whole array is processed in one pass.
Here's the canonical one: longest substring without repeating characters.
function longestUniqueSubstring(s: string): number {
const lastSeen = new Map<string, number>(); // char -> most recent index
let start = 0; // left edge of the window
let best = 0;
for (let end = 0; end < s.length; end++) {
const ch = s[end];
// If we've seen this char inside the current window, jump start past it.
if (lastSeen.has(ch) && lastSeen.get(ch)! >= start) {
start = lastSeen.get(ch)! + 1;
}
lastSeen.set(ch, end);
best = Math.max(best, end - start + 1);
}
return best;
}
The end pointer expands the window every iteration. The moment we hit a repeat, start jumps forward to kick the duplicate out. The window is always valid, and its widest size is the answer. One pass, O(n).
The part that took me longest to trust: start only ever moves forward. It never resets. That's what keeps it linear: each character enters the window once and leaves at most once.
Fixed vs dynamic windows
Two flavors, and knowing which you're in makes the code fall out naturally.
A fixed window has a known size k: "maximum sum of any k consecutive elements." You slide a frame of constant width: add the new element, drop the one that fell off the back.
function maxSumWindow(nums: number[], k: number): number {
let sum = 0;
for (let i = 0; i < k; i++) sum += nums[i]; // first window
let best = sum;
for (let i = k; i < nums.length; i++) {
sum += nums[i] - nums[i - k]; // slide: add front, drop back
best = Math.max(best, sum);
}
return best;
}
That sum += nums[i] - nums[i - k] line is the whole trick. Instead of re-summing k elements every step, you adjust by one in and one out. O(n) instead of O(n·k).
A dynamic window grows and shrinks based on a condition: "smallest subarray with sum ≥ target." You expand until the condition is met, then shrink while it stays met, tracking the best. The unique-substring example above is dynamic. The shape is: expand in the loop, shrink in an inner while.
How to know it's a window problem
The keywords give it away more than almost any other pattern. If I see:
- "longest / shortest / maximum / minimum substring or subarray"
- "contiguous"
- "window of size k"
- "at most / at least k of something"
my hand is already reaching for a sliding window. If instead it's a sorted array and I want pairs or a partition point, it's two pointers. If it's neither sorted nor contiguous, I go back to hashing from the last article.
That triage, narrowing to a pattern from the phrasing before I write anything, is the single habit that made interviews stop feeling like luck.
The mistake almost everyone makes
Backtracking the left pointer. People shrink the window, then later move start backward when a new element comes in, and suddenly it's O(n²) again and they can't figure out why it's slow. The whole point of these techniques is that each pointer marches forward and never retreats. If you catch yourself wanting to move a pointer back, the pattern is probably wrong for the problem. Stop and reconsider.
The other one: off-by-one errors in the window width. end - start + 1 is the size of an inclusive window. I've written end - start under pressure more times than I'd like to admit and watched my answer come out one short. Test a two-character example by hand before you trust it.
These two patterns plus the hashing ones from last time cover a genuinely large fraction of what you'll be asked. Next in the series, we go into binary search, including the "search on the answer" trick that solves problems that don't look like search problems at all.
Key takeaways
- Two pointers exploits a sorted array to find pairs or partitions in O(n) time and O(1) space, with no hash map needed.
- Sliding window handles contiguous substring and subarray problems in a single forward pass.
- Fixed windows adjust by one-in-one-out; dynamic windows expand to satisfy a condition, then shrink while it holds.
- Both pointers only ever move forward. Backtracking a pointer is the bug that quietly turns O(n) back into O(n²).
- Triage by phrasing: "substring/subarray/contiguous/window" means sliding window; "sorted + pairs" means two pointers.
FAQ
When should I use two pointers vs a hash map?
Two pointers when the array is sorted and you want pairs, partitions, or in-place work with O(1) space. A hash map when the data is unsorted and you need to look up values you've already seen. On a sorted array, two pointers usually beats a map on memory.
How do I recognize a sliding window problem?
Look for words like longest, shortest, maximum, minimum paired with substring, subarray, contiguous, or "window of size k." Those almost always point to a sliding window.
What's the difference between a fixed and dynamic sliding window?
A fixed window has a constant size k and slides one element at a time. A dynamic window grows until a condition is satisfied and shrinks while it stays satisfied, so its size changes as it moves.
Why does the sliding window stay O(n) if there's a nested while loop?
Because each element enters the window once and leaves at most once. The inner loop's total work across the whole run is bounded by n, so it's still linear overall.
What's the most common sliding window bug?
Backtracking a pointer, which reintroduces O(n²) behavior, and off-by-one mistakes on window size. Remember an inclusive window is end - start + 1 wide.
Further reading
- Previous: Arrays & Hashing: The Interview Patterns You Must Know
- Start of series: The Senior Engineer's Guide to Coding Interviews
About the Author
Hi, I'm Aman Singh — Senior Full Stack Engineer specializing in scalable SaaS products, distributed systems, cloud architecture, and AI-powered applications.
I write about System Design, Full Stack Engineering, Distributed Systems, Redis, PostgreSQL, AWS, Node.js, and NestJS.
- Portfolio: https://amanksingh.com
- GitHub: https://github.com/amansingh1501
- Product: https://vowerole.com
- Email: aman97aman@gmail.com
Top comments (0)