DSA sheet · Arrays · Two Pointers pattern

3Sum

The second two-pointer problem, right after Two Sum II. Now we need three numbers that add to 0, and we must return every such triplet, without repeats. The teacher first writes the obvious three nested loops (O(n³)) and shows from the constraints that it's far too slow. Then she turns three pointers into "one fixed pointer + the Two Sum II two pointers" after sorting the array. Her first version works but returns duplicate triplets, so she shows exactly where the duplicates come from and adds the skipping rules one by one.

Why it matters: "fix one element, then two pointers on the rest" turns a k-sum into a (k−1)-sum, and the duplicate-skipping rules here are reused in 4Sum and many "unique combinations" problems.

Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Before starting

What is a "pointer"?

A pointer here is just a variable holding an index of the array. "Moving" it means adding or subtracting 1. Two-pointer solutions keep a few such indexes and move them by rules, instead of using nested loops that try every combination.

The brute force over all pairs, and why sorting lets us skip pairs

For "find two numbers with sum X", trying every pair costs about n²/2 checks. If the array is sorted, we can do it in one pass: left at the start, right at the end.

Each move throws away a whole row of pairs that we've proven can't work. That's why the scan is safe and why it's O(n). This is the opposite-end shape, from Two Sum II (Problem 1).

sorted−4−1035want 1: −4 + 5 = 1 ✓ found
L   R

Fixing one element: from 3 numbers to 2

3Sum asks for three numbers a + b + c = 0. If we fix a (with a normal loop), then we need b + c = −a. That's exactly "find a pair with a given sum" in the part of the array after a, so we can run the opposite-end two pointers there. One loop × one O(n) scan = O(n²) instead of O(n³).

Skipping duplicates, the idea

After sorting, equal values sit next to each other. So "have I already used this value in this position?" becomes a simple check: "is this the same as its neighbour?" If yes, skip it. Part C shows exactly where.

The other two-pointer shapes on this sheet

Not needed for 3Sum, but good to recognise.

shapehow the pointers moveexample
Same direction (read/write, slow/fast)both start at the left; the read pointer visits everything, the write pointer moves only when something is keptMove Zeroes
Three pointers (low/mid/high)mid scans; small values are swapped to the low zone, big ones to the high zoneSort Colors
Opposite ends + left-max/right-maxtrack the tallest bar from each side, move the side with the smaller maxTrapping Rain Water
Ends of a string / expand around a centercompare the outer characters moving inward, or grow outward from a middle (odd and even centers)Valid Palindrome, Longest Palindromic Substring

The teacher's 4 array patterns

patternwhen she uses it3Sum?
Two pointerssorted array, a few particular positions combined (sum, compare)yes (after sorting)
Sliding windowa continuous subarray (e.g. 2, 3, 4, 5 in a row) for max / min / sum / averageno: the three numbers can be anywhere
Prefix sumrepeated sums of continuous subarrays of different lengthsno
Kadane'sbest continuous sum when negatives existno: not a continuous sum

Part A · Brute force: three nested loops

LeetCode 15

1The question in simple words

You get an integer array nums. Return all triplets [nums[i], nums[j], nums[k]] such that:

The answer must not contain duplicate triplets. Two triplets are duplicates if they have the same three values, in any order. For example [−1, 0, 1] and [0, −1, 1] are the same triplet: written in sorted order, both are [−1, 0, 1]. Keep one.

nums−1012−1−4answer: [[−1, −1, 2], [−1, 0, 1]]

Note: [−1, 0, 1] can be made with the −1 at index 0 or the −1 at index 4. Different indexes, same values → still only one triplet in the answer. The order of the triplets in the output doesn't matter.

2What the constraints tell us

3Intuition: try every group of three

Three fingers: i, j, k, always in order i < j < k. Start them at 0, 1, 2. Move k to the end, checking every sum. Then move j one step and run k again from just after j. When j runs out, move i. This visits every group of three cells exactly once.

