Two Pointers
Instead of checking every pair with two nested loops, walk two fingers through the data and use what you learn at each step to throw away a whole row of pairs at once.
1. What is this pattern for?
A pointer here is just an index variable like left or right (the visualizers shorten them to l and r). The Two Pointers pattern keeps two of them and moves them according to a simple rule, so the whole job takes one pass: O(n) instead of O(n²).
There are two flavours:
- Opposite ends (
lstarts left,rstarts right, they walk toward each other). Used on sorted arrays or when the answer depends on both ends (pair sums, palindromes, container/water problems). - Same direction (a slow writer and a fast reader). Used to filter or compact an array in place (remove duplicates, move zeros, partition).
2. How it works (bisected)
- Put the pointers where every answer is still possible. Opposite ends:
l = 0,r = n − 1. Same direction:write = 0(or 1),readloops over everything. - Look at what the two pointers see. Compute something cheap: the pair sum, the area, whether
nums[read]should be kept. - Decide which pointer moves, and why it's safe. On a sorted array, if
nums[l] + nums[r]is too small, no partner ofnums[l]can work (r was already the biggest), solcan be discarded forever. That's the "eliminate a whole row" argument. - Stop when they cross (
l >= r) or when the reader reaches the end. Every element was touched at most once or twice, so the total isO(n). - Level up: fix one, two-pointer the rest. For triplets (3Sum) or quadruplets, loop over the first element and run opposite-end pointers on what's to its right:
O(n²)instead ofO(n³).
3. Seeing a problem: how to spot Two Pointers
Trigger words
- "sorted array", "find a pair / triplet that sums to"
- "in place", "O(1) extra space", "remove / move / partition"
- "palindrome", "reverse", "compare from both ends"
- "container", "trap water", "two lines / walls"
- "merge two sorted arrays / lists"
Shape of the input
- A linear structure: array, string, or two of them.
- Either it's sorted, or sorting it first doesn't break the question (you return values, not original indices).
- The brute force smells like "for every i, for every j > i, check the pair". That's
O(n²).
4. When to use it, and when not to
| Use Two Pointers when… | Prefer something else when… |
|---|---|
| The array is sorted and you need a pair/triplet with a target sum | It's unsorted and you must return original indices: use a hash map (Two Sum #1), O(n) time and space |
| The answer depends on the two ends (container, rain water, palindrome) | The answer is a contiguous window that grows and shrinks: use Sliding Window (it's really two same-direction pointers with a running state) |
| Compacting/partitioning an array in place | You need to count subarrays with a sum and values can be negative: use Prefix Sum |
| Two pointers on two different sorted sequences (merge) | The structure is a linked list and you need the middle or a cycle: use Fast & Slow Pointers |
Cost: one pass, O(n) time, O(1) extra space. If you must sort first, it's O(n log n). k-Sum with "fix k−2, two-pointer the rest" is O(n^(k−1)).
5. The Python template (memorize this)
# A) Opposite ends on a SORTED array
def pair_with_sum(nums, target):
# "left" starts at the smallest number, "right" at the biggest number
left = 0
right = len(nums) - 1
# keep going while the two pointers have not met
# (if left == right they point at the SAME number, which is not a pair)
while left < right:
pair_sum = nums[left] + nums[right]
if pair_sum == target:
# found it: return the two positions
return [left, right]
if pair_sum < target:
# too small: even the biggest partner (nums[right]) is not enough
# for nums[left], so nums[left] can never be in the answer. Drop it.
left = left + 1
else:
# too big: even the smallest partner (nums[left]) is too much
# for nums[right], so nums[right] can never be in the answer. Drop it.
right = right - 1
# the pointers met and no pair was found
return []
# B) Same direction: reader / writer (compact in place)
def dedupe_sorted(nums):
# an empty list has nothing to keep
if len(nums) == 0:
return 0
# nums[0] is always kept, so the next free slot is position 1.
# Everything before position "write" is the clean, kept part.
write = 1
# "read" visits every other position, from left to right
for read in range(1, len(nums)):
last_kept = nums[write - 1]
# keep rule: the list is sorted, so a NEW value differs from the last kept one
if nums[read] != last_kept:
nums[write] = nums[read] # copy the new value into the next free slot
write = write + 1 # the kept part just grew by one
# "write" is now the number of kept values, i.e. the new length
return write
What the code is doing, line by line:
A) pair_with_sum (opposite ends)
left = 0andright = len(nums) - 1: put one finger on the smallest number (the start) and one on the biggest number (the end). Every pair is still possible.while left < right:repeat as long as the two fingers point at two different numbers. When they meet, every pair has been ruled out.pair_sum = nums[left] + nums[right]: add the two numbers we are pointing at.if pair_sum == target: return [left, right]: found the pair, hand back the two positions.if pair_sum < target: left = left + 1: the sum is too small.nums[right]is the biggest number still in play, sonums[left]cannot reach the target with any partner. Throw it away by steppingleftone place to the right.else: right = right - 1: the sum is too big.nums[left]is the smallest number still in play, sonums[right]is too big for every partner. Throw it away.return []: the loop ended without a match, so no pair exists.
B) dedupe_sorted (reader / writer)
if len(nums) == 0: return 0: an empty list keeps nothing.write = 1: the first value is always kept, so the next free slot is position 1.writeis also the number of values kept so far.for read in range(1, len(nums)):the reader visits every other position, left to right.last_kept = nums[write - 1]: the last value in the kept part.if nums[read] != last_kept:the list is sorted, so copies sit next to each other. A value that differs from the last kept one must be new.nums[write] = nums[read]thenwrite = write + 1: copy the new value into the next free slot and grow the kept part by one. Duplicates are simply skipped (the writer does not move).return write: the new length. The firstwriteslots hold the unique values.
Dry run on the example:
A) nums = [1, 2, 4, 6, 8, 11, 15], target = 14 (the same input as the first visualizer). Returns [3, 4].
| step | left | right | nums[left] | nums[right] | pair_sum | decision |
|---|---|---|---|---|---|---|
| 1 | 0 | 6 | 1 | 15 | 16 | too big → right = right - 1 |
| 2 | 0 | 5 | 1 | 11 | 12 | too small → left = left + 1 |
| 3 | 1 | 5 | 2 | 11 | 13 | too small → left = left + 1 |
| 4 | 2 | 5 | 4 | 11 | 15 | too big → right = right - 1 |
| 5 | 2 | 4 | 4 | 8 | 12 | too small → left = left + 1 |
| 6 | 3 | 4 | 6 | 8 | 14 | equal to target → return [3, 4] |
B) nums = [0, 0, 1, 1, 1, 2, 2, 3, 3, 4] (the same input as the second visualizer). Returns 5; the first 5 slots become [0, 1, 2, 3, 4].
| read | nums[read] | write | last_kept | action | write after | kept part after |
|---|---|---|---|---|---|---|
| 1 | 0 | 1 | 0 | duplicate → skip | 1 | [0] |
| 2 | 1 | 1 | 0 | new value → copy to slot 1 | 2 | [0, 1] |
| 3 | 1 | 2 | 1 | duplicate → skip | 2 | [0, 1] |
| 4 | 1 | 2 | 1 | duplicate → skip | 2 | [0, 1] |
| 5 | 2 | 2 | 1 | new value → copy to slot 2 | 3 | [0, 1, 2] |
| 6 | 2 | 3 | 2 | duplicate → skip | 3 | [0, 1, 2] |
| 7 | 3 | 3 | 2 | new value → copy to slot 3 | 4 | [0, 1, 2, 3] |
| 8 | 3 | 4 | 3 | duplicate → skip | 4 | [0, 1, 2, 3] |
| 9 | 4 | 4 | 3 | new value → copy to slot 4 | 5 | [0, 1, 2, 3, 4] |
6. How to remember it forever
How this hook works: what “Squeeze the shelf” means
What the slogan means, phrase by phrase
- “the shelf”
- A sorted array. Sorted is what makes it a shelf: cheap books on the left, expensive ones on the right.
- “Two hands”
- Two indices,
leftat the cheap end andrightat the expensive end. - “Squeeze”
- The hands only ever move inwards, towards each other. Too much → the right hand moves in. Too little → the left hand moves in.
- “Each move retires a book forever”
- The book a hand leaves can never be part of the answer, so you never look back. That's why one pass (
O(n)) replaces two nested loops (O(n²)).
Why this picture fits the pattern: Two-pointer problems ask about pairs in a sorted array. Because it is sorted, moving one hand changes the sum in a known direction, so every comparison tells you which hand to move.
The story → the code
| In the story | In the code |
|---|---|
| The sorted shelf | nums (sorted) |
| Left hand / right hand | left = 0 / right = len(nums) - 1 |
| Too much: right hand in | pair_sum > target → right = right - 1 |
| Too little: left hand in | pair_sum < target → left = left + 1 |
| The hands meet | while left < right ends: no pair |
Watch it on numbers
nums = [1, 3, 4, 6, 8, 11], target = 10.
- 1 + 11 = 12, too much. Even the cheapest partner is too much for 11, so 11 is retired. Right hand moves to 8.
- 1 + 8 = 9, too little. Even the most expensive partner left isn't enough for 1, so 1 is retired. Left hand moves to 3.
- 3 + 8 = 11, too much. Right hand moves to 6.
- 3 + 6 = 9, too little. Left hand moves to 4.
- 4 + 6 = 10. Found at indices
[2, 3]after 5 checks, not 15.
The trap it saves you from
Using it on an unsorted array (the shelf rule is gone, so moving a hand tells you nothing), or moving the wrong hand. Ask "which book can't possibly be in the answer?" and move that hand.
It still works when…
3Sum: fix one book, then squeeze the rest of the shelf. Container With Most Water: "move the shorter wall", because the shorter wall is the one that can never do better. Remove Duplicates uses two hands moving the same way: a reader and a writer.
Picture
A pair of tongs closing on an answer from both sides. For reader/writer: a scanner (reader) racing ahead and a printer (writer) only printing the lines worth keeping.
Chant
"Sorted? Squeeze. Small, left moves; big, right moves; cross, we're done."
Shorter wall rule
For container/water problems: "Move the shorter wall." The shorter side is the bottleneck, so it's the only move that can improve things.
7. LeetCode problems, solved step by step
Medium 11. Container With Most Water Google Meta Amazon Microsoft
Problem: height[i] is a vertical wall at position i. Pick two walls so that, together with the x-axis, they hold the most water. Return that area.
Solve it on LeetCode ↗ · Video walkthrough ↗
height = [1,8,6,2,5,4,8,3,7]49height = [1,1]1Constraints: n == height.length · 2 ≤ n ≤ 10⁵ · 0 ≤ height[i] ≤ 10⁴
Hint 1
The water between two lines is min(height[l], height[r]) × (r − l). Start with the widest possible pair.
Hint 2
Moving the taller line inward can never help (width shrinks, height is still capped by the shorter one). Which line should move?
From brute force to the pattern: Trying every pair of lines is O(n²). Start at both ends (maximum width) and always move the shorter wall inward: every pair you skip by doing so is provably no better, because the shorter wall already limits it. One pass: O(n).
Spot it: the answer depends on two ends (width × shorter wall) and the brute force checks all pairs. Start with the widest container and squeeze.
Key insight: area = (r − l) × min(h[l], h[r]). If h[l] is the shorter wall, every container that keeps l and moves r inward is narrower and no taller, so it can't win. Discard l.
Approach:
- Put
leftat the first wall andrightat the last: the widest possible container. - Compute the area
(right − left) × min(height[left], height[right])and keep the best inbest_area. - Move the pointer at the shorter wall inward (on a tie, either one).
- Stop when
leftmeetsrightand returnbest_area.
from typing import List
class Solution:
def maxArea(self, height: List[int]) -> int:
# start with the WIDEST container: the first wall and the last wall
left = 0
right = len(height) - 1
best_area = 0
while left < right:
# water can only rise as high as the SHORTER of the two walls
shorter_wall = min(height[left], height[right])
width = right - left
area = width * shorter_wall
# remember the biggest area we have seen so far
if area > best_area:
best_area = area
# move the pointer at the shorter wall inward:
# keeping the shorter wall can only give narrower, no-taller containers
if height[left] < height[right]:
left = left + 1
else:
right = right - 1
return best_area
What the code is doing, line by line:
left = 0,right = len(height) - 1: start with the widest container, the first wall and the last wall.best_area = 0: the biggest area found so far.while left < right:a container needs two different walls.shorter_wall = min(height[left], height[right]): water spills over the shorter wall, so the water level is the height of the shorter wall.width = right - leftandarea = width * shorter_wall: the area of a rectangle.if area > best_area: best_area = area: remember it if it is the best so far.if height[left] < height[right]: left = left + 1/else: right = right - 1: move the pointer at the shorter wall. Any other container that keeps the shorter wall is narrower and still capped by that same short wall, so it can only be smaller. On a tie the code movesright(either is fine).return best_areaonce the pointers meet.
Dry run on the example:
height = [1, 8, 6, 2, 5, 4, 8, 3, 7] (LeetCode Example 1). Returns 49.
| left | right | height[left] | height[right] | width | shorter_wall | area | best_area | next move |
|---|---|---|---|---|---|---|---|---|
| 0 | 8 | 1 | 7 | 8 | 1 | 8 | 8 | left is shorter → move left |
| 1 | 8 | 8 | 7 | 7 | 7 | 49 | 49 | right is shorter (or tie) → move right |
| 1 | 7 | 8 | 3 | 6 | 3 | 18 | 49 | right is shorter (or tie) → move right |
| 1 | 6 | 8 | 8 | 5 | 8 | 40 | 49 | right is shorter (or tie) → move right |
| 1 | 5 | 8 | 4 | 4 | 4 | 16 | 49 | right is shorter (or tie) → move right |
| 1 | 4 | 8 | 5 | 3 | 5 | 15 | 49 | right is shorter (or tie) → move right |
| 1 | 3 | 8 | 2 | 2 | 2 | 4 | 49 | right is shorter (or tie) → move right |
| 1 | 2 | 8 | 6 | 1 | 6 | 6 | 49 | right is shorter (or tie) → move right |
Edge cases to test:
- Two lines only, e.g.
[1, 1]→ 1. - Equal heights at both ends: moving either is safe.
- Strictly decreasing heights, e.g.
[5, 4, 3, 2, 1].
Complexity: O(n) time, O(1) space.
Medium 15. 3Sum Google Meta Amazon Cisco Microsoft Apple
Problem: Return every unique triplet [a, b, c] from nums with a + b + c = 0.
Solve it on LeetCode ↗ · Video walkthrough ↗
nums = [-1,0,1,2,-1,-4][[-1,-1,2],[-1,0,1]]nums = [0,1,1][]Constraints: 3 ≤ nums.length ≤ 3000 · -10⁵ ≤ nums[i] ≤ 10⁵
Hint 1
Sort first. Fix the first number nums[i]; now you need a pair summing to −nums[i] in a sorted range.
Hint 2
That pair search is Two Sum II with two pointers. To avoid duplicate triplets, skip equal values for i and after each found triplet.
From brute force to the pattern: Three nested loops over all triplets is O(n³), and removing duplicate triplets needs a set. Sorting lets you fix one number and find the other two with opposite-end pointers in O(n), so O(n²) total, and sorted order makes skipping duplicates trivial.
Spot it: "triplets that sum to a target", values (not indices) returned, so we may sort. Fix the first number, then it's Two Sum II on the rest.
Key insight: sorting gives both the squeeze rule and an easy way to skip duplicates: skip an i equal to the previous i, and after a hit skip l values equal to the one just used.
Approach:
- Sort the array.
- For each index
i(the first number,first = nums[i]): stop iffirst > 0; skip it if it equalsnums[i − 1]. - Run Two Sum II on the part to the right of
iwith target−first:left = i + 1,right = length − 1, and squeeze by the sign oftotal. - On a hit, record the triplet, move both pointers, and skip repeated values of
left.
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
# sorting lets us squeeze with two pointers AND puts equal values side by side
nums.sort()
result = []
length = len(nums)
# i is the position of the FIRST number of the triplet
for i in range(length - 2):
first = nums[i]
# the list is sorted: if the smallest of the three numbers is already
# positive, the sum can never be zero, so we can stop completely
if first > 0:
break
# the same first number as last round would only find the same triplets
if i > 0 and first == nums[i - 1]:
continue
# now find two numbers to the right of i that add up to -first
left = i + 1
right = length - 1
while left < right:
total = first + nums[left] + nums[right]
if total < 0:
# too small: we need a bigger number, so move left to the right
left = left + 1
elif total > 0:
# too big: we need a smaller number, so move right to the left
right = right - 1
else:
# the three numbers sum to zero: save the triplet
result.append([first, nums[left], nums[right]])
# step both pointers past the numbers we just used
left = left + 1
right = right - 1
# skip copies of the second number so we don't save the same triplet twice
while left < right and nums[left] == nums[left - 1]:
left = left + 1
return result
What the code is doing, line by line:
nums.sort(): sorting makes the squeeze rule work and puts equal numbers next to each other (needed to skip duplicates).result = [],length = len(nums): the list of triplets we will return, and the list size.for i in range(length - 2):ipicks the first number. We stop two places before the end so there is room for a second and third number.first = nums[i];if first > 0: break: the list is sorted, so if the smallest of the three is positive, all three are positive and can never sum to zero. Nothing later can work either, so stop the whole loop.if i > 0 and first == nums[i - 1]: continue: we already tried this same first number in the previous round; trying again would find the same triplets.left = i + 1,right = length - 1: two pointers on the part to the right ofi.total = first + nums[left] + nums[right]: the sum of the three candidates.total < 0→left = left + 1(we need a bigger number);total > 0→right = right - 1(we need a smaller number).- Otherwise the total is zero:
result.append([...])saves the triplet, then both pointers step inward. The innerwhile ... nums[left] == nums[left - 1]keeps movingleftpast copies of the number we just used, so the same triplet is not saved twice. return result.
Dry run on the example:
nums = [-1, 0, 1, 2, -1, -4] (LeetCode Example 1). After nums.sort() it is [-4, -1, -1, 0, 1, 2]. Returns [[-1, -1, 2], [-1, 0, 1]].
| i | first | left | right | nums[left] | nums[right] | what happens |
|---|---|---|---|---|---|---|
| 0 | -4 | 1 | 5 | -1 | 2 | total = -3: too small → move left |
| 0 | -4 | 2 | 5 | -1 | 2 | total = -3: too small → move left |
| 0 | -4 | 3 | 5 | 0 | 2 | total = -2: too small → move left |
| 0 | -4 | 4 | 5 | 1 | 2 | total = -1: too small → move left |
| 1 | -1 | 2 | 5 | -1 | 2 | total = 0: zero! save [-1, -1, 2], move both pointers |
| 1 | -1 | 3 | 4 | 0 | 1 | total = 0: zero! save [-1, 0, 1], move both pointers |
| 2 | -1 | — | — | — | — | same as nums[1] → continue (skip this i) |
| 3 | 0 | 4 | 5 | 1 | 2 | total = 3: too big → move right |
Edge cases to test:
- All zeros, e.g.
[0, 0, 0, 0]→ just[[0, 0, 0]](duplicate skipping). - No triplet exists, e.g.
[0, 1, 1]→[]. - Many repeated values, e.g.
[-2, 0, 0, 2, 2].
Complexity: O(n²) time, O(1) extra space (ignoring sort/output).
Medium 167. Two Sum II – Input Array Is Sorted Start here Google Meta Amazon Microsoft
Problem: A sorted array and a target; exactly one pair sums to it. Return their 1-based indices using constant extra space.
Solve it on LeetCode ↗ · Video walkthrough ↗
numbers = [2,7,11,15], target = 9[1,2]numbers = [2,3,4], target = 6[1,3]Constraints: 2 ≤ numbers.length ≤ 3 * 10⁴ · -1000 ≤ numbers[i] ≤ 1000 · numbers is sorted in non-decreasing order. · -1000 ≤ target ≤ 1000 · The tests are generated such that there is exactly one solution.
Hint 1
The array is sorted. If the current pair's sum is too small, which pointer should move?
Hint 2
Start at both ends: too small → move left right; too big → move right left.
From brute force to the pattern: Checking every pair is O(n²); a hash map is O(n) but uses O(n) space. Because the input is sorted, two pointers at the ends find the pair in O(n) time and O(1) space: every move discards a value that can't be part of the answer.
Spot it: "sorted" + "pair sums to target" + "constant space" is the textbook opposite-ends case (the first visualizer above is exactly this).
Approach:
left = 0,right = len(numbers) − 1.- If
pair_sumequals the target, return[left + 1, right + 1](1-based). - Sum too small →
left = left + 1; too big →right = right - 1. - Repeat while
left < right. A solution is guaranteed.
class Solution:
def twoSum(self, numbers: List[int], target: int) -> List[int]:
# left = smallest number, right = biggest number
left = 0
right = len(numbers) - 1
while left < right:
pair_sum = numbers[left] + numbers[right]
if pair_sum == target:
# the problem wants 1-based positions, so add 1 to each index
return [left + 1, right + 1]
if pair_sum < target:
# sum too small: the only way to grow it is a bigger left number
left = left + 1
else:
# sum too big: the only way to shrink it is a smaller right number
right = right - 1
# the problem promises an answer exists, so we never get here
return []
What the code is doing, line by line:
left = 0,right = len(numbers) - 1: smallest and biggest numbers.while left < right:keep going while they point at two different numbers.pair_sum = numbers[left] + numbers[right]: the sum of the current pair.if pair_sum == target: return [left + 1, right + 1]: LeetCode numbers positions from 1, while Python indexes from 0, so add 1 to each.if pair_sum < target: left = left + 1: too small, and only a bigger left number can make it bigger.else: right = right - 1: too big, and only a smaller right number can make it smaller.return []: never reached because the problem guarantees one answer, but every function path should return something.
Dry run on the example:
numbers = [2, 7, 11, 15], target = 9 (LeetCode Example 1). Returns [1, 2].
| left | right | numbers[left] | numbers[right] | pair_sum | decision |
|---|---|---|---|---|---|
| 0 | 3 | 2 | 15 | 17 | too big → right = right - 1 |
| 0 | 2 | 2 | 11 | 13 | too big → right = right - 1 |
| 0 | 1 | 2 | 7 | 9 | equal → return [0 + 1, 1 + 1] = [1, 2] |
In that example only right moved, so here is one more input (not from LeetCode) where both pointers move: numbers = [1, 3, 4, 5, 7, 11], target = 9. Returns [3, 4].
| left | right | numbers[left] | numbers[right] | pair_sum | decision |
|---|---|---|---|---|---|
| 0 | 5 | 1 | 11 | 12 | too big → right = right - 1 |
| 0 | 4 | 1 | 7 | 8 | too small → left = left + 1 |
| 1 | 4 | 3 | 7 | 10 | too big → right = right - 1 |
| 1 | 3 | 3 | 5 | 8 | too small → left = left + 1 |
| 2 | 3 | 4 | 5 | 9 | equal → return [2 + 1, 3 + 1] = [3, 4] |
Edge cases to test:
- The answer uses the first and last elements.
- Negative numbers, e.g.
[-1, 0], target-1→[1, 2]. - Remember the answer is 1-indexed.
Complexity: O(n) time, O(1) space.
Medium 75. Sort Colors (Dutch National Flag) Google Meta Amazon Cisco Microsoft
Problem: Sort an array of 0s, 1s and 2s in place, in one pass, without a library sort.
Solve it on LeetCode ↗ · Video walkthrough ↗
nums = [2,0,2,1,1,0][0,0,1,1,2,2]nums = [2,0,1][0,1,2]Constraints: n == nums.length · 1 ≤ n ≤ 300 · nums[i] is either 0, 1, or 2.
Hint 1
Only three values exist. Could you keep the 0s at the front and the 2s at the back while scanning once?
Hint 2
Use three pointers: low (next slot for 0), mid (scanner), high (next slot for 2). After swapping with high, don't advance mid.
From brute force to the pattern: Calling sort() is O(n log n); counting 0s, 1s and 2s and rewriting needs two passes. The Dutch-flag partition puts every element in its zone in a single pass: O(n) time, O(1) space.
Spot it: "in place" + "partition into groups". Use three pointers: low (next slot for a 0), mid (reader), high (next slot for a 2).
Key insight: after swapping a 2 to high, don't advance mid: the value that came back hasn't been inspected yet. After swapping a 0 to low, the value that came back is a 1 (already inspected), so advance both.
Approach:
- Keep regions:
[0, low)are 0s,[low, mid)are 1s,(high, end]are 2s, and[mid, high]is still unknown. nums[mid] == 0→ swap it withlow, then advancelowandmid.nums[mid] == 1→ it's already in place; advancemid.nums[mid] == 2→ swap it withhighand shrinkhigh. Do not advancemid.- Stop when
midpasseshigh.
class Solution:
def sortColors(self, nums: List[int]) -> None:
low = 0 # next slot (from the front) where a 0 should go
mid = 0 # the reader: the value we are inspecting now
high = len(nums) - 1 # next slot (from the back) where a 2 should go
# positions mid .. high are still unchecked; stop when that range is empty
while mid <= high:
value = nums[mid]
if value == 0:
# swap nums[low] and nums[mid] using a temporary variable
temp = nums[low]
nums[low] = nums[mid]
nums[mid] = temp
# the 0 is now in its region; the value that came back from "low"
# was already inspected (it is a 1), so both pointers move on
low = low + 1
mid = mid + 1
elif value == 1:
# a 1 belongs in the middle, which is exactly where it is: move on
mid = mid + 1
else:
# value is 2: swap it to the back
temp = nums[high]
nums[high] = nums[mid]
nums[mid] = temp
high = high - 1
# do NOT move mid: the value that came from the back is still unchecked
What the code is doing, line by line:
low = 0,mid = 0,high = len(nums) - 1: everything beforelowis 0, betweenlowandmidis 1, afterhighis 2, andmid .. highis still unchecked.while mid <= high:keep going while there is at least one unchecked value.value = nums[mid]: the value we are inspecting.if value == 0:swapnums[low]andnums[mid]in three steps: savenums[low]intemp, overwritenums[low], then puttempintonums[mid]. (Withouttemp, the first assignment would destroy the old value.) Thenlow = low + 1andmid = mid + 1: the value that came back fromlowwas already inspected, so we can move past it.elif value == 1: mid = mid + 1: a 1 is already in the middle region, just move on.else:(a 2) swapnums[mid]withnums[high]the same way andhigh = high - 1.middoes not move, because the value that just arrived from the back has never been inspected.- There is no
return: LeetCode wants the list changed in place, and the function returnsNone.
Dry run on the example:
nums = [2, 0, 2, 1, 1, 0] (LeetCode Example 1). Ends as [0, 0, 1, 1, 2, 2].
| low | mid | high | nums[mid] | action | nums after |
|---|---|---|---|---|---|
| 0 | 0 | 5 | 2 | 2 → swap slots 0 and 5; high − 1 (mid stays) | [0, 0, 2, 1, 1, 2] |
| 0 | 0 | 4 | 0 | 0 → swap slots 0 and 0; low + 1, mid + 1 | [0, 0, 2, 1, 1, 2] |
| 1 | 1 | 4 | 0 | 0 → swap slots 1 and 1; low + 1, mid + 1 | [0, 0, 2, 1, 1, 2] |
| 2 | 2 | 4 | 2 | 2 → swap slots 2 and 4; high − 1 (mid stays) | [0, 0, 1, 1, 2, 2] |
| 2 | 2 | 3 | 1 | 1 → mid + 1 | [0, 0, 1, 1, 2, 2] |
| 2 | 3 | 3 | 1 | 1 → mid + 1 | [0, 0, 1, 1, 2, 2] |
Edge cases to test:
- Already sorted, e.g.
[0, 1, 2]. - All one colour, e.g.
[2, 2, 2]. - Swapping a 2 from
highthat lands another 2 atmid: that's whymidstays put.
Complexity: O(n) time, O(1) space.
Hard 42. Trapping Rain Water Google Meta Nvidia Amazon Microsoft
Problem: height is an elevation map of bars of width 1. How much rain water is trapped after it rains?
Solve it on LeetCode ↗ · Video walkthrough ↗
height = [0,1,0,2,1,0,1,3,2,1,2,1]6height = [4,2,0,3,2,5]9Constraints: n == height.length · 1 ≤ n ≤ 2 * 10⁴ · 0 ≤ height[i] ≤ 10⁵
Hint 1
Water above bar i = min(highest bar to its left, highest bar to its right) − height[i].
Hint 2
With two pointers, the side whose running maximum is smaller already knows its water level. Settle that side and move it inward.
From brute force to the pattern: For every bar, scan left and right for the tallest walls: O(n²). Precomputing left-max and right-max arrays makes it O(n) time but O(n) space. Two pointers drop the arrays: the lower running maximum decides the water, so one side can always be settled: O(1) space.
Spot it: water above bar i = min(tallest on the left, tallest on the right) − height[i]. Depends on both ends, so squeeze from both sides.
Key insight: if height[l] < height[r], then the right side certainly has a wall at least as tall as height[r], so the water at l is decided by left_max alone. Settle l and move on. Mirror for the right.
Approach:
left = 0,right = n − 1, andleft_max,right_max,waterall start at 0.- Compare
height[left]andheight[right]. The lower side is the one whose water level can be settled now. - If
height[left] < height[right]: updateleft_max, addleft_max − height[left], moveleftright. - Otherwise do the mirror image on the right side.
- Stop when the pointers meet;
wateris the answer.
class Solution:
def trap(self, height: List[int]) -> int:
left = 0
right = len(height) - 1
left_max = 0 # tallest bar seen so far coming from the left
right_max = 0 # tallest bar seen so far coming from the right
water = 0 # total trapped water
while left < right:
if height[left] < height[right]:
# the right side has a wall at least as tall as height[right],
# so the water above "left" is decided by left_max alone
if height[left] > left_max:
left_max = height[left]
# water above this bar = how far it sits below the tallest wall on its left
water = water + (left_max - height[left])
left = left + 1
else:
# mirror image: the left side is at least as tall, so settle "right"
if height[right] > right_max:
right_max = height[right]
water = water + (right_max - height[right])
right = right - 1
return water
What the code is doing, line by line:
left = 0,right = len(height) - 1: one pointer at each end.left_max = 0,right_max = 0,water = 0: the tallest bar seen from each side so far, and the running total of water.while left < right:settle one bar per round until the pointers meet.if height[left] < height[right]:the left bar is the lower one. Somewhere on the right there is a wall at least as tall asheight[right], so the water aboveleftis limited only by the tallest wall on its left,left_max.if height[left] > left_max: left_max = height[left]: update the tallest-on-the-left.water = water + (left_max - height[left]): the water above this bar is how far it sits belowleft_max. If this bar is itself the new tallest, that is 0.left = left + 1: this bar is settled, move on.- The
elsebranch is the exact mirror image for the right side, usingright_maxand movingrightleft. return water.
Dry run on the example:
height = [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1] (LeetCode Example 1). Returns 6.
| left | right | height[left] | height[right] | settle | running max (after) | water added | water total |
|---|---|---|---|---|---|---|---|
| 0 | 11 | 0 | 1 | left side: bar 0 (height 0) | left_max = 0 | 0 | 0 |
| 1 | 11 | 1 | 1 | right side: bar 11 (height 1) | right_max = 1 | 0 | 0 |
| 1 | 10 | 1 | 2 | left side: bar 1 (height 1) | left_max = 1 | 0 | 0 |
| 2 | 10 | 0 | 2 | left side: bar 2 (height 0) | left_max = 1 | 1 | 1 |
| 3 | 10 | 2 | 2 | right side: bar 10 (height 2) | right_max = 2 | 0 | 1 |
| 3 | 9 | 2 | 1 | right side: bar 9 (height 1) | right_max = 2 | 1 | 2 |
| 3 | 8 | 2 | 2 | right side: bar 8 (height 2) | right_max = 2 | 0 | 2 |
| 3 | 7 | 2 | 3 | left side: bar 3 (height 2) | left_max = 2 | 0 | 2 |
| 4 | 7 | 1 | 3 | left side: bar 4 (height 1) | left_max = 2 | 1 | 3 |
| 5 | 7 | 0 | 3 | left side: bar 5 (height 0) | left_max = 2 | 2 | 5 |
| 6 | 7 | 1 | 3 | left side: bar 6 (height 1) | left_max = 2 | 1 | 6 |
Edge cases to test:
- Fewer than 3 bars → 0.
- Monotonic heights (only rising or only falling) → 0.
- A deep valley between two tall walls, e.g.
[5, 0, 0, 5]→ 10.
Complexity: O(n) time, O(1) space (versus O(n) for the prefix-max/suffix-max arrays version).
8. Common mistakes
while l <= r for pairsThat lets l == r, pairing an element with itself. For pairs, use l < r.i and after a hit for l) produces repeated answers.mid after swapping with highIn Sort Colors the value swapped in from the back hasn't been checked yet.9. Self-check quiz
In Container With Most Water, h[l] = 3, h[r] = 8. What do you do?
Unsorted array, return the original indices of two numbers summing to a target, in O(n) time.
O(n log n). A hash map of value → index gives O(n).Move all zeros to the end of an array in place, keeping the order of the non-zeros.
Time complexity of 3Sum with sort + fix-one + two pointers?
n times and each inner squeeze is O(n). Sorting's O(n log n) is dominated.10. Practice list (in order)
| # | Problem | Level | Data structure | Twist |
|---|---|---|---|---|
| 125 | Valid Palindrome | Easy | Warm-up: skip non-alphanumerics from both ends | |
| 26 | Remove Duplicates from Sorted Array | Easy | Reader/writer: copy a value only when it differs from the last one kept | |
| 88 | Merge Sorted Array | Easy | Fill from the back so nothing gets overwritten | |
| 344 | Reverse String | Easy | The purest opposite-ends swap | |
| 392 | Is Subsequence | Easy | One pointer per string; advance in s only on a match | |
| 977 | Squares of a Sorted Array | Easy | The largest square is always at one of the two ends | |
| 1768 | Merge Strings Alternately | Easy | One pointer per string; append the leftover tail | |
| 167 | Two Sum II – Input Array Is Sorted | Medium | The plain opposite-end template | |
| 80 | Remove Duplicates from Sorted Array II | Medium | Reader/writer, compare with nums[write−2] | |
| 11 | Container With Most Water | Medium | Move the shorter wall | |
| 15 | 3Sum | Medium | Fix one + squeeze + skip duplicates | |
| 16 | 3Sum Closest | Medium | Track the best |sum − target| | |
| 75 | Sort Colors | Medium | Three pointers (Dutch flag) | |
| 18 | 4Sum | Medium | Fix two + squeeze, O(n³) | |
| 881 | Boats to Save People | Medium | Greedy: heaviest pairs with lightest if it fits | |
| 5 | Longest Palindromic Substring | Medium | Two pointers moving outward from every centre | |
| 189 | Rotate Array | Medium | Three reversals, each one an opposite-ends swap | |
| 31 | Next Permutation | Medium | Find the rightmost ascent, swap with the next larger value, reverse the tail | |
| 165 | Compare Version Numbers | Medium | One pointer per version string; missing revisions count as 0 | |
| 443 | String Compression | Medium | Read pointer scans runs; write pointer writes letter + count in place | |
| 532 | K-diff Pairs in an Array | Medium | Sort, then two same-direction pointers on the difference | |
| 556 | Next Greater Element III | Medium | Next Permutation on the digits, then a 32-bit overflow check | |
| 611 | Valid Triangle Number | Medium | Fix the longest side; squeeze the other two (3Sum-style counting) | |
| 633 | Sum of Square Numbers | Medium | Two Sum II on the squares 0², 1², …, ⌊√c⌋² | |
| 777 | Swap Adjacent in LR String | Medium | Same L/R order; L only moves left, R only moves right | |
| 923 | 3Sum With Multiplicity | Medium | 3Sum counting: count index triples, careful with equal values | |
| 1861 | Rotating the Box | Medium | Per row: a write pointer from the right drops stones; then rotate | |
| 2149 | Rearrange Array Elements by Sign | Medium | Two write pointers: even slots for positives, odd slots for negatives | |
| 2332 | The Latest Time to Catch a Bus | Medium | Simulate boarding with a passenger pointer, then step back from the last seat | |
| 2337 | Move Pieces to Obtain a String | Medium | Same as LR String: pieces keep their order; L moves only left, R only right | |
| 3302 | Find the Lexicographically Smallest Valid Sequence | Medium | Suffix-match table + greedy forward pass that spends the single change as early as safely possible | |
| 3867 | Sum of GCD of Formed Pairs | Medium | Build prefix GCDs, sort, pair smallest with largest using two pointers | |
| 277 | Find the Celebrity | Medium | One pass eliminates a candidate per knows() call; a second pass verifies the survivor | |
| 969 | Pancake Sorting | Medium | Bring the largest unsorted value to the front with one flip, then to its final place with a second | |
| 1574 | Shortest Subarray to be Removed to Make Array Sorted | Medium | Keep a sorted prefix and a sorted suffix; two pointers find the best way to glue them | |
| 1679 | Max Number of K-Sum Pairs | Medium | Sort and squeeze two pointers: sum == k → pair them; smaller → move left; larger → move right | |
| 2161 | Partition Array According to Given Pivot | Medium | Stable three-way partition: less, equal, greater, each in original order | |
| 2938 | Separate Black and White Balls | Medium | Each white ball (0) must move past every black ball (1) before it: add the count of 1s seen so far | |
| 3752 | Lexicographically Smallest Negated Permutation that Sums to Target | Medium | Negating a set N gives sum S − 2·sum(N): pick N greedily from the largest values, then sort | |
| 809 | Expressive Words | Medium | Compare runs with two pointers: same letters, and each run in s is equal to the word's or ≥ 3 and longer | |
| 42 | Trapping Rain Water | Hard | Settle the lower side using its running max | |
| 4 | Median of Two Sorted Arrays | Hard | Two-pointer merge is O(m+n); the real answer needs binary search | |
| 1163 | Last Substring in Lexicographical Order | Hard | Two candidate starts i, j compared k characters in; the loser jumps past the compared part |
Worked solutions for the practice list
Problems already solved in section 7 link back up there. Every other problem from the table is solved here.
Easy 125. Valid Palindrome Start here Google Meta Amazon Cisco Microsoft Apple
Problem: After lower-casing and ignoring everything that isn't a letter or digit, does s read the same forwards and backwards?
Solve it on LeetCode ↗ · Video walkthrough ↗
s = "A man, a plan, a canal: Panama"trues = "race a car"falseConstraints: 1 ≤ s.length ≤ 2 * 10⁵ · s consists only of printable ASCII characters.
Hint 1
Compare characters from both ends moving inward.
Hint 2
Skip anything that isn't a letter or digit, and compare letters case-insensitively.
From brute force to the pattern: Building a cleaned, lower-cased copy and comparing it with its reverse is O(n) time but O(n) extra space. Two pointers skip junk characters in place and compare as they go: O(n) time, O(1) space.
Spot it: "Compare from both ends" → opposite-end pointers. No need to build a cleaned copy of the string.
Approach:
left = 0,right = len(s) − 1.- If
s[left]isn't a letter or digit, moveleftright; ifs[right]isn't, moverightleft. - Otherwise compare the lowercase characters; a mismatch means
False. - Move both inward. If they meet or cross, it's a palindrome.
class Solution:
def isPalindrome(self, s: str) -> bool:
# one pointer at each end of the string
left = 0
right = len(s) - 1
while left < right:
left_char = s[left]
right_char = s[right]
if not left_char.isalnum():
# a space or punctuation mark on the left: ignore it
left = left + 1
elif not right_char.isalnum():
# a space or punctuation mark on the right: ignore it
right = right - 1
else:
# both are letters/digits: compare them ignoring upper/lower case
if left_char.lower() != right_char.lower():
return False
# they match: move both pointers inward
left = left + 1
right = right - 1
# the pointers met without finding a mismatch
return True
What the code is doing, line by line:
left = 0,right = len(s) - 1: one pointer at each end of the string.while left < right:compare characters until the pointers meet in the middle.left_char = s[left],right_char = s[right]: the two characters we are looking at.if not left_char.isalnum(): left = left + 1:isalnum()isTruefor letters and digits. A space or punctuation mark on the left is ignored by moving past it. Only one pointer moves this round; the loop checks again next round.elif not right_char.isalnum(): right = right - 1: same on the right.else:both are letters/digits..lower()makes'A'and'a'equal. If they differ,return Falseimmediately; otherwise move both pointers inward.return True: no mismatch was found, so it is a palindrome.
Dry run on the example:
s = "race a car" (LeetCode Example 2). Returns False.
| left | right | left_char | right_char | what happens |
|---|---|---|---|---|
| 0 | 9 | 'r' | 'r' | match → move both inward |
| 1 | 8 | 'a' | 'a' | match → move both inward |
| 2 | 7 | 'c' | 'c' | match → move both inward |
| 3 | 6 | 'e' | ' ' | right is a space → skip it |
| 3 | 5 | 'e' | 'a' | mismatch → return False |
Edge cases to test:
- Only punctuation, e.g.
" , . "→ True (nothing left to mismatch). - Digits next to letters, e.g.
"0P"→ False. - Mixed case, e.g.
"Aa"→ True.
Complexity: O(n) time, O(1) space.
Medium 80. Remove Duplicates from Sorted Array II Google Meta Amazon
Problem: A sorted array; remove extra copies in place so every value appears at most twice. Return the new length k (the first k slots hold the result).
Solve it on LeetCode ↗ · Video walkthrough ↗
nums = [1,1,1,2,2,3]5, nums = [1,1,2,2,3,_]nums = [0,0,1,1,1,1,2,3,3]7, nums = [0,0,1,1,2,3,3,_,_]Constraints: 1 ≤ nums.length ≤ 3 * 10⁴ · -10⁴ ≤ nums[i] ≤ 10⁴ · nums is sorted in non-decreasing order.
Hint 1
A reader scans every element; a writer marks where the next kept element goes.
Hint 2
Keep nums[read] only if it differs from nums[write − 2]: then no value appears more than twice.
From brute force to the pattern: Deleting extra copies from a Python list shifts everything after them: O(n²). A read/write pointer pair copies keepers forward in one pass. Comparing with the element two slots back is all the bookkeeping needed: O(n), O(1) space.
Spot it: "In place" + keep-or-drop each element → reader/writer pointers. Sorted, so copies sit next to each other.
Key insight: Compare with the value two slots back in the kept part, nums[write − 2]. If they're equal, keeping value would make three copies.
Approach:
write = 0marks the next free slot.- For each position
read(the reader), withvalue = nums[read]: keep it ifwrite < 2orvalue != nums[write − 2]. - To keep it:
nums[write] = value, thenwrite = write + 1. - Return
write.
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
# next free slot; positions 0 .. write-1 hold the kept part
write = 0
# "read" visits every position from left to right
for read in range(len(nums)):
value = nums[read]
if write < 2:
# the first two values can always be kept
keep = True
elif value != nums[write - 2]:
# different from the value two slots back in the kept part,
# so keeping it makes at most 2 copies
keep = True
else:
# equal to the value two slots back: it would be a 3rd copy
keep = False
if keep:
nums[write] = value # copy it into the next free slot
write = write + 1 # the kept part grew by one
# "write" is how many values were kept
return write
What the code is doing, line by line:
write = 0: the next free slot. Positions0 .. write - 1are the kept part.for read in range(len(nums)):andvalue = nums[read]: the reader looks at every value once.if write < 2: keep = True: with fewer than two values kept, any value may still appear twice.elif value != nums[write - 2]: keep = True: look two slots back in the kept part. The kept part is sorted, so if that value equalsvalue, the last two kept values are bothvalueand this would be a 3rd copy. If it differs, keeping it is fine.else: keep = False: a 3rd copy, drop it (the writer does not move).if keep: nums[write] = valuethenwrite = write + 1: copy into the next free slot. This is safe becausewriteis never ahead ofread, so we never overwrite a value the reader still needs.return write: the number of values kept.
Dry run on the example:
nums = [1, 1, 1, 2, 2, 3] (LeetCode Example 1). Returns 5; the first 5 slots are [1, 1, 2, 2, 3].
| read | value | write | nums[write − 2] | keep? | write after | kept part after |
|---|---|---|---|---|---|---|
| 0 | 1 | 0 | — (write < 2) | keep | 1 | [1] |
| 1 | 1 | 1 | — (write < 2) | keep | 2 | [1, 1] |
| 2 | 1 | 2 | 1 | drop (would be a 3rd copy) | 2 | [1, 1] |
| 3 | 2 | 2 | 1 | keep | 3 | [1, 1, 2] |
| 4 | 2 | 3 | 1 | keep | 4 | [1, 1, 2, 2] |
| 5 | 3 | 4 | 2 | keep | 5 | [1, 1, 2, 2, 3] |
Edge cases to test:
- Length ≤ 2 → return the length unchanged.
- All the same value, e.g.
[1, 1, 1, 1]→ 2. - No duplicates at all.
Complexity: O(n) time, O(1) space. Change the 2 to any k for "at most k copies".
Medium 16. 3Sum Closest Google Meta Amazon Microsoft
Problem: Choose three numbers whose sum is as close as possible to target; return that sum.
nums = [-1,2,1,-4], target = 12nums = [0,0,0], target = 10Constraints: 3 ≤ nums.length ≤ 500 · -1000 ≤ nums[i] ≤ 1000 · -10⁴ ≤ target ≤ 10⁴
Hint 1
Same shape as 3Sum: sort, fix nums[i], squeeze the other two from both ends.
Hint 2
Track the triplet whose sum is closest to target; move the pointer that brings the sum toward the target.
From brute force to the pattern: All triplets in O(n³). Sorting plus a two-pointer squeeze per fixed element covers the useful triplets in O(n²): when the sum is below target only moving left can help, and above it only moving right can.
Spot it: Same skeleton as 3Sum, but instead of looking for == 0 we track the smallest |sum − target|.
Approach:
- Sort the array; start
best_sumat the sum of the first three numbers. - Fix
i; setleft = i + 1,right = length − 1. - Compute
current_sum; if it's closer to the target thanbest_sum, updatebest_sum. current_sum < target→ moveleftright;current_sum > target→ moverightleft; equal → return it straight away.
class Solution:
def threeSumClosest(self, nums: List[int], target: int) -> int:
# sort so we can squeeze with two pointers
nums.sort()
length = len(nums)
# start with any real sum of three numbers: the first three
best_sum = nums[0] + nums[1] + nums[2]
# i is the position of the first number of the three
for i in range(length - 2):
left = i + 1
right = length - 1
while left < right:
current_sum = nums[i] + nums[left] + nums[right]
# how far is this sum from the target, compared with the best so far?
current_distance = abs(current_sum - target)
best_distance = abs(best_sum - target)
if current_distance < best_distance:
best_sum = current_sum
if current_sum < target:
# too small: try a bigger second number
left = left + 1
elif current_sum > target:
# too big: try a smaller third number
right = right - 1
else:
# exactly the target: nothing can be closer
return current_sum
return best_sum
What the code is doing, line by line:
nums.sort(),length = len(nums): sort so the squeeze works.best_sum = nums[0] + nums[1] + nums[2]: we need a starting answer that is a real sum of three numbers, so take the first three.for i in range(length - 2):fix the first number;left = i + 1,right = length - 1squeeze the other two.current_sum = nums[i] + nums[left] + nums[right]: the sum of the three candidates.current_distance = abs(current_sum - target)andbest_distance = abs(best_sum - target):absdrops the minus sign, so these are "how far from the target". If the current sum is closer, it becomesbest_sum.if current_sum < target: left = left + 1(try a bigger sum);elif current_sum > target: right = right - 1(try a smaller sum);else: return current_sum(distance 0 can't be beaten).return best_sumafter everyihas been tried.
Dry run on the example:
nums = [-1, 2, 1, -4], target = 1 (LeetCode Example 1). Sorted: [-4, -1, 1, 2]; best_sum starts at -4 + -1 + 1 = -4. Returns 2.
| i | left | right | numbers | current_sum | distance to target | best_sum (after) | move |
|---|---|---|---|---|---|---|---|
| 0 | 1 | 3 | -4 + -1 + 2 | -3 | 4 | -3 | < target → move left |
| 0 | 2 | 3 | -4 + 1 + 2 | -1 | 2 | -1 | < target → move left |
| 1 | 2 | 3 | -1 + 1 + 2 | 2 | 1 | 2 | > target → move right |
Edge cases to test:
- An exact match → return immediately.
- Exactly three numbers → their sum.
- Large negative and positive values mixed.
Complexity: O(n²) time, O(1) extra space.
Medium 18. 4Sum Google Meta Amazon Microsoft
Problem: Return every unique quadruplet [a, b, c, d] with a + b + c + d == target.
nums = [1,0,-1,0,-2,2], target = 0[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]nums = [2,2,2,2,2], target = 8[[2,2,2,2]]Constraints: 1 ≤ nums.length ≤ 200 · -10⁹ ≤ nums[i] ≤ 10⁹ · -10⁹ ≤ target ≤ 10⁹
Hint 1
Generalise 3Sum: fix two numbers with two loops, then squeeze the remaining pair.
Hint 2
Skip duplicates at both fixed levels and after each found quadruplet.
From brute force to the pattern: Four nested loops: O(n⁴). Fixing two elements and squeezing the other two after sorting gives O(n³), with sorted order making duplicate handling easy.
Spot it: k-Sum: fix the first two numbers with loops, then squeeze the last two with opposite-end pointers.
Approach:
- Sort the array.
- Loop
i, skipping repeated values; loopj > i, skipping repeated values. - Squeeze
left,righton the rest, looking fortotal == target. - On a hit, record it, move both pointers, and skip repeated
leftvalues.
class Solution:
def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
# sort so we can squeeze with two pointers and skip duplicates easily
nums.sort()
length = len(nums)
result = []
# i = position of the first number
for i in range(length - 3):
# same first number as last time would only repeat quadruplets
if i > 0 and nums[i] == nums[i - 1]:
continue
# j = position of the second number (always to the right of i)
for j in range(i + 1, length - 2):
# same second number as last time would only repeat quadruplets
if j > i + 1 and nums[j] == nums[j - 1]:
continue
# squeeze the last two numbers from both ends of what is left
left = j + 1
right = length - 1
while left < right:
total = nums[i] + nums[j] + nums[left] + nums[right]
if total < target:
left = left + 1 # too small: bigger third number
elif total > target:
right = right - 1 # too big: smaller fourth number
else:
result.append([nums[i], nums[j], nums[left], nums[right]])
left = left + 1
right = right - 1
# skip copies of the third number so we don't repeat this answer
while left < right and nums[left] == nums[left - 1]:
left = left + 1
return result
What the code is doing, line by line:
nums.sort(),length = len(nums),result = []: sort, remember the size, and prepare the answer list.for i in range(length - 3):picks the first number, leaving room for three more.if i > 0 and nums[i] == nums[i - 1]: continueskips a first number we have already tried.for j in range(i + 1, length - 2):picks the second number, to the right ofi.if j > i + 1 and nums[j] == nums[j - 1]: continueskips a repeated second number (thej > i + 1part makes sure we only compare with an earlierj, never withnums[i]).left = j + 1,right = length - 1: squeeze the last two numbers from both ends of what is left.total = nums[i] + nums[j] + nums[left] + nums[right]; too small →left = left + 1; too big →right = right - 1.- On a hit,
result.append([...]), move both pointers, and the innerwhilemovesleftpast copies of the third number. return result.
Dry run on the example:
nums = [1, 0, -1, 0, -2, 2], target = 0 (LeetCode Example 1). Sorted: [-2, -1, 0, 0, 1, 2]. Returns [[-2, -1, 1, 2], [-2, 0, 0, 2], [-1, 0, 0, 1]].
| i | j | left | right | the four numbers | total | what happens |
|---|---|---|---|---|---|---|
| 0 | 1 | 2 | 5 | -2, -1, 0, 2 | -1 | too small → move left |
| 0 | 1 | 3 | 5 | -2, -1, 0, 2 | -1 | too small → move left |
| 0 | 1 | 4 | 5 | -2, -1, 1, 2 | 0 | hit! save [-2, -1, 1, 2], move both |
| 0 | 2 | 3 | 5 | -2, 0, 0, 2 | 0 | hit! save [-2, 0, 0, 2], move both |
| 0 | 3 | — | — | — | — | same second number as before → skip this j |
| 1 | 2 | 3 | 5 | -1, 0, 0, 2 | 1 | too big → move right |
| 1 | 2 | 3 | 4 | -1, 0, 0, 1 | 0 | hit! save [-1, 0, 0, 1], move both |
| 1 | 3 | — | — | — | — | same second number as before → skip this j |
| 2 | 3 | 4 | 5 | 0, 0, 1, 2 | 3 | too big → move right |
Edge cases to test:
- Sums that overflow 32-bit in other languages (Python is fine).
- Many duplicates, e.g.
[2, 2, 2, 2, 2], target 8 → one quadruplet. - Fewer than four numbers →
[].
Complexity: O(n³) time, O(1) extra space.
Medium 881. Boats to Save People Google Amazon
Problem: Each boat holds at most two people with total weight ≤ limit. What's the minimum number of boats to carry everyone?
people = [1,2], limit = 31people = [3,2,2,1], limit = 33Constraints: 1 ≤ people.length ≤ 5 * 10⁴ · 1 ≤ people[i] ≤ limit ≤ 3 * 10⁴
Hint 1
Sort the weights. Who is the hardest person to pair?
Hint 2
Always put the heaviest remaining person in a boat; add the lightest too if they fit together.
From brute force to the pattern: Trying all pairings is exponential. Greedy with two pointers on the sorted weights is optimal: the heaviest person must go now, and pairing them with the lightest person is the best chance of sharing. O(n log n) for the sort.
Spot it: Pair the lightest with the heaviest after sorting: a greedy opposite-end squeeze.
Key insight: The heaviest person must ride in some boat. Giving them the lightest partner (if one fits) is never worse than any other choice.
Approach:
- Sort
people. lightest= index of the lightest person,heaviest= index of the heaviest.- If
people[lightest] + people[heaviest] ≤ limit, the lightest boards too (lightest = lightest + 1). - The heaviest always boards (
heaviest = heaviest - 1); count one boat. - Repeat while
lightest ≤ heaviest(when they are equal, one person still needs a boat).
class Solution:
def numRescueBoats(self, people: List[int], limit: int) -> int:
# sort by weight so the lightest is at the front and the heaviest at the back
people.sort()
lightest = 0 # index of the lightest person still waiting
heaviest = len(people) - 1 # index of the heaviest person still waiting
boats = 0
# when lightest == heaviest, one person is left and still needs a boat
while lightest <= heaviest:
if people[lightest] + people[heaviest] <= limit:
# the lightest person fits in the same boat as the heaviest
lightest = lightest + 1
# the heaviest person always leaves on this boat
heaviest = heaviest - 1
boats = boats + 1
return boats
What the code is doing, line by line:
people.sort(): lightest at the front, heaviest at the back.lightest = 0,heaviest = len(people) - 1,boats = 0: indexes of the lightest and heaviest person still waiting, and the boat count.while lightest <= heaviest:someone is still waiting. We use<=because when the two indexes are equal there is exactly one person left, and they still need a boat.if people[lightest] + people[heaviest] <= limit: lightest = lightest + 1: if the lightest person fits alongside the heaviest, they ride together.heaviest = heaviest - 1andboats = boats + 1: the heaviest person always leaves on this boat (alone or with the lightest).return boats.
Dry run on the example:
people = [3, 2, 2, 1], limit = 3 (LeetCode Example 2). Sorted: [1, 2, 2, 3]. Returns 3.
| lightest | heaviest | who gets on this boat | boats so far |
|---|---|---|---|
| 0 | 3 | 1 + 3 = 4 > 3 → heaviest goes alone | 1 |
| 0 | 2 | 1 + 2 = 3 ≤ 3 → both board | 2 |
| 1 | 1 | only one person left (weight 2) → goes alone | 3 |
Edge cases to test:
- Everyone exactly at the limit → one boat each.
- Everyone pairs perfectly →
n / 2boats. - A single person → 1.
Complexity: O(n log n) time for the sort, O(1) extra space.
Hard 4. Median of Two Sorted Arrays Google Meta Amazon Microsoft Apple
Problem: Given two sorted arrays, return the median of all their numbers combined. The required time is O(log(m + n)).
nums1 = [1,3], nums2 = [2]2.00000nums1 = [1,2], nums2 = [3,4]2.50000Constraints: nums1.length == m · nums2.length == n · 0 ≤ m ≤ 1000 · 0 ≤ n ≤ 1000 · 1 ≤ m + n ≤ 2000 · -10⁶ ≤ nums1[i], nums2[i] ≤ 10⁶
Hint 1
Merging both arrays and taking the middle is O(m + n). For O(log(m + n)), binary-search a cut in the smaller array.
Hint 2
A correct cut puts the same number of elements on the left as on the right, with every left element ≤ every right element.
From brute force to the pattern: Concatenating and sorting is O((m + n) log(m + n)); a two-pointer merge to the middle is O(m + n). The intended solution binary-searches where to cut the shorter array so both halves balance: O(log(min(m, n))).
Spot it: Two sorted arrays → a two-pointer merge reaches the middle in O(m + n) (shown as findMedianMerge). To get logarithmic time, binary-search the cut point instead (see Modified Binary Search).
Key insight: Cut both arrays so that the left parts together hold half the numbers and every left number ≤ every right number. Then the median sits right at the cut.
Approach:
- Make
shorterthe shorter list;half = (total_length + 1) // 2. - Binary-search
cut_short= how many items ofshortergo on the left;cut_long = half − cut_shortcome fromlonger. - The cut is valid when
short_left ≤ long_rightandlong_left ≤ short_right(use ±∞ beyond the ends). - Odd total: median = the larger left value. Even: average of the larger left value and the smaller right value.
- If
short_left > long_right, take fewer fromshorter(high = cut_short − 1); otherwise take more (low = cut_short + 1).
class Solution:
def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
# binary-search the SHORTER list, so the search is as small as possible
if len(nums1) <= len(nums2):
shorter = nums1
longer = nums2
else:
shorter = nums2
longer = nums1
short_length = len(shorter)
long_length = len(longer)
total_length = short_length + long_length
# how many numbers go into the combined LEFT half (the extra one goes left if odd)
half = (total_length + 1) // 2
# we search for cut_short = how many numbers of "shorter" go into the left half
low = 0
high = short_length
while low <= high:
cut_short = (low + high) // 2
# the rest of the left half comes from "longer"
cut_long = half - cut_short
# the four numbers touching the two cuts (use -inf / +inf past the ends)
if cut_short > 0:
short_left = shorter[cut_short - 1]
else:
short_left = float("-inf")
if cut_short < short_length:
short_right = shorter[cut_short]
else:
short_right = float("inf")
if cut_long > 0:
long_left = longer[cut_long - 1]
else:
long_left = float("-inf")
if cut_long < long_length:
long_right = longer[cut_long]
else:
long_right = float("inf")
if short_left <= long_right and long_left <= short_right:
# valid cut: every number on the left is <= every number on the right
biggest_left = max(short_left, long_left)
if total_length % 2 == 1:
# odd count: the median is the biggest number of the left half
return float(biggest_left)
# even count: average the two numbers around the cut
smallest_right = min(short_right, long_right)
return (biggest_left + smallest_right) / 2
if short_left > long_right:
# took too many from "shorter": move the cut left
high = cut_short - 1
else:
# took too few from "shorter": move the cut right
low = cut_short + 1
# only reached if the inputs were not sorted
return 0.0
def findMedianMerge(self, nums1: List[int], nums2: List[int]) -> float:
# The two-pointer version: merge until the middle, O(m + n).
total_length = len(nums1) + len(nums2)
i = 0 # pointer into nums1
j = 0 # pointer into nums2
previous = 0 # the value taken one step before "current"
current = 0 # the latest value taken, in sorted order
middle = total_length // 2
steps = 0
# take values in sorted order until we have taken position "middle"
while steps <= middle:
previous = current
# decide which list holds the next smallest value
if j >= len(nums2):
take_from_first = True # nums2 is used up
elif i >= len(nums1):
take_from_first = False # nums1 is used up
elif nums1[i] <= nums2[j]:
take_from_first = True
else:
take_from_first = False
if take_from_first:
current = nums1[i]
i = i + 1
else:
current = nums2[j]
j = j + 1
steps = steps + 1
if total_length % 2 == 1:
# odd count: the middle value itself
return float(current)
# even count: average of the two middle values
return (previous + current) / 2
What the code is doing, line by line:
findMedianSortedArrays (binary search on the cut)
- The first
if / elsenames the shorter listshorterand the otherlonger, so the binary search runs over the smaller range. short_length,long_length,total_length: the sizes.half = (total_length + 1) // 2is how many numbers belong in the combined left half (//is whole-number division; with an odd count the extra number goes left).low = 0,high = short_length:cut_short(how many numbers ofshortergo left) can be anything from 0 to all of them.while low <= high:try the middle choice:cut_short = (low + high) // 2, and the rest of the left half comes fromlonger:cut_long = half - cut_short.- The four
if / elseblocks read the numbers touching each cut:short_left/short_rightandlong_left/long_right. If a cut is at the very start or end there is no number there, so we use-inf/inf, which can never break the rule below. if short_left <= long_right and long_left <= short_right:each list is already sorted on its own, so these two "cross" checks are all that's needed to know every left number ≤ every right number. Then the median is at the cut:biggest_leftfor an odd count, or the average ofbiggest_leftandsmallest_rightfor an even count.if short_left > long_right: high = cut_short - 1: we took too many fromshorter; try a smaller cut.else: low = cut_short + 1: too few; try a bigger cut.return 0.0: only reachable if the inputs were not sorted.
findMedianMerge (the two-pointer merge)
iandjpoint intonums1andnums2.currentis the latest value taken in sorted order andpreviousthe one before it.middle = total_length // 2.while steps <= middle:takemiddle + 1values in sorted order; the last one taken is the middle value.previous = current: remember the value from the step before (needed for an even count).- The
if / elif / elsechain decides where the next smallest value is: ifnums2is used up take fromnums1; ifnums1is used up take fromnums2; otherwise take the smaller front value. - Take it into
currentand step that list's pointer forward;steps = steps + 1. - Odd count: the median is
current. Even count: the average ofpreviousandcurrent.
Dry run on the example:
nums1 = [1, 2], nums2 = [3, 4] (LeetCode Example 2). Both have length 2, so shorter = [1, 2], longer = [3, 4], half = 2. findMedianSortedArrays returns 2.5 = (2 + 3) / 2.
| low | high | cut_short | cut_long | short_left | short_right | long_left | long_right | verdict |
|---|---|---|---|---|---|---|---|---|
| 0 | 2 | 1 | 1 | 1 | 2 | 3 | 4 | long_left > short_right → too few from shorter: low = cut_short + 1 |
| 2 | 2 | 2 | 0 | 2 | ∞ | −∞ | 3 | valid cut → even total: average of the biggest left and smallest right |
And the merge version on nums1 = [1, 3], nums2 = [2] (LeetCode Example 1): total_length = 3, middle = 1, so it keeps taking values while steps <= 1. findMedianMerge returns 2.0 (odd count, so just current).
| steps | i | j | nums1[i] | nums2[j] | take from | previous after | current after |
|---|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 1 | 2 | nums1 | 0 | 1 |
| 1 | 1 | 0 | 3 | 2 | nums2 | 1 | 2 |
Edge cases to test:
- One array empty.
- Total length even → the average of the two middle values.
- All of one array smaller than all of the other.
Complexity: Binary search: O(log min(m, n)) time; merge: O(m + n). Both O(1) space.
Easy 26. Remove Duplicates from Sorted Array Google Meta Amazon Cisco Microsoft
Problem: nums is sorted. Remove the duplicates in place so each value appears once, keep the order, and return k, the number of unique values. The first k slots must hold them; the rest don't matter.
Solve it on LeetCode ↗ · Video walkthrough ↗
nums = [1,1,2]2, nums = [1,2,_]nums = [0,0,1,1,1,2,2,3,3,4]5, nums = [0,1,2,3,4,_,_,_,_,_]Constraints: 1 ≤ nums.length ≤ 3 * 10⁴ · -100 ≤ nums[i] ≤ 100 · nums is sorted in non-decreasing order.
Hint 1
The array is sorted, so duplicates sit next to each other.
Hint 2
Keep a writer index; copy a value forward only when it differs from the last value you kept.
From brute force to the pattern: Removing each duplicate from the list shifts the rest of it: O(n²). A reader/writer pointer pair rewrites the unique values to the front in one pass: O(n), O(1) space.
Spot it: "In place" + "sorted" + "remove" → two pointers moving the same direction: a fast read pointer scans everything and a slow write pointer marks where the next keeper goes.
Approach:
nums[0]is always kept, so startwrite = 1.- Move
readfrom index 1 to the end. - Because the array is sorted, a new value is simply one that differs from the last value kept,
nums[write − 1]. - When it differs, copy it to
nums[write]and advancewrite. Duplicates are just skipped. writeends up equal to the number of unique values. Return it.
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
# nums[0] is always unique, so the next unique value goes to index 1
write = 1
for read in range(1, len(nums)):
# sorted input: a new value is one that differs from the last value we kept
if nums[read] != nums[write - 1]:
nums[write] = nums[read]
write = write + 1
# write counts how many unique values sit at the front
return write
What the code is doing, line by line:
write = 1: the first element can never be a duplicate, so it stays where it is.for read in range(1, len(nums)):the reader looks at every other element exactly once.if nums[read] != nums[write - 1]:nums[write - 1]is the last value we kept. Equal means duplicate (skip it); different means a brand-new value.nums[write] = nums[read]copies the new value into the next free slot, andwrite = write + 1moves the slot forward.return write: everything beforewriteis the unique, ordered prefix.
Dry run on the example:
nums = [0, 0, 1, 1, 1, 2, 2, 3, 3, 4] (LeetCode Example 2). Returns 5, with nums starting [0, 1, 2, 3, 4].
| read | nums[read] | last kept nums[write−1] | action | write after |
|---|---|---|---|---|
| 1 | 0 | 0 | duplicate → skip | 1 |
| 2 | 1 | 0 | new → nums[1] = 1 | 2 |
| 3 | 1 | 1 | duplicate → skip | 2 |
| 4 | 1 | 1 | duplicate → skip | 2 |
| 5 | 2 | 1 | new → nums[2] = 2 | 3 |
| 6 | 2 | 2 | duplicate → skip | 3 |
| 7 | 3 | 2 | new → nums[3] = 3 | 4 |
| 8 | 3 | 3 | duplicate → skip | 4 |
| 9 | 4 | 3 | new → nums[4] = 4 | 5 |
Edge cases to test:
- A single element → 1.
- All the same, e.g.
[1, 1, 1]→ 1. - No duplicates →
n.
Complexity: O(n) time, O(1) extra space.
Easy 88. Merge Sorted Array Google Meta Amazon Cisco Microsoft
Problem: nums1 has length m + n: its first m values are sorted and the last n slots are zeros. nums2 holds n sorted values. Merge nums2 into nums1 in place so nums1 ends up sorted.
Solve it on LeetCode ↗ · Video walkthrough ↗
nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3[1,2,2,3,5,6]nums1 = [1], m = 1, nums2 = [], n = 0[1]Constraints: nums1.length == m + n · nums2.length == n · 0 ≤ m, n ≤ 200 · 1 ≤ m + n ≤ 200 · -10⁹ ≤ nums1[i], nums2[j] ≤ 10⁹
Hint 1
Filling nums1 from the front would overwrite values you still need. Where is the free space?
Hint 2
Fill from the back: compare the largest remaining values of both arrays and place the larger at the end.
From brute force to the pattern: Copying nums2 in and calling sort() is O((m + n) log(m + n)); merging into a new list costs O(m + n) extra space. Three pointers filling from the back merge in place in O(m + n) with O(1) space.
Spot it: Two sorted inputs → one pointer per array. Filling from the front would overwrite values of nums1 we still need, but the empty space is at the end, so fill backwards: always place the larger of the two tails.
Approach:
ipoints at the last real value ofnums1(m − 1),jat the last value ofnums2(n − 1), andwriteat the very last slot (m + n − 1).- While
nums2still has values (j ≥ 0): comparenums1[i]andnums2[j], copy the larger intonums1[write], and step that pointer left. - Move
writeleft after every placement. - If
nums2runs out first, the rest ofnums1is already in place, so we can stop.
class Solution:
def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None:
i = m - 1 # last real value of nums1
j = n - 1 # last value of nums2
write = m + n - 1 # last slot of nums1 (the empty space is at the end)
# once nums2 is used up, whatever is left in nums1 is already in place
while j >= 0:
if i >= 0 and nums1[i] > nums2[j]:
# nums1's tail is bigger: it belongs at the back
nums1[write] = nums1[i]
i = i - 1
else:
# nums2's tail is bigger (or nums1 is used up)
nums1[write] = nums2[j]
j = j - 1
write = write - 1
What the code is doing, line by line:
i,j,write: three pointers that all start at the right end and move left.while j >= 0:we only need to keep going whilenums2has values. Leftovernums1values are already sitting in sorted position.if i >= 0 and nums1[i] > nums2[j]:checki >= 0first so we never readnums1[-1]. Ifnums1's tail is larger, it goes towrite.else:otherwisenums2[j]is larger or equal (ornums1is exhausted), so it goes towrite.write = write - 1: the slot is filled; the next largest value goes one to the left.- The function returns nothing: LeetCode checks
nums1itself.
Dry run on the example:
nums1 = [1, 2, 3, 0, 0, 0], m = 3, nums2 = [2, 5, 6], n = 3 (LeetCode Example 1). Result [1, 2, 2, 3, 5, 6].
| write | nums1[i] | nums2[j] | placed | nums1 after |
|---|---|---|---|---|
| 5 | 3 | 6 | 6 (from nums2) | [1, 2, 3, 0, 0, 6] |
| 4 | 3 | 5 | 5 (from nums2) | [1, 2, 3, 0, 5, 6] |
| 3 | 3 | 2 | 3 (from nums1) | [1, 2, 3, 3, 5, 6] |
| 2 | 2 | 2 | 2 (from nums2, tie) | [1, 2, 2, 3, 5, 6] |
j is now −1, so the loop stops. The [1, 2] at the front never had to move.
Edge cases to test:
n = 0: nothing to merge.m = 0: copynums2entirely.- Every
nums2value smaller than everynums1value.
Complexity: O(m + n) time, O(1) extra space.
Easy 344. Reverse String Start here Google Meta Amazon Microsoft
Problem: Reverse the list of characters s in place, using O(1) extra memory.
Solve it on LeetCode ↗ · Video walkthrough ↗
s = ["h","e","l","l","o"]["o","l","l","e","h"]s = ["H","a","n","n","a","h"]["h","a","n","n","a","H"]Constraints: 1 ≤ s.length ≤ 10⁵ · s[i] is a printable ascii character.
Hint 1
Swap the first and last characters. Then what?
Hint 2
Move both pointers inward and repeat until they meet.
From brute force to the pattern: s[::-1] builds a new list (O(n) extra space), which the problem forbids. Swapping from both ends in place does it with O(1) space.
Spot it: "Reverse" + "in place" → one pointer at each end; swap and walk inward until they meet.
Approach:
left = 0,right = len(s) − 1.- While
left < right: swaps[left]ands[right]. - Move
leftright andrightleft. When they meet (odd length) or cross (even length), every pair has been swapped.
class Solution:
def reverseString(self, s: List[str]) -> None:
left = 0
right = len(s) - 1
while left < right:
# swap the two ends using a temporary variable
temp = s[left]
s[left] = s[right]
s[right] = temp
# both pointers step toward the middle
left = left + 1
right = right - 1
What the code is doing, line by line:
left = 0,right = len(s) - 1: the first and last positions.while left < right:stop when the pointers meet: the middle character of an odd-length list stays put.temp = s[left]…s[right] = temp: a three-line swap. (Python'ss[left], s[right] = s[right], s[left]does the same in one line.)left = left + 1,right = right - 1: move inward to the next pair.- Nothing is returned: the list is changed in place, which is what LeetCode checks.
Dry run on the example:
s = ["h", "e", "l", "l", "o"] (LeetCode Example 1). Result ["o", "l", "l", "e", "h"].
| left | right | swap | s after |
|---|---|---|---|
| 0 | 4 | h ↔ o | o e l l h |
| 1 | 3 | e ↔ l | o l l e h |
| 2 | 2 | pointers meet → stop | o l l e h |
Edge cases to test:
- One character → unchanged.
- Even length: the pointers cross without ever meeting.
- A palindrome → looks unchanged.
Complexity: O(n) time, O(1) extra space.
Easy 392. Is Subsequence Google Meta Amazon Microsoft
Problem: Return True if s is a subsequence of t: you can delete some (or no) characters of t, without reordering the rest, to get s.
Solve it on LeetCode ↗ · Video walkthrough ↗
s = "abc", t = "ahbgdc"trues = "axc", t = "ahbgdc"falseConstraints: 0 ≤ s.length ≤ 100 · 0 ≤ t.length ≤ 10⁴ · s and t consist only of lowercase English letters.
Hint 1
Walk through t once. When do you advance in s?
Hint 2
Advance the pointer in s only on a match; at the end, did it reach the end of s?
From brute force to the pattern: Generating subsequences of t is exponential. Two pointers scan both strings once, greedily matching each character of s at its earliest possible position in t: O(len(t)).
Spot it: Two sequences scanned left to right at different speeds → two pointers moving the same way. t is always consumed; s only moves forward when its current character is found.
Approach:
ipoints at the next character ofswe still need to find.- Walk through every character of
t. - If it equals
s[i], we found that character in order: moveiforward. - At the end,
sis a subsequence exactly when every character was found, i.e.i == len(s).
class Solution:
def isSubsequence(self, s: str, t: str) -> bool:
# i = index of the next character of s we still need to find
i = 0
for ch in t:
# only compare while s still has characters left to match
if i < len(s) and s[i] == ch:
i = i + 1
# every character of s was found, in order
return i == len(s)
What the code is doing, line by line:
i = 0: nothing ofshas been matched yet.for ch in t:the second pointer walkstone character at a time.if i < len(s) and s[i] == ch:guard first, so once all ofsis matched we never read past its end. A match letssmove on to its next character.- Non-matching characters of
tare simply the ones we "delete". return i == len(s): true only if all ofswas matched in order.
Dry run on the example:
s = "abc", t = "ahbgdc" (LeetCode Example 1). Returns True.
| ch in t | s[i] we want | match? | i after |
|---|---|---|---|
a | a | yes | 1 |
h | b | no | 1 |
b | b | yes | 2 |
g | c | no | 2 |
d | c | no | 2 |
c | c | yes | 3 = len(s) → True |
Edge cases to test:
- Empty
s→ True. slonger thant→ False.- Repeated letters, e.g.
s = "aaa",t = "aa"→ False.
Complexity: O(len(t)) time, O(1) space.
Easy 977. Squares of a Sorted Array Google Meta Amazon
Problem: nums is sorted in non-decreasing order and may contain negatives. Return the squares of each number, also sorted.
Solve it on LeetCode ↗ · Video walkthrough ↗
nums = [-4,-1,0,3,10][0,1,9,16,100]nums = [-7,-3,2,3,11][4,9,9,49,121]Constraints: 1 ≤ nums.length ≤ 10⁴ · -10⁴ ≤ nums[i] ≤ 10⁴ · nums is sorted in non-decreasing order.
Hint 1
Big negative numbers become big squares. Where are the largest squares in the sorted input?
Hint 2
The largest square is at one of the two ends. Fill the result from the back.
From brute force to the pattern: Squaring everything and sorting is O(n log n). Because the largest absolute values sit at the ends, two pointers pick the next largest square in O(1), building the sorted result in O(n).
Spot it: Squaring breaks the order because big negatives become big positives. But the largest square must come from one of the two ends. Opposite-end pointers pick it, and we fill the answer from the back.
Approach:
left = 0,right = n − 1, andwrite = n − 1(the last slot of the answer).- Compare
abs(nums[left])andabs(nums[right]). The bigger one gives the largest remaining square. - Put that square at
result[write]and move the pointer it came from inward. - Move
writeleft and repeat untilleftpassesright.
class Solution:
def sortedSquares(self, nums: List[int]) -> List[int]:
n = len(nums)
result = [0] * n
left = 0
right = n - 1
# fill the answer from the largest square down to the smallest
write = n - 1
while left <= right:
if abs(nums[left]) > abs(nums[right]):
# a big negative on the left gives the larger square
result[write] = nums[left] * nums[left]
left = left + 1
else:
result[write] = nums[right] * nums[right]
right = right - 1
write = write - 1
return result
What the code is doing, line by line:
result = [0] * n: a list of the right size to fill in.left,right: the two ends ofnums. The largest absolute value is always at one of them.write = n - 1: we discover squares from biggest to smallest, so we fill the answer from the back.while left <= right:use<=so the last remaining element (when the pointers meet) is also placed.if abs(nums[left]) > abs(nums[right]):the left end wins: square it, put it atwrite, moveleftright. Otherwise the right end wins.write = write - 1: the next square is the next smaller one.
Dry run on the example:
nums = [-4, -1, 0, 3, 10] (LeetCode Example 1). Returns [0, 1, 9, 16, 100].
| left | right | compare | placed at write | result |
|---|---|---|---|---|
| 0 | 4 | |−4| vs |10| | 100 at 4 | [0, 0, 0, 0, 100] |
| 0 | 3 | |−4| vs |3| | 16 at 3 | [0, 0, 0, 16, 100] |
| 1 | 3 | |−1| vs |3| | 9 at 2 | [0, 0, 9, 16, 100] |
| 1 | 2 | |−1| vs |0| | 1 at 1 | [0, 1, 9, 16, 100] |
| 2 | 2 | |0| vs |0| | 0 at 0 | [0, 1, 9, 16, 100] |
Edge cases to test:
- All negative, e.g.
[-3, -2, -1]→ reversed squares. - All non-negative → squares already sorted.
- Zeros in the middle.
Complexity: O(n) time, O(n) for the output (O(1) extra besides it).
Easy 1768. Merge Strings Alternately Google Meta Amazon Microsoft
Problem: Merge word1 and word2 by taking letters in alternating order, starting with word1. If one word is longer, append its remaining letters at the end.
Solve it on LeetCode ↗ · Video walkthrough ↗
word1 = "abc", word2 = "pqr""apbqcr"word1 = "ab", word2 = "pqrs""apbqrs"Constraints: 1 ≤ word1.length, word2.length ≤ 100 · word1 and word2 consist of lowercase English letters.
Hint 1
Use one index per word and take a letter from each in turn.
Hint 2
When one word runs out, append the rest of the other.
From brute force to the pattern: Building the string with += in a loop can be O(n²). Collecting letters in a list with two pointers and joining once is O(m + n).
Spot it: Two strings consumed side by side → one pointer per string, moving together.
Approach:
- Start both pointers at 0 and collect letters in a list (building strings with
+=in a loop is slow). - While at least one word still has letters: take
word1[i]if it exists, thenword2[j]if it exists. - Join the list into a string.
class Solution:
def mergeAlternately(self, word1: str, word2: str) -> str:
i = 0
j = 0
parts = []
# keep going while EITHER word still has letters
while i < len(word1) or j < len(word2):
if i < len(word1):
parts.append(word1[i])
i = i + 1
if j < len(word2):
parts.append(word2[j])
j = j + 1
return "".join(parts)
What the code is doing, line by line:
i,j: the next letter to take from each word.parts = []: collect letters in a list;"".join(parts)at the end isO(n), while repeated string concatenation can beO(n²).while i < len(word1) or j < len(word2):loop until both words are used up, so a longer word's tail is not lost.if i < len(word1):take a letter fromword1only if it has one left; same forword2. This handles the leftover tail automatically.return "".join(parts): glue the letters together.
Dry run on the example:
word1 = "ab", word2 = "pqrs" (LeetCode Example 2). Returns "apbqrs".
| round | from word1 | from word2 | parts so far |
|---|---|---|---|
| 1 | a | p | a p |
| 2 | b | q | a p b q |
| 3 | — (used up) | r | a p b q r |
| 4 | — (used up) | s | a p b q r s |
Edge cases to test:
- Equal lengths.
- One word much longer than the other.
- One-letter words.
Complexity: O(m + n) time and space.
Medium 5. Longest Palindromic Substring Google Meta Amazon Cisco Microsoft Apple
Problem: Return the longest substring of s that reads the same forwards and backwards.
Solve it on LeetCode ↗ · Video walkthrough ↗
s = "babad""bab"s = "cbbd""bb"Constraints: 1 ≤ s.length ≤ 1000 · s consist of only digits and English letters.
Hint 1
Every palindrome has a centre: a character (odd length) or a gap between two characters (even length).
Hint 2
From each of the 2n − 1 centres, expand outward while the two ends match.
From brute force to the pattern: Checking every substring for being a palindrome is O(n³). Expanding around each centre reuses the matching work: each centre costs at most O(n), so O(n²) total with O(1) space.
Spot it: A palindrome is a mirror around its centre. Instead of squeezing two pointers inward, put them on a centre and walk them outward while the characters match. There are only 2n − 1 centres: every character (odd length) and every gap between two characters (even length).
Key insight: Checking every substring is O(n³). Growing from each centre reuses the matching work, so each centre costs at most O(n) and the whole thing is O(n²) with O(1) space.
Approach:
- Keep the best palindrome found so far as a start index and a length.
- For each index
center, try two starting pairs:(center, center)for odd lengths and(center, center + 1)for even lengths. - While both pointers are inside the string and
s[left] == s[right], moveleftone step left andrightone step right. - When the loop stops, the palindrome is
s[left + 1 : right], of lengthright − left − 1. Keep it if it beats the best. - Return the best slice.
class Solution:
def longestPalindrome(self, s: str) -> str:
best_start = 0
best_len = 0
for center in range(len(s)):
# odd length: centred ON a character; even length: centred BETWEEN two characters
for left, right in ((center, center), (center, center + 1)):
# spread outwards while both ends are inside the string and match
while left >= 0 and right < len(s) and s[left] == s[right]:
left = left - 1
right = right + 1
# the loop overshot by one on each side, so the palindrome is s[left+1 : right]
length = right - left - 1
if length > best_len:
best_len = length
best_start = left + 1
return s[best_start:best_start + best_len]
What the code is doing, line by line:
best_start,best_len: where the longest palindrome so far begins and how long it is.for center in range(len(s)):every index is a possible middle.for left, right in ((center, center), (center, center + 1)):two tries per centre.(c, c)grows odd palindromes like"bab";(c, c + 1)grows even ones like"abba".while left >= 0 and right < len(s) and s[left] == s[right]:keep expanding while the ends are valid and equal. The bounds are checked first sos[-1]is never read by mistake.left = left - 1,right = right + 1: one step outward on each side.length = right - left - 1: the loop exits one step past the palindrome on both sides, so the real span isleft + 1 .. right − 1.if length > best_len:remember the new best;best_start = left + 1undoes the overshoot.return s[best_start:best_start + best_len]: slice out the winner.
Dry run on the example:
s = "babad" (LeetCode Example 1). Returns "bab" ("aba" is equally long and also accepted).
| center | odd try grows to | even try grows to | best so far |
|---|---|---|---|
| 0 | "b" | (no match) | "b" |
| 1 | "bab" | (no match) | "bab" |
| 2 | "aba" | (no match) | "bab" (tie, not longer) |
| 3 | "a" | (no match) | "bab" |
| 4 | "d" | (no match) | "bab" |
Edge cases to test:
- A single character → itself.
- An even-length palindrome, e.g.
"cbbd"→"bb"(needs the gap centres). - All the same character, e.g.
"aaaa"→ the whole string.
Complexity: O(n²) time (2n − 1 centres, each expanding up to n), O(1) extra space.
Medium 189. Rotate Array Google Meta Amazon Microsoft Apple
Problem: Rotate nums to the right by k steps, in place. For example [1,2,3,4,5,6,7], k = 3 becomes [5,6,7,1,2,3,4].
Solve it on LeetCode ↗ · Video walkthrough ↗
nums = [1,2,3,4,5,6,7], k = 3[5,6,7,1,2,3,4]nums = [-1,-100,3,99], k = 2[3,99,-1,-100]Constraints: 1 ≤ nums.length ≤ 10⁵ · -2³¹ ≤ nums[i] ≤ 2³¹ - 1 · 0 ≤ k ≤ 10⁵
Hint 1
Rotating right by k moves the last k elements to the front. Is k bigger than n a problem?
Hint 2
Reverse the whole array, then reverse the first k and the remaining n − k separately.
From brute force to the pattern: Rotating one step k times is O(n · k); copying into a new array uses O(n) space. Three in-place reversals give the rotation in O(n) time and O(1) space.
Spot it: "In place" with O(1) extra space rules out building a new array. Reversing a range in place is the classic opposite-end two-pointer swap, and three reversals produce a rotation.
Key insight: Rotating right by k moves the last k values to the front. Reverse the whole array (the last k are now first, but backwards), then reverse the first k and the rest separately to fix their order.
Approach:
k = k % n: rotating bynchanges nothing, so only the remainder matters.- Reverse the whole array.
- Reverse the first
kvalues. - Reverse the remaining
n − kvalues. - Each reversal swaps
leftandright, then moves them toward each other until they meet.
class Solution:
def rotate(self, nums: List[int], k: int) -> None:
n = len(nums)
# rotating by n is a full circle, so only the remainder matters
k = k % n
def reverse(left, right):
# classic opposite-ends two pointers: swap, then step inward
while left < right:
temp = nums[left]
nums[left] = nums[right]
nums[right] = temp
left = left + 1
right = right - 1
reverse(0, n - 1) # whole array: the last k values move to the front (backwards)
reverse(0, k - 1) # put the first k back in order
reverse(k, n - 1) # put the rest back in order
What the code is doing, line by line:
k = k % n: withn = 7, rotating by10is the same as rotating by3. Without this,k − 1could point past the end.def reverse(left, right):a helper that reversesnums[left..right]in place.while left < right:swap the two ends using a temporary variable, then move both pointers inward until they meet.reverse(0, n - 1): the lastkvalues are now at the front, but in reverse order, and so is everything else.reverse(0, k - 1)andreverse(k, n - 1): fix the order inside each block.
Dry run on the example:
nums = [1, 2, 3, 4, 5, 6, 7], k = 3 (LeetCode Example 1). Result [5, 6, 7, 1, 2, 3, 4].
| step | range reversed | nums after |
|---|---|---|
| start | — | [1, 2, 3, 4, 5, 6, 7] |
| 1 | 0..6 (whole) | [7, 6, 5, 4, 3, 2, 1] |
| 2 | 0..2 (first k) | [5, 6, 7, 4, 3, 2, 1] |
| 3 | 3..6 (the rest) | [5, 6, 7, 1, 2, 3, 4] |
Edge cases to test:
k ≥ n: usek % n(e.g.k = nchanges nothing).k = 0.- A single element.
Complexity: O(n) time (every element is swapped at most twice), O(1) extra space.
Medium 31. Next Permutation Google Meta Amazon Microsoft
Problem: Rearrange nums in place into the next lexicographically greater permutation. If it's already the largest (sorted descending), wrap around to the smallest (sorted ascending).
nums = [1,2,3][1,3,2]nums = [3,2,1][1,2,3]Constraints: 1 ≤ nums.length ≤ 100 · 0 ≤ nums[i] ≤ 100
Hint 1
Compare with counting: to get the next bigger number you change the rightmost digit you can. Scan from the right: where does the order first go down?
Hint 2
After swapping the pivot with the next larger value from the right, the tail is still descending. What one operation makes it as small as possible?
From brute force to the pattern: Generating every permutation, sorting them and finding the next one is O(n! · n). All the work is in the tail: the longest descending suffix can't be increased on its own, so you change the element just before it, then reset the suffix to its smallest order. That's three linear passes: O(n) time, O(1) space.
Spot it: "Next greater arrangement", in place, O(1) extra memory. Think of the digits of a number: to make the smallest bigger number you change the digits as far right as possible. That's a scan from the right plus two-pointer swaps and an in-place reverse.
Why not another pattern? Backtracking could list all permutations in order and pick the next one, but that's O(n!). Sorting the tail is O(n log n); since the tail is always in descending order, reversing it with two pointers does the same job in O(n).
Key insight: Scanning from the right, the suffix is non-increasing until some nums[i] < nums[i + 1] (the pivot). That suffix is already the largest arrangement of its values, so the change must happen at i: swap nums[i] with the smallest value in the suffix that is bigger than it (the rightmost one), then make the suffix as small as possible by reversing it.
Approach:
- From the right, find the first index
iwithnums[i] < nums[i + 1]. If there's none, the whole array is descending: reverse it and stop. - From the right again, find the first
jwithnums[j] > nums[i](the smallest bigger value in the suffix). - Swap
nums[i]andnums[j]. The suffix afteriis still in descending order. - Reverse the suffix
nums[i + 1:]with two pointers so it becomes ascending: the smallest possible tail.
class Solution:
def nextPermutation(self, nums: List[int]) -> None:
n = len(nums)
# 1) find the pivot: the rightmost i with nums[i] < nums[i + 1]
i = n - 2
while i >= 0 and nums[i] >= nums[i + 1]:
i = i - 1 # still descending from the right: keep moving left
if i >= 0:
# 2) find the rightmost value bigger than the pivot
j = n - 1
while nums[j] <= nums[i]:
j = j - 1 # too small: keep moving left
# 3) swap them: the prefix up to i is now as small as possible but bigger
nums[i], nums[j] = nums[j], nums[i]
# 4) reverse the tail after i so it becomes the smallest arrangement
left = i + 1
right = n - 1
while left < right:
nums[left], nums[right] = nums[right], nums[left] # swap the two ends
left = left + 1
right = right - 1
What the code is doing, line by line:
i = n - 2and the firstwhile: walk left while the array is non-increasing from the right. Stop at the pivotiwherenums[i] < nums[i + 1].if i >= 0:a pivot exists. (Ifibecame −1, the array was the last permutation and we only reverse everything.)- Second
while:jmoves left from the end untilnums[j] > nums[i]. Because the suffix is descending, the first suchjis the smallest value larger than the pivot. nums[i], nums[j] = nums[j], nums[i]: Python swaps both values in one statement.left = i + 1,right = n - 1and the last loop: reverse the suffix in place, turning descending into ascending.- The function returns nothing: LeetCode checks
numsafter the call.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Already the largest, e.g.
[3, 2, 1]→[1, 2, 3]. - Duplicates, e.g.
[1, 5, 1]→[5, 1, 1](use≥/≤in the scans so equal values are skipped). - A single element → unchanged.
Complexity: O(n) time (at most three linear scans), O(1) extra space.
Medium 165. Compare Version Numbers Google Amazon
Problem: Compare two version strings such as "1.01" and "1.001". Revisions are separated by dots and compared as integers (leading zeros ignored, missing revisions are 0). Return −1, 1 or 0.
version1 = "1.2", version2 = "1.10"-1version1 = "1.01", version2 = "1.001"0Constraints: 1 ≤ version1.length, version2.length ≤ 500 · version1 and version2 only contain digits and '.'. · version1 and version2 are valid version numbers. · All the given revisions in version1 and version2 can be stored in a 32-bit integer.
Hint 1
Compare revision by revision, as integers, not as strings.
Hint 2
Walk both strings with two pointers, parsing one number up to each dot; treat a finished string as 0.
From brute force to the pattern: Padding both versions to the same number of revisions and comparing them as strings fails on leading zeros and on 10 vs 9. Parsing each revision as an integer with two pointers handles both, in one pass and O(1) space.
Spot it: Two sequences compared position by position, possibly of different lengths: one pointer walking each string, reading one revision number at a time.
Why not another pattern? Splitting both strings on . is simpler and fine here (O(n) space). The two-pointer scan avoids building the lists and stops at the first difference. String comparison is wrong: "10" < "9" as strings.
Approach:
- Keep a pointer
iintoversion1andjintoversion2. - While either string has characters left: read the next revision from each (digits up to the next dot) as an integer; an exhausted string gives 0.
- If the two numbers differ, return −1 or 1. Otherwise skip the dots and continue.
- If everything matched, return 0.
class Solution:
def compareVersion(self, version1: str, version2: str) -> int:
i = 0
j = 0
while i < len(version1) or j < len(version2):
a = 0 # next revision of version1 (0 if it has run out)
while i < len(version1) and version1[i] != ".":
a = a * 10 + int(version1[i]) # build the number digit by digit
i = i + 1
b = 0 # next revision of version2
while j < len(version2) and version2[j] != ".":
b = b * 10 + int(version2[j])
j = j + 1
if a != b:
return -1 if a < b else 1 # the first different revision decides
i = i + 1 # skip the dot
j = j + 1
return 0
What the code is doing, line by line:
while i < len(version1) or j < len(version2): keep going while either version has revisions left.- The inner loops read one revision each, turning digits into a number (
"01"becomes 1, so leading zeros vanish). - If a string is exhausted its inner loop doesn't run, so its revision stays 0, exactly the rule for missing revisions.
if a != b: return ...: the first differing revision decides the comparison.i = i + 1,j = j + 1: step over the dots (stepping past the end is harmless).
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Leading zeros, e.g.
"1.01"vs"1.001"→ 0. - Different lengths with trailing zeros, e.g.
"1.0"vs"1.0.0.0"→ 0. "0.1"vs"1.1"→ −1.
Complexity: O(n + m) time, O(1) extra space.
Medium 443. String Compression Google Meta Amazon Microsoft
Problem: Compress the character array in place: each run of a repeated character becomes the character followed by the run length (only if longer than 1, written digit by digit). Return the new length.
chars = ["a","a","b","b","c","c","c"]6chars = ["a"]1Constraints: 1 ≤ chars.length ≤ 2000 · chars[i] is a lowercase English letter, uppercase English letter, digit, or symbol.
Hint 1
You need to know where each run of equal characters ends.
Hint 2
Use a read pointer to find runs and a write pointer to overwrite the array from the front.
From brute force to the pattern: Building the compressed string in a new list and copying it back is easy but uses O(n) extra space. A read/write pair works in place because the output is never longer than the part of the input already read.
Spot it: "In place" + rewriting a shorter version of the array → a fast read pointer and a slow write pointer moving the same way.
Why not another pattern? Building a new string and copying it back uses O(n) space, which the problem forbids. A hash map of counts loses the order and the run boundaries.
Approach:
readfinds the end of the current run of equal characters.- Write the character at
write; if the run is longer than 1, write each digit of its length. - Continue from the start of the next run.
writeis the new length.
class Solution:
def compress(self, chars: List[str]) -> int:
write = 0 # where the next output character goes
read = 0 # start of the current run
while read < len(chars):
letter = chars[read]
run_end = read
while run_end < len(chars) and chars[run_end] == letter:
run_end = run_end + 1 # extend the run of equal letters
count = run_end - read
chars[write] = letter # write the letter once
write = write + 1
if count > 1:
for digit in str(count):
chars[write] = digit # then its count, digit by digit
write = write + 1
read = run_end # jump to the next run
return write
What the code is doing, line by line:
write,read: the slow and fast pointers.writenever passesread, because a run of lengthkproduces at most1 + len(str(k)) ≤ kcharacters.- Inner
while:run_endstops at the first different character, socount = run_end - readis the run length. chars[write] = letter: every run writes its letter.for digit in str(count): a count like 12 is written as'1','2'.read = run_end: the next run starts right after this one;return writeis the compressed length.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- A single character → 1.
- A run longer than 9, e.g. 12
'b's →['b', '1', '2']. - No repeats at all → unchanged length.
Complexity: O(n) time, O(1) extra space.
Medium 532. K-diff Pairs in an Array Google
Problem: Return the number of unique pairs (nums[i], nums[j]) with i != j whose difference is exactly k.
nums = [3,1,4,1,5], k = 22nums = [1,2,3,4,5], k = 14Constraints: 1 ≤ nums.length ≤ 10⁴ · -10⁷ ≤ nums[i] ≤ 10⁷ · 0 ≤ k ≤ 10⁷
Hint 1
Sorting turns "difference equals k" into a monotone search.
Hint 2
Two pointers moving the same way: move right when the difference is too small and left when it's too big; skip duplicates after a match.
From brute force to the pattern: Comparing all pairs and putting the matches into a set is O(n²). After sorting, each pointer only moves forward, so the scan is O(n), plus the sort.
Spot it: A pair condition on a difference: after sorting, the difference between nums[right] and nums[left] grows when right moves and shrinks when left moves, so two pointers moving in the same direction can find every pair.
Why not another pattern? A hash-map version (count values; for k = 0 count values seen twice, else check x + k) is also O(n) and a fine alternative. Checking all pairs is O(n²).
Approach:
- Sort the array.
- Keep
left = 0,right = 1. Comparenums[right] − nums[left]withk. - Too small → move
right. Too big → moveleft(keepleft < right). - Equal → count it, then move
leftpast all copies ofnums[left]so the pair isn't counted again.
class Solution:
def findPairs(self, nums: List[int], k: int) -> int:
nums = sorted(nums)
count = 0
left = 0
right = 1
while right < len(nums):
if left == right:
right = right + 1 # the two pointers must point at different elements
continue
diff = nums[right] - nums[left]
if diff < k:
right = right + 1 # need a bigger difference
elif diff > k:
left = left + 1 # need a smaller difference
else:
count = count + 1 # found a unique pair
value = nums[left]
while left < len(nums) and nums[left] == value:
left = left + 1 # skip copies so this pair isn't counted again
return count
What the code is doing, line by line:
nums = sorted(nums): sorting makes the difference move predictably with the pointers.if left == right: a pair needs two different indices.diff < k: movingrightto a larger value increases the difference;diff > k: movingleftdecreases it.- On a match, skip every copy of
nums[left]; that guarantees each value pair is counted once (this also handlesk = 0, which needs two equal values).
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
k = 0: only values that appear at least twice count, e.g.[1, 1, 1]→ 1.- Duplicates that would create the same pair twice, e.g.
[3, 1, 4, 1, 5],k = 2→ 2. - Negative numbers.
Complexity: O(n log n) time for the sort, O(n) space for the sorted copy.
Medium 556. Next Greater Element III Google
Problem: Return the smallest integer greater than n that uses exactly the same digits, or −1 if none exists or it doesn't fit in a signed 32-bit integer.
n = 1221n = 21-1Constraints: 1 ≤ n ≤ 2³¹ - 1
Hint 1
You want the next permutation of the digits.
Hint 2
Pivot from the right, swap with the next larger digit, reverse the tail. Then check the 32-bit limit.
From brute force to the pattern: Trying every permutation of the digits and picking the smallest larger one is O(d! · d). Next Permutation finds it in O(d).
Spot it: "Smallest greater arrangement of the same digits" is exactly Next Permutation applied to the digit list.
Why not another pattern? Generating all permutations of the digits is O(d!). Sorting is unnecessary because the tail is already in descending order and only needs reversing.
Approach:
- Turn
ninto a list of digits. - Find the pivot
i(rightmostdigits[i] < digits[i + 1]). If there is none, return −1. - Swap it with the rightmost digit larger than it, then reverse the tail after
i. - Convert back; return −1 if the result exceeds
2³¹ − 1.
class Solution:
def nextGreaterElement(self, n: int) -> int:
digits = list(str(n))
i = len(digits) - 2
while i >= 0 and digits[i] >= digits[i + 1]:
i = i - 1 # find the pivot from the right
if i < 0:
return -1 # digits are in descending order: no bigger number
j = len(digits) - 1
while digits[j] <= digits[i]:
j = j - 1 # rightmost digit bigger than the pivot
digits[i], digits[j] = digits[j], digits[i]
left = i + 1
right = len(digits) - 1
while left < right:
digits[left], digits[right] = digits[right], digits[left] # reverse the tail
left = left + 1
right = right - 1
result = int("".join(digits))
return result if result < 2 ** 31 else -1 # must fit in 32 bits
What the code is doing, line by line:
digits = list(str(n)): work on the digits as characters; comparing'3' < '5'works digit by digit.- The first loop finds the pivot exactly as in Next Permutation. No pivot means the digits are already the largest arrangement.
- The second loop finds the smallest digit bigger than the pivot (it's the rightmost one, since the tail is descending).
- Swap, then reverse the tail with two pointers to make it as small as possible.
result < 2 ** 31: LeetCode requires a 32-bit result; Python itself wouldn't overflow.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Digits in descending order, e.g. 21 → −1.
- Overflow, e.g. 2147483486 → −1.
- Repeated digits, e.g. 12443322 → 13222344.
Complexity: O(d) time and space for d digits (at most 10).
Medium 611. Valid Triangle Number Google Amazon
Problem: Count the triplets in nums that can form a triangle (the sum of any two sides is greater than the third).
nums = [2,2,3,4]3nums = [4,2,3,4]4Constraints: 1 ≤ nums.length ≤ 1000 · 0 ≤ nums[i] ≤ 1000
Hint 1
Sort the sides. If a ≤ b ≤ c, which single inequality matters?
Hint 2
Fix the largest side c, then count pairs a + b > c with two pointers, adding a whole block of pairs at once.
From brute force to the pattern: Checking every triplet is O(n³). Sorting reduces the triangle rule to one inequality, and fixing the longest side lets two pointers count all valid pairs in O(n) per side: O(n²) total.
Spot it: With sides sorted a ≤ b ≤ c, the only condition that can fail is a + b > c. Fixing c turns it into "count pairs with sum > c" in a sorted prefix: a classic opposite-end two-pointer count.
Why not another pattern? Three nested loops are O(n³). Binary search for each pair is O(n² log n). Two pointers count a whole block of pairs at once, giving O(n²).
Approach:
- Sort the array.
- For each index
k(the longest side), setleft = 0,right = k − 1. - If
nums[left] + nums[right] > nums[k], everyleft'fromlefttoright − 1also works withright: addright − left, moverightleft. - Otherwise move
leftright to increase the sum.
class Solution:
def triangleNumber(self, nums: List[int]) -> int:
nums = sorted(nums)
count = 0
for k in range(len(nums) - 1, 1, -1):
longest = nums[k] # fix the longest side
left = 0
right = k - 1
while left < right:
if nums[left] + nums[right] > longest:
count = count + (right - left) # every left..right-1 also works with this right
right = right - 1
else:
left = left + 1 # the sum is too small: use a longer short side
return count
What the code is doing, line by line:
nums = sorted(nums): witha ≤ b ≤ c, onlya + b > cneeds checking.for k in range(len(nums) - 1, 1, -1): each element in turn plays the longest side.count + (right - left): if the smallest candidatenums[left]already works withnums[right], all larger ones in between work too.else: left = left + 1: the pair is too short, so increase the smaller side.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Zeros can't form a triangle (
0 + b > cfails whenb ≤ c). - Fewer than three numbers → 0.
- Equal sides, e.g.
[2, 2, 2]→ 1.
Complexity: O(n²) time, O(n) space for the sorted copy.
Medium 633. Sum of Square Numbers Google Amazon
Problem: Return True if there are integers a and b with a² + b² = c.
c = 5truec = 3falseConstraints: 0 ≤ c ≤ 2³¹ - 1
Hint 1
The candidate squares form a sorted sequence.
Hint 2
Put one pointer at 0 and one at ⌊√c⌋, and squeeze them like Two Sum II.
From brute force to the pattern: Trying every pair (a, b) up to √c is O(c). Since squares are sorted, two pointers find a pair in O(√c).
Spot it: The squares 0², 1², …, r² (with r = ⌊√c⌋) form a sorted list, and you want two of them adding up to c: Two Sum II with opposite-end pointers.
Why not another pattern? Checking every a and computing whether c − a² is a perfect square is also O(√c) and fine. Trying all pairs (a, b) is O(c).
Approach:
left = 0,right = isqrt(c).- If
left² + right² == c, return True. - If the sum is too small, increase
left; if too big, decreaseright. - Stop when
left > right.
from math import isqrt
class Solution:
def judgeSquareSum(self, c: int) -> bool:
left = 0
right = isqrt(c) # the largest b with b * b <= c
while left <= right:
total = left * left + right * right
if total == c:
return True
if total < c:
left = left + 1 # need a bigger sum
else:
right = right - 1 # need a smaller sum
return False
What the code is doing, line by line:
isqrt(c): the integer square root, soright²never exceedsc.while left <= right:aandbmay be equal (e.g.2 = 1² + 1²).- The comparisons move exactly one pointer, like Two Sum II on a sorted list of squares.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
c = 0→ True (0² + 0²).- Perfect squares, e.g.
c = 4→ True. - Large
cnear2³¹ − 1(useisqrt, not floating-pointsqrt).
Complexity: O(√c) time, O(1) space.
Medium 777. Swap Adjacent in LR String Google
Problem: A move replaces "XL" with "LX" (an L moves left) or "RX" with "XR" (an R moves right). Can start be turned into result?
start = "RXXLRXRXL", result = "XRLXXRRLX"truestart = "X", result = "L"falseConstraints: 1 ≤ start.length ≤ 10⁴ · start.length == result.length · Both start and result will only consist of characters in 'L', 'R', and 'X'.
Hint 1
Can an L ever jump over an R? What does that say about the order of the pieces?
Hint 2
Match the pieces of both strings in order with two pointers; check each piece only moved in its allowed direction.
From brute force to the pattern: Simulating all possible moves with BFS explores exponentially many strings. Two invariants (same piece order, allowed direction per piece) characterise reachability, so a single two-pointer scan decides it.
Spot it: Pieces can't pass each other, so the sequence of Ls and Rs (ignoring X) must be identical. Matching the pieces in order means one pointer per string, each skipping the Xs.
Why not another pattern? BFS over string states is exponential. The invariant (same piece order, L never moves right, R never moves left) is checkable in one two-pointer pass.
Approach:
- Move
iinstartandjinresultto their next non-Xcharacters. - If one string has a piece and the other doesn't, or the pieces differ, return False.
- An
Lcan only move left: its position instartmust be ≥ its position inresult. AnRmust havei ≤ j. - Advance both and repeat; return True at the end.
class Solution:
def canTransform(self, start: str, result: str) -> bool:
n = len(start)
i = 0
j = 0
while i < n or j < n:
while i < n and start[i] == "X":
i = i + 1 # skip blanks in start
while j < n and result[j] == "X":
j = j + 1 # skip blanks in result
if i == n or j == n:
return i == n and j == n # both must run out of pieces together
if start[i] != result[j]:
return False # pieces can't pass each other, so the order must match
if start[i] == "L" and i < j:
return False # an L would have to move right
if start[i] == "R" and i > j:
return False # an R would have to move left
i = i + 1
j = j + 1
return True
What the code is doing, line by line:
- The two inner
whileloops skipXs, soiandjland on the next piece of each string. if i == n or j == n: one string ran out of pieces; that's fine only if the other did too.start[i] != result[j]: the k-th piece must be the same letter in both strings.Lwithi < jorRwithi > j: the piece would have to move in a forbidden direction.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- No pieces at all → True.
- Different numbers of pieces → False.
start = "X",result = "L"→ False.
Complexity: O(n) time, O(1) space.
Medium 923. 3Sum With Multiplicity Google
Problem: Count index triples i < j < k with arr[i] + arr[j] + arr[k] == target, modulo 10⁹ + 7.
arr = [1,1,2,2,3,3,4,4,5,5], target = 820arr = [1,1,2,2,2,2], target = 512Constraints: 3 ≤ arr.length ≤ 3000 · 0 ≤ arr[i] ≤ 100 · 0 ≤ target ≤ 300
Hint 1
Fix one element and count pairs in the rest that sum to the remainder.
Hint 2
With duplicates, count a matching pair of runs as (left run length) × (right run length), and all-equal ranges as m·(m−1)/2.
From brute force to the pattern: Three nested loops are O(n³), about 27·10⁹ steps for n = 3000. Fixing one index and squeezing the other two is O(n²); counting runs of equal values keeps it correct when values repeat.
Spot it: A triplet-sum condition on an array: fix the first element and squeeze the other two from both ends of a sorted array. Because indices (not values) are counted, runs of equal values have to be counted with combinatorics.
Why not another pattern? Values are only 0..100, so counting by value and enumerating value triples is another neat O(101²) solution. Three nested loops (O(n³)) are too slow for n = 3000.
Approach:
- Sort the array. Fix
i; look for pairs ini + 1 .. n − 1summing totarget − arr[i]withleftandright. - If the pair sum is too small, move
left; too big, moveright. - On a match with
arr[left] != arr[right]: count the run ofarr[left]values and the run ofarr[right]values; add their product and skip both runs. - On a match with equal values: all
m = right − left + 1elements are the same, so addm·(m−1)/2and stop.
class Solution:
def threeSumMulti(self, arr: List[int], target: int) -> int:
MOD = 10 ** 9 + 7
arr = sorted(arr)
n = len(arr)
answer = 0
for i in range(n):
need = target - arr[i] # the other two must add up to this
left = i + 1
right = n - 1
while left < right:
pair = arr[left] + arr[right]
if pair < need:
left = left + 1
elif pair > need:
right = right - 1
elif arr[left] != arr[right]:
left_count = 1
while left + 1 < right and arr[left + 1] == arr[left]:
left = left + 1 # extend the run of equal left values
left_count = left_count + 1
right_count = 1
while right - 1 > left and arr[right - 1] == arr[right]:
right = right - 1 # extend the run of equal right values
right_count = right_count + 1
answer = answer + left_count * right_count # every left copy pairs with every right copy
left = left + 1
right = right - 1
else:
m = right - left + 1 # everything between is the same value
answer = answer + m * (m - 1) // 2 # choose any 2 of them
break
return answer % MOD
What the code is doing, line by line:
need = target - arr[i]: the remaining pair must sum toneed.- Too-small / too-big moves are the usual sorted two-pointer squeeze.
- Different values on a match: count the run lengths on both sides; each combination is a distinct index pair.
- Equal values on a match: every index between
leftandrightholds that value, so any 2 of themwork:m·(m−1)/2. answer % MODat the end (Python integers don't overflow, so one final%is enough).
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- All values equal, e.g.
[1, 1, 1, 1],target = 3→ 4. - Many duplicates, e.g.
[1, 1, 2, 2, 2, 2],target = 5→ 12. - No valid triple → 0.
Complexity: O(n²) time, O(n) space for the sorted copy.
Medium 1861. Rotating the Box Google Amazon
Problem: A box is an m × n grid of stones '#', obstacles '*' and empty cells '.'. Rotate it 90° clockwise; gravity then pulls stones down until they hit an obstacle, another stone or the floor. Return the rotated box.
boxGrid = [["#",".","#"]][["."],
["#"],
["#"]]boxGrid = [["#",".","*","."],
["#","#","*","."]][["#","."],
["#","#"],
["*","*"],
[".","."]]Constraints: m == boxGrid.length · n == boxGrid[i].length · 1 ≤ m, n ≤ 500 · boxGrid[i][j] is either '#', '*', or '.'.
Hint 1
After a clockwise rotation, which direction is "down" in the original rows?
Hint 2
Let stones fall right in each row with a write pointer that resets at obstacles, then rotate.
From brute force to the pattern: Moving each stone one cell at a time until it stops costs O(n²) per row. A write pointer drops every stone directly into its final slot, so each row takes one pass.
Spot it: After rotating clockwise, "down" is the right end of each original row. So in each row, stones slide right until an obstacle: a read pointer scanning from the right and a write pointer marking where the next stone lands.
Why not another pattern? Simulating gravity one cell at a time repeats work (O(n²) per row). Rotating first and then simulating gravity column by column is the same idea done twice. The write pointer handles each row in one pass.
Approach:
- For each row, set
empty = n − 1(the rightmost free spot) and scancfrom right to left. - An obstacle
'*'resetsempty = c − 1(stones can't pass it). - A stone
'#'moves toempty: setrow[c] = '.',row[empty] = '#', thenempty -= 1. - Rotate: cell
(r, c)of the box goes to(c, m − 1 − r)of the result.
class Solution:
def rotateTheBox(self, boxGrid: List[List[str]]) -> List[List[str]]:
m = len(boxGrid)
n = len(boxGrid[0])
for row in boxGrid:
empty = n - 1 # where the next falling stone lands
for c in range(n - 1, -1, -1):
if row[c] == "*":
empty = c - 1 # an obstacle: stones stop just before it
elif row[c] == "#":
row[c] = "." # the stone leaves its cell ...
row[empty] = "#" # ... and lands as far right as possible
empty = empty - 1
rotated = [["."] * m for _ in range(n)]
for r in range(m):
for c in range(n):
rotated[c][m - 1 - r] = boxGrid[r][c] # 90 degrees clockwise
return rotated
What the code is doing, line by line:
empty = n - 1: before rotating, gravity points to the right end of every row.- Scanning right to left,
'*'sets the landing spot just left of the obstacle. - For a stone, clear its cell first and then write it at
empty(if they're the same cell, the stone simply stays). rotated[c][m - 1 - r] = boxGrid[r][c]: the standard clockwise rotation; the result isn × m.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- A row with no obstacles: all stones pile up at the right end.
- A stone directly next to an obstacle stays put.
- A single row → the output is a single column.
Complexity: O(m · n) time, O(m · n) for the output.
Medium 2149. Rearrange Array Elements by Sign Google Meta Amazon
Problem: nums has equally many positive and negative numbers. Rearrange it so signs alternate, starting with a positive, while keeping the original order within each sign.
nums = [3,1,-2,-5,2,-4][3,-2,1,-5,2,-4]nums = [-1,1][1,-1]Constraints: 2 ≤ nums.length ≤ 2 * 10⁵ · nums.length is even · 1 ≤ |nums[i]| ≤ 10⁵ · nums consists of equal number of positive and negative integers.
Hint 1
Where must the k-th positive number end up? And the k-th negative?
Hint 2
Use two write pointers: even indices for positives, odd indices for negatives.
From brute force to the pattern: Repeatedly searching for the next number of the needed sign is O(n²). Writing each number straight into its slot with two pointers is one pass.
Spot it: Two interleaved output streams: one write pointer for even positions and one for odd positions, both moving forward.
Why not another pattern? Splitting into two lists and zipping them back is equally O(n) (with two extra lists). Sorting would break the relative order.
Approach:
- Create
resultof the same length;pos = 0,neg = 1. - Scan
numsin order: a positive goes toresult[pos](thenpos += 2), a negative toresult[neg](thenneg += 2).
class Solution:
def rearrangeArray(self, nums: List[int]) -> List[int]:
result = [0] * len(nums)
pos = 0 # next even index (positives)
neg = 1 # next odd index (negatives)
for x in nums:
if x > 0:
result[pos] = x
pos = pos + 2 # skip over the slot reserved for a negative
else:
result[neg] = x
neg = neg + 2
return result
What the code is doing, line by line:
posandnegare write pointers into the even and odd slots.- Scanning
numsonce keeps each sign in its original order, as required. - Each pointer jumps by 2 because the slots in between belong to the other sign.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Two elements, e.g.
[-1, 1]→[1, -1]. - All positives first, then all negatives.
- Already alternating.
Complexity: O(n) time, O(n) for the output.
Medium 2332. The Latest Time to Catch a Bus Google
Problem: Buses depart at buses[i], passengers arrive at passengers[j], each bus takes at most capacity waiting passengers (earliest first). You may arrive at any minute not already used by another passenger. What's the latest time you can arrive and still board?
buses = [10,20], passengers = [2,17,18,19], capacity = 216buses = [20,30,10], passengers = [19,13,26,4,25,11,21], capacity = 220Constraints: n == buses.length · m == passengers.length · 1 ≤ n, m, capacity ≤ 10⁵ · 2 ≤ buses[i], passengers[i] ≤ 10⁹ · Each element in buses is unique. · Each element in passengers is unique.
Hint 1
Only the last bus matters for the answer. Simulate boarding to find who gets on it.
Hint 2
Start from the latest possible minute (departure time or last boarder's time) and step back while that minute is taken.
From brute force to the pattern: Trying every possible arrival minute and re-simulating the boarding is far too slow (times go up to 10⁹). One simulation pinpoints the latest feasible minute, and a short backward walk avoids taken minutes.
Spot it: Two sorted timelines (buses and passengers) processed together: a pointer over passengers advances as each bus fills. The answer depends only on the last bus, so after the simulation, step back from the latest possible time.
Why not another pattern? Binary search on your arrival time with a full simulation per guess works but is O((n + m) log T). The direct simulation plus a backward scan is linear after sorting.
Approach:
- Sort both arrays. For each bus, board waiting passengers (
passengers[j] ≤ bus) until it's full;jmoves forward. - Look at the last bus: if it has a free seat, the latest candidate is its departure time; otherwise it's the time of the last passenger who boarded it.
- Move the candidate one minute earlier while it collides with an existing passenger (walking
kback over the boarded passengers).
class Solution:
def latestTimeCatchTheBus(self, buses: List[int], passengers: List[int], capacity: int) -> int:
buses = sorted(buses)
passengers = sorted(passengers)
j = 0 # next passenger who hasn't boarded
count = 0
for bus in buses:
count = 0 # seats used on this bus
while j < len(passengers) and passengers[j] <= bus and count < capacity:
j = j + 1 # this passenger boards
count = count + 1
k = j - 1 # last passenger who boarded (on any bus)
# free seat on the last bus: arrive when it leaves; full: arrive with the last boarder
time = buses[-1] if count < capacity else passengers[k]
while k >= 0 and passengers[k] == time:
time = time - 1 # that minute is taken: try one minute earlier
k = k - 1
return time
What the code is doing, line by line:
- Sorting makes boarding a simple forward scan: passengers board in arrival order, buses leave in time order.
- After the loop,
countis how many boarded the last bus andjis the number of passengers who boarded at all. - A free seat means arriving exactly at the last departure works; a full bus means you must beat its last boarder.
- The backward
whileskips minutes already used by boarded passengers (their times are consecutive in the sorted list).
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- The last bus isn't full: the answer can be its departure time.
- Several passengers arrive in consecutive minutes right before the candidate.
- Passengers who arrive after the last bus never board.
Complexity: O(n log n + m log m) for the sorts, then O(n + m).
Medium 2337. Move Pieces to Obtain a String Google
Problem: Strings contain 'L', 'R' and blanks '_'. An L can move left into a blank, an R can move right into a blank. Can start be turned into target?
start = "_L__R__R_", target = "L______RR"truestart = "R_L_", target = "__LR"falseConstraints: n == start.length == target.length · 1 ≤ n ≤ 10⁵ · start and target consist of the characters 'L', 'R', and '_'.
Hint 1
Can an L ever get past an R?
Hint 2
Match pieces in order with two pointers and check each piece moved in its allowed direction.
From brute force to the pattern: Exploring move sequences is exponential. The two invariants (piece order and direction) make one linear scan enough.
Spot it: Pieces never pass each other, so match the k-th piece of both strings with two pointers that skip blanks.
Why not another pattern? Simulating moves or searching over states is exponential; the order-and-direction invariant decides it in one pass.
Approach:
- Advance
iandjto the next piece instartandtarget. - If only one string has a piece left, or the pieces differ, return False.
Lneedsi ≥ j(it can only move left);Rneedsi ≤ j.- Advance both and continue.
class Solution:
def canChange(self, start: str, target: str) -> bool:
n = len(start)
i = 0
j = 0
while i < n or j < n:
while i < n and start[i] == "_":
i = i + 1 # skip blanks in start
while j < n and target[j] == "_":
j = j + 1 # skip blanks in target
if i == n or j == n:
return i == n and j == n # both must run out of pieces together
if start[i] != target[j]:
return False # pieces can't pass each other
if start[i] == "L" and i < j:
return False # L can't move right
if start[i] == "R" and i > j:
return False # R can't move left
i = i + 1
j = j + 1
return True
What the code is doing, line by line:
- Skip blanks so
iandjpoint at the next piece in each string. - Running out of pieces in only one string means the counts differ.
- Mismatched letters mean the order changed, which is impossible.
- The direction checks enforce L-only-left and R-only-right.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Only blanks in both → True.
- Different counts of
L/R→ False. - An
Rthat would have to move left → False.
Complexity: O(n) time, O(1) space.
Medium 3302. Find the Lexicographically Smallest Valid Sequence Google Meta Amazon
Problem: Pick ascending indices in word1 whose characters spell a string that equals word2 with at most one character changed. Return the lexicographically smallest such index list, or [].
word1 = "vbcca", word2 = "abc"[0,1,2]word1 = "bacdc", word2 = "abc"[1,2,4]Constraints: 1 ≤ word2.length < word1.length ≤ 3 * 10⁵ · word1 and word2 consist only of lowercase English letters.
Hint 1
Plain greedy subsequence matching, but you may spend one mismatch. When is spending it right now safe?
Hint 2
Precompute, for every position of word1, how much of the end of word2 can still be matched exactly from there.
From brute force to the pattern: For every possible change position, re-checking whether the rest is a subsequence costs O(n) each: O(n · m). The suffix table answers "can the rest still match?" in O(1), so one greedy pass suffices.
Spot it: Matching word2 as a subsequence of word1 is a two-pointer walk. To decide greedily whether the one allowed mismatch can be used now, you need to know whether the rest of word2 can still be matched exactly afterwards, which a precomputed suffix table answers in O(1).
Why not another pattern? Trying every position for the single change and re-running a subsequence check is O(n · m). DP over (index, changed) can't easily recover the lexicographically smallest list. The suffix table makes the greedy choice safe and linear.
Key insight: suf[i] = how many characters at the end of word2 can be matched as a subsequence of word1[i:]. Scanning word1 from the left with pointer j into word2: take index i when word1[i] == word2[j], or, if the change is unused and suf[i + 1] ≥ m − j − 1, take i as the changed character. Taking the earliest possible index each time gives the smallest list.
Approach:
- Build
suffrom the right: walkword1backwards, matchingword2from its end greedily. - Walk
word1from the left withj= next character ofword2to place. - If
word1[i] == word2[j], takei. Else if the change is unused and the remainingm − j − 1characters fit inword1[i + 1:](suf[i + 1]), takeiand mark the change used. - Return the indices if all of
word2was placed, otherwise[].
class Solution:
def validSequence(self, word1: str, word2: str) -> List[int]:
n = len(word1)
m = len(word2)
# suf[i] = how many characters at the END of word2 match as a subsequence of word1[i:]
suf = [0] * (n + 1)
j = m - 1
for i in range(n - 1, -1, -1):
if j >= 0 and word1[i] == word2[j]:
j = j - 1 # matched one more character from the back
suf[i] = m - 1 - j
answer = []
changed = False # has the single allowed change been used?
j = 0 # next character of word2 to place
for i in range(n):
if j == m:
break # everything placed
if word1[i] == word2[j]:
answer.append(i) # exact match: always take the earliest index
j = j + 1
elif not changed and suf[i + 1] >= m - j - 1:
answer.append(i) # use the change here: the rest can still match exactly
changed = True
j = j + 1
return answer if j == m else []
What the code is doing, line by line:
- The first loop fills
suf: matchingword2backwards greedily from the end ofword1gives the longest matchable suffix for every start. suf[i] = m - 1 - j: after processingword1[i], charactersj + 1 .. m − 1ofword2are matched.- Second loop: exact matches are always taken immediately (the earliest index keeps the list smallest).
- A mismatch is taken as the change only if it's unused and the remaining
m − j − 1characters can still be matched exactly afteri; using it as early as possible also keeps the list smallest. answer if j == m else []: success only if all ofword2was placed.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- No valid sequence even with one change →
[]. - An exact subsequence exists, but changing an earlier character gives smaller indices.
word2one shorter thanword1.
Complexity: O(n) time, O(n) space for the suffix table.
Medium 3867. Sum of GCD of Formed Pairs Google Amazon
Problem: Build prefixGcd[i] = gcd(nums[i], max(nums[0..i])), sort it, then repeatedly pair the smallest and largest unpaired values (ignoring the middle one if n is odd). Return the sum of the GCDs of all pairs.
nums = [2,6,4]2nums = [3,6,2,8]5Constraints: 1 ≤ n == nums.length ≤ 10⁵ · 1 ≤ nums[i] ≤ 10⁹
Hint 1
Compute the prefix maximum as you go; then the problem is sorting and pairing.
Hint 2
Pair the two ends of the sorted list and move inward.
From brute force to the pattern: Recomputing max(nums[0..i]) for every i is O(n²); a running maximum makes it O(1) each. Pairing extremes is then a simple two-pointer walk after sorting.
Spot it: "Pair the smallest with the largest" on a sorted array is opposite-end two pointers moving inward.
Why not another pattern? There's no clever shortcut needed: the work is one pass for the prefix maxima, a sort, and an inward two-pointer pass.
Approach:
- Scan
numskeeping the running maximum; recordgcd(nums[i], running_max). - Sort these values.
left = 0,right = n − 1: whileleft < right, addgcd(values[left], values[right])and move both inward.
from math import gcd
class Solution:
def gcdSum(self, nums: List[int]) -> int:
prefix_gcd = []
running_max = 0
for x in nums:
running_max = max(running_max, x) # max(nums[0..i])
prefix_gcd.append(gcd(x, running_max))
prefix_gcd.sort()
total = 0
left = 0
right = len(prefix_gcd) - 1
while left < right:
total = total + gcd(prefix_gcd[left], prefix_gcd[right]) # smallest with largest
left = left + 1
right = right - 1
return total
What the code is doing, line by line:
running_maxis the maximum of the prefix, updated before computing the GCD (soxitself counts).prefix_gcd.sort(): pairing needs the values in order.while left < right: pairs the extremes and moves inward; with oddnthe middle element is left out automatically.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- A single element → 0 (nothing to pair).
- Odd length: the middle value is ignored.
- All equal values.
Complexity: O(n log n) for the sort (plus O(n log V) for the GCDs).
Hard 1163. Last Substring in Lexicographical Order Google
Problem: Return the lexicographically largest substring of s. (It's always a suffix: extending a substring never makes it smaller.)
s = "abab""bab"s = "leetcode""tcode"Constraints: 1 ≤ s.length ≤ 4 * 10⁵ · s contains only lowercase English letters.
Hint 1
Why is the answer always a suffix of s?
Hint 2
Compare two candidate suffixes character by character; when one loses after k matched characters, it can skip k + 1 positions at once.
From brute force to the pattern: max(s[i:] for i in range(n)) compares up to n suffixes of length up to n: O(n²), too slow for 4·10⁵. Keeping just two candidates and letting the loser skip everything it already matched makes the scan O(n).
Spot it: The answer is the largest suffix. Comparing suffixes naively is O(n²). Instead keep two candidate starts i < j and a match length k, and let the loser of each comparison jump forward: three pointers walking the string once.
Why not another pattern? Sorting all suffixes (or building a suffix array) works but is O(n log n) or more and far more code. Taking the maximum of s[i:] over all i builds n substrings: O(n²) time.
Key insight: If s[i:i+k] == s[j:j+k] and then s[i+k] < s[j+k], every start in i..i+k loses to the matching start in j..j+k, so i can jump to max(i + k + 1, j). Symmetrically, if s[i+k] > s[j+k], j jumps to j + k + 1. Each jump skips k + 1 positions, so the whole scan is linear.
Approach:
i = 0is the best start so far,j = 1the challenger,k = 0the matched length.- If
s[i + k] == s[j + k], extend the match:k += 1. - If
s[i + k] > s[j + k]: the challenger loses;j = j + k + 1,k = 0. - If
s[i + k] < s[j + k]: the best loses;i = max(i + k + 1, j),j = i + 1,k = 0. - Stop when
j + kreaches the end; the answer iss[i:].
class Solution:
def lastSubstring(self, s: str) -> str:
n = len(s)
i = 0 # best starting index so far
j = 1 # challenger starting index
k = 0 # how many characters of s[i:] and s[j:] matched
while j + k < n:
if s[i + k] == s[j + k]:
k = k + 1 # still tied: compare the next character
elif s[i + k] > s[j + k]:
j = j + k + 1 # challenger loses, and so do the starts it just matched
k = 0
else:
i = max(i + k + 1, j) # best loses: skip past everything it matched
j = i + 1
k = 0
return s[i:]
What the code is doing, line by line:
iholds the best start found so far andjthe one being tested;kcounts how far they agree.- Equal characters: keep comparing (
k += 1). s[i + k] > s[j + k]:s[j:]is smaller, and so is everys[j + t:]fort ≤ k(each loses tos[i + t:]), sojjumps byk + 1.s[i + k] < s[j + k]: the same argument in reverse;imoves tomax(i + k + 1, j)and a new challenger starts right after it.return s[i:]: the winning suffix.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- All characters equal, e.g.
"aaaa"→ the whole string. - The largest letter appears once, e.g.
"leetcode"→"tcode". - Repeating patterns, e.g.
"abab"→"bab".
Complexity: O(n) time (every jump skips the characters just compared), O(1) extra space besides the answer.
Medium 277. Find the Celebrity Amazon
Problem: A celebrity is known by everyone and knows nobody. Using knows(a, b), find them (or −1) with few calls.
Solve it on LeetCode ↗ (Premium)
graph = [[1,1,0],[0,1,0],[1,1,1]]1graph = [[1,0,1],[1,1,0],[0,1,1]]-1Constraints: n == graph.length == graph[i].length · 2 ≤ n ≤ 100 · graph[i][j] is 0 or 1 · graph[i][i] == 1
Hint 1
What can you conclude from a single knows(a, b) answer?
Hint 2
Eliminate one person per call, then verify the last one.
From brute force to the pattern: Checking all pairs is O(n²) calls; elimination is O(n).
Spot it: Each knows(a, b) call rules someone out: if a knows b, a isn't the celebrity; otherwise b isn't. A two-pointer elimination leaves one candidate in n − 1 calls; then verify it.
Why not another pattern? Checking every pair is O(n²) calls.
Approach:
cand = 0; foriin1..n−1: ifknows(cand, i),cand = i.- Verify: for every
i ≠ cand,knows(cand, i)must be false andknows(i, cand)true.
class Solution:
def findCelebrity(self, n: int) -> int:
cand = 0
for i in range(1, n):
if knows(cand, i):
cand = i # cand knows someone, so cand can't be the celebrity
for i in range(n):
if i != cand and (knows(cand, i) or not knows(i, cand)):
return -1 # the survivor fails the definition
return cand
What the code is doing, line by line:
- If
candknowsi,candis out; if not,iis out (the celebrity is known by everyone, includingcand). - Only one candidate survives, and it still has to be checked.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- No celebrity → −1.
- The celebrity is person 0.
- n = 2.
Complexity: O(n) time and at most 3n calls to knows, O(1) space.
Medium 969. Pancake Sorting Amazon
Problem: Sort a permutation of 1..n using prefix reversals (flip the first k elements). Return any sequence of ks with at most 10n flips.
arr = [3,2,4,1][4,2,4,3]arr = [1,2,3][]Constraints: 1 ≤ arr.length ≤ 100 · 1 ≤ arr[i] ≤ arr.length · All integers in arr are unique (i.e. arr is a permutation of the integers from 1 to arr.length).
Hint 1
Put the largest pancake at the bottom first.
Hint 2
How do you move any pancake to the top with one flip?
From brute force to the pattern: Searching for the minimum flips is exponential; two flips per value is O(n²) and valid.
Spot it: Place values from largest to smallest: each takes two flips (to the front, then to the back of the unsorted part) — a two-pointer reversal pattern.
Why not another pattern? Searching for the fewest flips is NP-hard; the problem only asks for any sequence within 10n.
Approach:
- For
sizefromndown to 2: find the indexiofsize. - Flip
i + 1(it goes to the front), then flipsize(it goes to positionsize − 1).
class Solution:
def pancakeSort(self, arr: List[int]) -> List[int]:
flips = []
for size in range(len(arr), 1, -1):
i = arr.index(size) # where the largest unsorted pancake is
if i == size - 1:
continue # already in place
if i > 0:
arr[:i + 1] = arr[:i + 1][::-1] # bring it to the top
flips.append(i + 1)
arr[:size] = arr[:size][::-1] # flip it down into place
flips.append(size)
return flips
What the code is doing, line by line:
- After processing
size, the lastn − size + 1positions hold their final values. - At most two flips per value:
2n ≤ 10n.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Already sorted → [].
- Reversed array.
- n = 1.
Complexity: O(n²) time, O(n) space for the answer.
Medium 1574. Shortest Subarray to be Removed to Make Array Sorted Amazon
Problem: Remove one subarray (possibly empty) so the rest is non-decreasing. Return the shortest such removal.
arr = [1,2,3,10,4,2,3,5]3arr = [5,4,3,2,1]4Constraints: 1 ≤ arr.length ≤ 10⁵ · 0 ≤ arr[i] ≤ 10⁹
Hint 1
What does the array look like after the removal?
Hint 2
Fix the sorted suffix, then extend the prefix with a second pointer.
From brute force to the pattern: Every removal + check is O(n³); binary search on the suffix is O(n log n); two pointers is O(n).
Spot it: What remains is a sorted prefix plus a sorted suffix. Find the longest sorted suffix starting at r; then for each prefix end l, move r forward until arr[r] ≥ arr[l] — two pointers, both only moving right.
Why not another pattern? Trying every removal and checking sortedness is O(n³).
Approach:
r= start of the longest non-decreasing suffix. Ifr == 0, return 0.- Answer starts as
r(remove the whole prefix). - For
l= 0, 1, … while the prefix stays sorted: advancerwhilearr[r] < arr[l]; candidater − l − 1.
class Solution:
def findLengthOfShortestSubarray(self, arr: List[int]) -> int:
n = len(arr)
r = n - 1
while r > 0 and arr[r - 1] <= arr[r]:
r -= 1 # longest sorted suffix starts at r
if r == 0:
return 0 # already sorted
best = r # remove everything before the suffix
for l in range(n):
if l > 0 and arr[l] < arr[l - 1]:
break # the prefix is no longer sorted
while r < n and arr[r] < arr[l]:
r += 1 # suffix must start at a value >= arr[l]
best = min(best, r - l - 1) # remove arr[l+1 .. r-1]
return best
What the code is doing, line by line:
- The kept prefix must be sorted and the kept suffix must be sorted, and the junction must not decrease.
- As
lgrows,arr[l]grows, so the neededrnever moves back.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Already sorted → 0.
- Strictly decreasing → n − 1.
- Equal values.
Complexity: O(n) time, O(1) space.
Medium 1679. Max Number of K-Sum Pairs Amazon
Problem: Repeatedly remove two numbers summing to k. Return the maximum number of removals.
nums = [1,2,3,4], k = 52nums = [3,1,3,4,3], k = 61Constraints: 1 ≤ nums.length ≤ 10⁵ · 1 ≤ nums[i] ≤ 10⁹ · 1 ≤ k ≤ 10⁹
Hint 1
Sort the array.
Hint 2
Use two pointers from both ends, as in Two Sum II.
From brute force to the pattern: Nested loops are O(n²); sort + two pointers is O(n log n).
Spot it: Pairs with a fixed sum → sort + two pointers from both ends (or a hash map of unmatched values).
Why not another pattern? Greedy pairing in arbitrary order with nested loops is O(n²).
Approach:
- Sort.
l,rat the ends: equal tok→ count, move both; less →l += 1; more →r −= 1.
class Solution:
def maxOperations(self, nums: List[int], k: int) -> int:
nums = sorted(nums)
l, r = 0, len(nums) - 1
pairs = 0
while l < r:
s = nums[l] + nums[r]
if s == k:
pairs += 1 # use both
l += 1
r -= 1
elif s < k:
l += 1 # need a bigger sum
else:
r -= 1 # need a smaller sum
return pairs
What the code is doing, line by line:
- In a sorted array, if
nums[l] + nums[r] < k,nums[l]can't pair with anything left, so it's discarded. - Pairing a matching couple never hurts the maximum (exchange argument).
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Duplicates (k = 2x).
- No pairs → 0.
- All pairs.
Complexity: O(n log n) time, O(n) space (or O(n) time with a hash map).
Medium 2161. Partition Array According to Given Pivot Amazon Microsoft
Problem: Rearrange so values < pivot come first, then == pivot, then > pivot, keeping the relative order inside each group.
nums = [9,12,5,10,14,3,10], pivot = 10[9,5,3,10,10,12,14]nums = [-3,4,3,2], pivot = 2[-3,2,4,3]Constraints: 1 ≤ nums.length ≤ 10⁵ · -10⁶ ≤ nums[i] ≤ 10⁶ · pivot equals to an element of nums.
Hint 1
Why doesn't the in-place Dutch flag algorithm work here?
Hint 2
Write the small values from the front and the large ones from the back.
From brute force to the pattern: Three separate list passes are also O(n); the two-pointer version does it in one.
Spot it: A three-way partition, but stable. In-place swapping (Dutch flag) breaks the order, so fill the result with two pointers: smaller values from the front, larger values from the back (reading the array backwards), pivots in between.
Why not another pattern? The Dutch national flag swap scheme is O(1) space but not stable.
Approach:
lowrites smaller values from the left (scan forward);hiwrites larger values from the right (scan backward).- Fill the gap between them with
pivot.
class Solution:
def pivotArray(self, nums: List[int], pivot: int) -> List[int]:
n = len(nums)
result = [pivot] * n # anything not overwritten is the pivot
lo, hi = 0, n - 1
for i in range(n):
if nums[i] < pivot:
result[lo] = nums[i] # smaller values, in order, from the left
lo += 1
if nums[n - 1 - i] > pivot:
result[hi] = nums[n - 1 - i] # larger values, scanned backwards, from the right
hi -= 1
return result
What the code is doing, line by line:
- Scanning backwards while writing backwards keeps the larger values in their original order.
- Pre-filling with
pivothandles the middle group automatically.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Pivot appears many times.
- All smaller or all larger.
- Negative numbers.
Complexity: O(n) time, O(n) space for the result.
Medium 2938. Separate Black and White Balls Amazon
Problem: Group all 1s to the right and 0s to the left using adjacent swaps. Return the minimum number of swaps.
s = "101"1s = "100"2Constraints: 1 ≤ n == s.length ≤ 10⁵ · s[i] is either '0' or '1'.
Hint 1
What does one adjacent swap of a 1 and a 0 achieve?
Hint 2
Count inversions: pairs (1 before 0).
From brute force to the pattern: Bubble-sort simulation is O(n²); counting is O(n).
Spot it: Each adjacent swap moves one 0 past one 1. So the answer is the number of (1, 0) pairs in order — count 1s seen so far while scanning (a two-pointer / counting idea: the next 0 goes to the next free left slot).
Why not another pattern? Simulating swaps (bubble sort) is O(n²).
Approach:
ones = 0; for each character:1→ones += 1;0→swaps += ones.
class Solution:
def minimumSteps(self, s: str) -> int:
swaps = 0
ones = 0 # black balls to the left of the current position
for ch in s:
if ch == "1":
ones += 1
else:
swaps += ones # this white ball must pass every black ball before it
return swaps
What the code is doing, line by line:
- Every out-of-order pair needs exactly one swap, and each swap fixes exactly one pair.
- The answer can exceed 32 bits (n up to 10⁵).
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Already separated → 0.
- All ones or all zeros.
- Alternating pattern.
Complexity: O(n) time, O(1) space.
Medium 3752. Lexicographically Smallest Negated Permutation that Sums to Target Amazon
Problem: Build the lexicographically smallest array whose absolute values are a permutation of 1..n and whose sum is target, or [].
n = 3, target = 0[-3,1,2]n = 1, target = 10000000000[]Constraints: 1 ≤ n ≤ 10⁵ · -10¹⁰ ≤ target ≤ 10¹⁰
Hint 1
How does negating some numbers change the total?
Hint 2
Which numbers should be negated to make the array start as small as possible?
From brute force to the pattern: All 2ⁿ sign choices vs a greedy pick from the top.
Spot it: Starting from 1 + … + n = S, negating a set N changes the sum to S − 2 · sum(N), so sum(N) = (S − target) / 2. To be lexicographically smallest, the array should start with the most negative numbers: greedily put the largest values into N, then sort.
Why not another pattern? Trying all 2ⁿ sign patterns is exponential.
Approach:
S = n(n + 1)/2. IfS − targetis odd or outside[0, 2S], return[].T = (S − target)/2; forv = n … 1: ifv ≤ T, negate it andT −= v.- Return the values sorted ascending.
class Solution:
def lexSmallestNegatedPerm(self, n: int, target: int) -> List[int]:
total = n * (n + 1) // 2
if (total - target) % 2 or not (-total <= target <= total):
return [] # the negated part would need a non-integer or impossible sum
need = (total - target) // 2 # sum of the values we negate
values = []
for v in range(n, 0, -1):
if v <= need:
values.append(-v) # negate the biggest values first
need -= v
else:
values.append(v)
return sorted(values)
What the code is doing, line by line:
- Any
Tbetween 0 andSis a subset sum of1..n, and taking the largest value that fits always leaves a reachable remainder. - Sorted ascending, the array starts with
−(largest negated); making that as large as possible makes the first element as small as possible, and the same for the next ones.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Target out of range → [].
- Wrong parity → [].
- Target equals the full sum (nothing negated).
Complexity: O(n log n) time (O(n) without the final sort, since the order is known), O(n) space.
Medium 809. Expressive Words Cisco
Problem: A word is stretchy if extending some of its letter groups to length ≥ 3 makes it equal to s. Count stretchy words.
s = "heeellooo", words = ["hello", "hi", "helo"]1s = "zzzzzyyyyy", words = ["zzyy","zy","zyy"]3Constraints: 1 ≤ s.length, words.length ≤ 100 · 1 ≤ words[i].length ≤ 100 · s and words[i] consist of lowercase letters.
Hint 1
Compare the strings group by group.
Hint 2
When is it allowed for s's group to be longer than the word's?
From brute force to the pattern: Trying stretch combinations is exponential; comparing runs is linear.
Spot it: Walk s and a word with two pointers, run by run. Runs must have the same letter, and the s run must equal the word's run or be at least 3 and not shorter.
Why not another pattern? Generating all stretched versions of a word is exponential.
Approach:
runs(x)= list of (letter, length).- A word matches if it has the same letters in order and for each run:
ls == lwor (ls ≥ 3andls ≥ lw).
from itertools import groupby
class Solution:
def expressiveWords(self, s: str, words: List[str]) -> int:
def runs(x):
return [(ch, len(list(g))) for ch, g in groupby(x)]
target = runs(s)
def stretchy(word):
r = runs(word)
if len(r) != len(target):
return False
for (c1, n1), (c2, n2) in zip(target, r): # walk both run lists together
if c1 != c2 or n1 < n2 or (n1 != n2 and n1 < 3):
return False # different letter, too short, or an extension shorter than 3
return True
return sum(stretchy(w) for w in words)
What the code is doing, line by line:
- Run-length encoding turns the two-pointer walk into comparing pairs.
- A group can only be extended to a total of 3 or more, never shrunk.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Group of length 2 in s (can't come from length 1).
- Equal strings (stretchy).
- Different letters.
Complexity: O(len(s) + Σ word lengths) time, O(len(s)) space.