Last 30 Days
No notifications
These are optimization techniques that reduce time complexity from O(n²) to O(n) for many array/string problems.
Patterns:
Fixed Window: Window size is constant
`
[1, 2, 3, 4, 5] → window size 3
[1,2,3] → [2,3,4] → [3,4,5]
`
Variable Window: Window expands/contracts based on condition
Two pointers and sliding window are the two most reliable ways to turn a brute-force O(n²) or O(n³) into a clean O(n). They aren't separate "data structures" — they're a way of *moving through* an array (or string, or linked list) so cleverly that each element gets touched a constant number of times. Once you spot the pattern, half of the medium LeetCode problems collapse into a few lines.
Two pointers — instead of nested loops, use two index variables that walk through the data in a coordinated way. The pointers can:
left ≤ right\ and define a contiguous window \arr[left..right]\. You incrementally add the element entering the right side and remove the element leaving the left side — never recomputing the whole window from scratch.Both ideas exploit that the array has order or contiguity, so once you've moved a pointer forward, you rarely need to move it back. Total work: O(n).
1. Trying to use two pointers on an *unsorted* array for problems that require order. Two-sum on unsorted data → use a hash map, not two pointers.
2. Using two pointers when the answer is non-contiguous. "Longest *increasing* subsequence" isn't sliding window — it's DP. Sliding window only works for *contiguous* segments.
3. Forgetting to shrink the window. \while (window invalid) left++;\ — without it, the right pointer races off and the window grows forever.
4. Off-by-one in window size. Window length is \right - left + 1\, not \right - left\.
5. Resetting the left pointer to 0 after each right move. That's nested-loop O(n²) again. The whole point is left never moves backward.
6. Updating the answer in the wrong place. For *longest* problems, update *after* the inner shrink loop. For *shortest*, update *inside* the shrink loop.
Pointers start at \left = 0\ and \right = n - 1\, move toward each other.
\\\`js
// Two-Sum on a SORTED array
function twoSum(arr, target) {
let l = 0, r = arr.length - 1;
while (l < r) {
const s = arr[l] + arr[r];
if (s === target) return [l, r];
if (s < target) l++; // need bigger sum → move left up
else r--; // need smaller → move right down
}
return [-1, -1];
}
\\\`
Classic problems:
s[l]\ and \s[r]\, move inward.min(h[l], h[r]) * (r - l)\. Move the *shorter* line.maxLeft\ and \maxRight\ while two-pointering.Both pointers march forward. The "write" pointer trails the "read" pointer, only advancing when the read finds something worth keeping.
\\\`js
// Remove duplicates from sorted array, in-place
function removeDuplicates(arr) {
if (arr.length === 0) return 0;
let write = 1;
for (let read = 1; read < arr.length; read++) {
if (arr[read] !== arr[read - 1]) {
arr[write++] = arr[read];
}
}
return write;
}
\\\`
Classic problems: Move Zeroes, Remove Element, partition step of quicksort, merging two sorted arrays into one with a third write pointer.
The slow pointer moves 1 step, the fast pointer moves 2. If there's a cycle, they're guaranteed to meet inside it.
\\\`js
function hasCycle(head) {
let slow = head, fast = head;
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
if (slow === fast) return true;
}
return false;
}
\\\`
Bonus uses:
Window size is a constant \k\. Slide one position at a time.
\\\`js
// Maximum sum of any subarray of size k
function maxSumK(arr, k) {
let sum = 0;
for (let i = 0; i < k; i++) sum += arr[i];
let best = sum;
for (let i = k; i < arr.length; i++) {
sum += arr[i] - arr[i - k]; // add new, remove old
best = Math.max(best, sum);
}
return best;
}
\\\`
The trick: don't recompute the sum — just add the entering element and subtract the leaving one. O(n).
Right pointer expands, left pointer shrinks when a constraint breaks. Same skeleton, two flavours.
For maximum window length (constraint must stay valid):
\\\`
left = 0
for right in 0..n-1:
add arr[right] to window
while window is INVALID:
remove arr[left] from window; left++
best = max(best, right - left + 1)
\\\`
For minimum window length (constraint must be reached):
\\\`
left = 0
for right in 0..n-1:
add arr[right] to window
while window is VALID: // note: VALID, not invalid
best = min(best, right - left + 1)
remove arr[left] from window; left++
\\\`
For "longest substring with property X over characters", maintain a frequency map of the current window.
\\\`js
// Longest substring without repeating characters
function longestUnique(s) {
const seen = new Map(); // char → most recent index
let left = 0, best = 0;
for (let r = 0; r < s.length; r++) {
if (seen.has(s[r]) && seen.get(s[r]) >= left) {
left = seen.get(s[r]) + 1; // jump past the duplicate
}
seen.set(s[r], r);
best = Math.max(best, r - left + 1);
}
return best;
}
\\\`
Other classics:
Looks like O(n²) because of the inner \while\, but isn't:
Signals for two pointers:
A pure sliding-window approach can't compute the max in O(1) when the leaving element *was* the max — you'd need to re-scan. The trick: maintain a deque of indices in decreasing order of value.
\\\`
for right in 0..n-1:
while deque non-empty and arr[deque.back()] <= arr[right]: deque.popBack()
deque.pushBack(right)
if deque.front() <= right - k: deque.popFront() // out of window
if right >= k - 1: result.push(arr[deque.front()])
\\\`
Each index enters and leaves the deque at most once → O(n).
1. Two-Sum II (sorted array) using opposite-direction two pointers. Then try Container With Most Water — same skeleton, just decide *which* pointer to move.
2. Move Zeroes in-place using same-direction two pointers (read/write). Verify O(1) extra space.
3. Find the longest substring without repeating characters with a sliding window + hash map. Trace it on \"abcabcbb"\ to confirm \left\ never moves backward.
4. Sliding-window maximum on \[1,3,-1,-3,5,3,6,7]\ with \k=3\ using a monotonic deque. Expected: \[3,3,5,5,6,7]\.