4Building the logic

Her first draft, and the fix

She first writes all three loops starting from 0, then corrects it: each loop must start one after the previous pointer, so that the indexes are different and no group is checked twice.

pointerfirst indexlast indexPython
i0n − 3range(n - 2)
ji + 1n − 2range(i + 1, n - 1)
kj + 1n − 1range(j + 1, n)

Inside: if nums[i] + nums[j] + nums[k] == 0, save the triplet.

Doubt: the teacher's brute force just adds every matching triplet to the result. Is that enough?
→ No, it would return duplicates. In [−1, 0, 1, 2, −1, −4], both (index 0, 1, 2) and (index 1, 2, 4) give the values −1, 0, 1. Her version would list [−1, 0, 1] twice. The fix for a brute force: sort the array first (so every triplet comes out in sorted order, and [0, −1, 1] can only appear as [−1, 0, 1]), and collect triplets in a set of tuples, which keeps only one copy. That's what the code below does. (She mentions the set idea later, in Part C, as the "easy but less efficient" way.)

5Approach steps

  1. Sort nums.
  2. For every i < j < k: if the three values add to 0, add the tuple to a set.
  3. Return the set as a list of lists.

6Code (Python)

Brute force: O(n³), TLE for n = 3000
class Solution:
    def threeSum(self, nums):
        nums.sort()                          # so every triplet comes out in sorted order
        n = len(nums)
        found = set()                        # a set keeps one copy of each triplet
        for i in range(n - 2):
            for j in range(i + 1, n - 1):
                for k in range(j + 1, n):
                    if nums[i] + nums[j] + nums[k] == 0:
                        found.add((nums[i], nums[j], nums[k]))
        return [list(t) for t in found]

7Code line by line

linewhat it means
nums.sort()After sorting, i < j < k means nums[i] ≤ nums[j] ≤ nums[k], so each triplet is written smallest-first. Two triplets with the same values become identical tuples.
found = set()Adding a tuple that's already in the set does nothing, so duplicates disappear.
for i in range(n - 2):i stops at the third-last index, leaving room for j and k.
for j in range(i + 1, n - 1):j always after i, stops at the second-last index.
for k in range(j + 1, n):k always after j, up to the last index.
if nums[i] + nums[j] + nums[k] == 0:Is this group of three a valid triplet?
return [list(t) for t in found]LeetCode wants a list of lists, so convert each tuple.

8Dry run (hand table)

nums = [−1, 0, 1, 2, −1, −4] → sorted [−4, −1, −1, 0, 1, 2]. Only the groups that add to 0 are shown (there are 20 groups in total).

i, j, kvaluessumset after
1, 2, 5−1, −1, 20{(−1,−1,2)}
1, 3, 4−1, 0, 10{(−1,−1,2), (−1,0,1)}
2, 3, 4−1, 0, 10same set: duplicate ignored

Answer: [[−1, −1, 2], [−1, 0, 1]] ✓. Without the set, [−1, 0, 1] would appear twice.

9Complexity & remember

Remember the brute forcei from 0, j from i + 1, k from j + 1. Ends: i < n − 2, j < n − 1, k < n. Correct only with a sort + set for duplicates. O(n³) → too slow.

Part B · Sort, fix i, two pointers on the rest

first version: fast, but it still returns duplicates

1The question again, with the new goal

Same question. Goal: O(n²). Idea: keep the outer i loop, but replace the two inner loops (j and k) with one two-pointer scan.

2Choosing the pattern (her reasoning)

3Intuition: Two Sum II inside a loop

Fix nums[i]. Now we need two numbers after it that add to −nums[i]. Put j (we'll call it left) right after i, and k (right) at the last index. Compute the total of all three:

Stop when left meets right. Then the for loop moves i forward and we do it again.

4Building the logic from examples

The teacher builds her own sorted array: three −1s, three 0s, one 1, two 2s and a 3.

index0123456789
nums−1−1−10001223i = 0, left = 1, right = 9
IL       R

Too big → move right

−1 + (−1) + 3 = 1, but we want 0. It's too big, so we need to add a smaller number. Would moving left forward give something smaller? No: moving forward in a sorted array gives bigger-or-equal values. Moving right back gives smaller-or-equal values. So right -= 1.

Too small → move left

If the total is negative, we need a bigger number, so left += 1.

Never touch i inside the while

The teacher stresses this: inside the scan we only play with left and right. i moves only through the for loop.

Why sorting makes these moves safe (the full reason)

Fix i. The pairs still "alive" are all (l, r) with left ≤ l < r ≤ right. Suppose the total is too big: nums[i] + nums[left] + nums[right] > 0. Take any alive pair that uses right: its other index l is ≥ left, so nums[l] ≥ nums[left] (sorted). Then:

nums[i] + nums[l] + nums[right] ≥ nums[i] + nums[left] + nums[right] > 0

So every remaining triplet that uses right is too big. None of them can be an answer. Dropping right loses nothing. The "too small" case is the mirror: every remaining partner of left is ≤ nums[right], so every triplet using left is too small, and dropping left loses nothing.

Without sorting, none of this holds: moving right back could give a bigger number, and we'd have no idea which pointer to move.

Doubt 1: we only look to the right of i. Could we miss a triplet that uses something before i?
→ No. Any triplet uses three indexes p < q < s. It gets checked when the for loop is at i = p (its smallest index), with left and right searching q and s. So every triplet has its turn exactly when its first element is fixed.
Doubt 2: after finding a triplet, why move both left and right?
→ If we moved only left to a bigger value but kept the same right value, the total would be ≥ 0 again, and it can only be 0 if the new left value is the same as the old one (a repeat). Moving both is safe and saves a step.

The bug: duplicates appear

Run this first version on her array and watch i:

In total this version returns 7 triplets, but only 3 are different. Part C fixes it.

5Approach steps

  1. Sort nums. Let n = len(nums).
  2. For i from 0 to n − 3: left = i + 1, right = n − 1.
  3. While left < right: total = nums[i] + nums[left] + nums[right].
  4. total == 0 → save, then move both pointers. total > 0 → right −= 1. total < 0 → left += 1.

6Code (Python)

First version: O(n²), but returns duplicate triplets
class Solution:
    def threeSum(self, nums):
        nums.sort()
        n = len(nums)
        result = []
        for i in range(n - 2):               # leave room for left and right
            left, right = i + 1, n - 1
            while left < right:
                total = nums[i] + nums[left] + nums[right]
                if total == 0:
                    result.append([nums[i], nums[left], nums[right]])
                    left += 1
                    right -= 1
                elif total > 0:
                    right -= 1               # too big: take a smaller number
                else:
                    left += 1                # too small: take a bigger number
        return result

7Code line by line

linewhat it means
nums.sort()Needed for the pointer moves to be safe (step 4).
for i in range(n - 2):The fixed first number. Stops at the third-last index, since left and right need two cells after it.
left, right = i + 1, n - 1The Two Sum II start: just after i, and at the very end.
while left < right:They must be two different cells. When they meet, this i is done.
total = nums[i] + nums[left] + nums[right]Sum of the current triplet.
if total == 0: … append …Save it, then move both (Doubt 2).
elif total > 0: right -= 1Every triplet using right is too big. Drop right.
else: left += 1Every triplet using left is too small. Drop left.

8Dry run: where the duplicates come from

Her array [−1, −1, −1, 0, 0, 0, 1, 2, 2, 3]. Only the found triplets are listed.

i (value)left, rightvaluesresult after
0 (−1)1, 8−1, −1, 2[−1,−1,2]
0 (−1)2, 7−1, −1, 2+ [−1,−1,2] again (left and right moved onto copies)
0 (−1)3, 6−1, 0, 1+ [−1,0,1]
1 (−1)2, 8−1, −1, 2+ [−1,−1,2] a third time (i is a copy)
1 (−1)3, 6−1, 0, 1+ [−1,0,1] again
2 (−1)3, 6−1, 0, 1+ [−1,0,1] again
3 (0)4, 50, 0, 0+ [0,0,0]

Two sources of repeats: i landing on a value it already used (rows 4–6), and left/right stepping onto a copy of the value they just used (row 2).

9Complexity & remember

Remember the coreSort. For each i: left = i + 1, right = n − 1. Too big → right −= 1, too small → left += 1, zero → save and move both. Fast, but on its own it repeats triplets.

Part C · Skipping duplicates (the final solution)

LeetCode 15 · the code she submits

1What stays the same, what changes

Same question, same constraints, same sort + fix i + two pointers. We only add two things: a skip for i, and a skip for left and right right after a triplet is found.

2Why not just use a set?

The teacher mentions it: you could store triplets in a set and duplicates would vanish. It works, but it still does the repeated work (finding the same triplet again and again), and the set costs extra memory. Skipping is better: we never even produce a duplicate.

3Intuition: once a value is used in a position, don't use it there again

Because the array is sorted, all copies of a value are neighbours. So:

4Building each skip, exactly where it goes

Skip 1: at the top of the for loop, for i

Her explanation: with the first −1 as i, left and right already found every triplet that starts with −1. If the next i is also −1, it can only find the same triplets again (its left/right range is even smaller). So:

Skip 1if i > 0 and nums[i] == nums[i - 1]: continue
"This value was already the fixed number last round" → skip this i.

On her array: i = 0 is −1 → used. i = 1 is −1, same as nums[0] → skip. i = 2 is −1 → skip. i = 3 is 0, different from nums[2] → use it.

Doubt 1: why compare with the previous one (i − 1) and not the next one (i + 1)?
→ Comparing with the previous one means we use the first copy and skip the later ones. The first copy has the most room after it, including the other copies, so it can find [−1, −1, 2]. If we compared with the next one, we'd skip the first copies and use the last −1. Then left starts after all the −1s, and [−1, −1, 2] is lost.
Doubt 2: why the i > 0 part?
→ At i = 0 there is no previous element. In Java, nums[-1] would crash. In Python it's worse: nums[-1] is the last element, with no error. For [0, 0, 0], nums[0] == nums[-1] is True, so i = 0 would be skipped wrongly, then i = 1 is a copy… and [0, 0, 0] would be missed. The i > 0 check protects us.

Skip 2: inside if total == 0, for left and right

On her array, i = 0, the triplet [−1, −1, 2] is at left = 1, right = 8. If we just did left += 1, right −= 1, we'd land on another −1 and another 2 and save the same triplet again. So after saving, walk left forward while the next value is the same, and walk right back while the previous value is the same:

the skip, placed right after result.append(...)
while left < right and nums[left] == nums[left + 1]:
    left += 1        # walk to the LAST copy of this left value
while left < right and nums[right] == nums[right - 1]:
    right -= 1       # walk to the FIRST copy of this right value
left += 1            # one more step: off the copies, onto a new value
right -= 1

Her walk-through: left is on the −1 at index 1. Is nums[2] also −1? Yes → move left to 2. Is nums[3] also −1? No, it's 0 → stop. Left now sits on the last −1, so one more left += 1 puts it on the 0. Right is on the 2 at index 8. Is nums[7] also 2? Yes → move right to 7. Is nums[6] 2? No, it's 1 → stop, and one more right -= 1 puts it on the 1.

Doubt 3: why the extra left += 1 and right -= 1 after the while loops?
→ The while loop checks the next cell and stops when the next cell is different. So it stops on the last copy, not after it. The extra step moves off it. Without the extra step, left and right would still be on −1 and 2, and we'd find [−1, −1, 2] again.
Doubt 4: why left < right in the skip loops too?
→ Two reasons. It keeps nums[left + 1] inside the array (left + 1 ≤ right). And it stops the pointers from walking past each other, as in [0, 0, 0, 0] where everything is a copy.
Doubt 5: can skipping the copies of left lose a different triplet?
→ No. With i fixed and the left value fixed, the third value is forced: it must be −(nums[i] + nums[left]). So any copy of the left value can only make the same triplet. Same argument for right. The copies have nothing new to offer.
Doubt 6: in her board explanation (i on the first 0), she also skips the repeated 2s while the sum is too big. Her code doesn't do that. Which is right?
→ Both give the same answer. When the sum is not 0, we move one step at a time; landing on a copy of 2 just gives the same "too big" result and we step again. No triplet is saved, so no duplicate can appear. The skipping is only needed after a match, which is exactly where her code puts it.

5Approach steps

  1. Sort nums, result = [].
  2. For i in 0 … n − 3: if i > 0 and nums[i] == nums[i − 1] → continue (Skip 1).
  3. left = i + 1, right = n − 1. While left < right: compute total.
  4. total == 0: save; skip left copies; skip right copies; left += 1; right −= 1 (Skip 2).
  5. total > 0: right −= 1. total < 0: left += 1.
  6. Return result.

6Code (Python)

Final: sort + fix i + two pointers + duplicate skipping, O(n²)
class Solution:
    def threeSum(self, nums):
        nums.sort()
        n = len(nums)
        result = []
        for i in range(n - 2):
            if i > 0 and nums[i] == nums[i - 1]:    # Skip 1: same fixed value as last time
                continue
            left, right = i + 1, n - 1
            while left < right:
                total = nums[i] + nums[left] + nums[right]
                if total == 0:
                    result.append([nums[i], nums[left], nums[right]])
                    # Skip 2: walk past copies of the values we just used
                    while left < right and nums[left] == nums[left + 1]:
                        left += 1
                    while left < right and nums[right] == nums[right - 1]:
                        right -= 1
                    left += 1
                    right -= 1
                elif total > 0:
                    right -= 1
                else:
                    left += 1
        return result

7Code line by line (only the new lines)

linewhat it means
if i > 0 and nums[i] == nums[i - 1]: continueThis fixed value was already fully explored by the previous i. Jump to the next i. i > 0 avoids comparing index 0 with nums[−1].
while left < right and nums[left] == nums[left + 1]: left += 1Move left to the last copy of the value it just used.
while left < right and nums[right] == nums[right - 1]: right -= 1Move right to the first copy (from the right side) of its value.
left += 1 right -= 1Step off the copies, onto new values. Needed even when there were no copies, to continue the scan.
elif / elseUnchanged from Part B: one step, no skipping needed.

8Dry run

Her array [−1, −1, −1, 0, 0, 0, 1, 2, 2, 3] (indexes 0–9).

stepileftrightvalues at i, left, righttotaldecision (which pointer moves and why)result after
1019−1, −1, 31> 0 → right −= 1[]
2018−1, −1, 20save. left skips copy at 2, then +1 → 3. right skips copy at 7, then −1 → 6[[−1,−1,2]]
3036−1, 0, 10save. left walks over the copies of 0 at 4 and 5 (stops on 5, since nums[6] is 1), then +1 → 6. right has no copy (nums[5] is 0), −1 → 5. 6 > 5 → scan ends+ [−1,0,1]
–1nums[1] == nums[0] (−1) → Skip 1, continuesame
–2nums[2] == nums[1] (−1) → Skip 1, continuesame
43490, 0, 33> 0 → right −= 1same
53480, 0, 22> 0 → right −= 1same
63470, 0, 22> 0 → right −= 1 (a copy of 2, just stepped over)same
73460, 0, 11> 0 → right −= 1same
83450, 0, 00save. left 5, right 4 → crossed, scan ends+ [0,0,0]
–4, 5copies of 0 → Skip 1same
9–10679 → 81, 2, 3 then 1, 2, 26, 5> 0 both times → right −= 1, then they meetsame
117892, 2, 37> 0 → right −= 1, they meet. Loop ends (i stops at 7)same

Final answer: [[−1, −1, 2], [−1, 0, 1], [0, 0, 0]] ✓, three triplets and no repeats (Part B gave 7).

The array at the key moments. Yellow = the three pointer cells, grey = cells already ruled out for this i.

step 2−1−1−10001223total 0 → save [−1, −1, 2]
IL      R 
after skip 2−1−1−10001223both copies stepped over → new values 0 and 1
I  L  R   
step 8−1−1−10001223i = 3 (i = 1, 2 were skipped) → save [0, 0, 0]
   ILR    

LeetCode's example [−1, 0, 1, 2, −1, −4] sorts to [−4, −1, −1, 0, 1, 2]. i = 0 (−4): totals −3, −3, −2, −1 → left moves every time, nothing found. i = 1 (−1): finds [−1, −1, 2], then [−1, 0, 1]. i = 2 (−1): Skip 1. i = 3 (0): 0 + 1 + 2 = 3 → no. Answer [[−1, −1, 2], [−1, 0, 1]] ✓.

9Complexity & remember

She submitted the code (Java on screen, with Python and C++ versions of the same logic shown after) and it passed.

Remember the duplicate skips Skip 1, top of the for: i > 0 and nums[i] == nums[i−1] → continue.
Skip 2, only after a match: walk left while it equals the next, walk right while it equals the previous, then one more step each.

Part D · Revision page

Brute forceTwo pointers, no skipsTwo pointers + skips (final)
idea3 nested loopssort, fix i, Two Sum II on the restsame + skip repeated values
duplicates?yes, unless sort + setyesnever produced
timeO(n³), TLEO(n²)O(n²)
extra spaceset of tripletsO(1)O(1)
whereskip rulewhy
start of each ii > 0 and nums[i] == nums[i−1] → continuethat value was already the fixed number; use only the first copy
after a match, leftwhile equal to next → left += 1; then left += 1same left value → forced same third value → same triplet
after a match, rightwhile equal to previous → right −= 1; then right −= 1same reason
when total ≠ 0no skip, just one stepnothing is saved, so no duplicate can appear
If you remember only 5 lines 1. Sort first: it makes the pointer moves safe and puts duplicates next to each other.
2. Fix i, then left = i + 1, right = n − 1 (Two Sum II for −nums[i]).
3. Total > 0 → right −= 1. Total < 0 → left += 1.
4. Skip i if it equals the previous i.
5. After a match, skip copies on both sides, then step both pointers.
Mistakes to avoid ✗ forgetting to sort
✗ comparing i with nums[i+1] (loses [−1, −1, 2])
✗ dropping i > 0 (Python's nums[−1] wraps to the last element)
✗ forgetting the extra left += 1 / right −= 1 after the skip loops
✗ skip loops without left < right
✗ moving i inside the while loop
test it yourself (paste under the final Solution)
s = Solution()
print(s.threeSum([-1, 0, 1, 2, -1, -4]))            # [[-1, -1, 2], [-1, 0, 1]]
print(s.threeSum([-1, -1, -1, 0, 0, 0, 1, 2, 2, 3])) # [[-1, -1, 2], [-1, 0, 1], [0, 0, 0]]
print(s.threeSum([0, 1, 1]))                         # []
print(s.threeSum([0, 0, 0, 0]))                      # [[0, 0, 0]]
print(s.threeSum([-2, 0, 1, 1, 2]))                  # [[-2, 0, 2], [-2, 1, 1]]

Based on this video: 3Sum | Two Pointers