K-sum (three or more items with a target sum)
Find groups of three or more items that add up to a target by fixing items one at a time and solving the last pair with two pointers.
O(n²)Space O(1) besides the output and the sort5 practice problemsStep-through pictureThe idea
Finding a pair with a target sum in a sorted list takes one walk with two pointers. K-sum asks the same for three, four, or more items: which three items add up to zero, or which four widths fill a row exactly.
The trick is to reduce the problem. Sort the list. For three items, fix the first item nums[i] with a loop. Now you need a pair in the rest of the list that adds up to target - nums[i], and that is the two-pointer pair search. For four items, fix two items with two loops and search for the last pair.
Most k-sum questions want each group only once. Sorting puts equal values next to each other, so you can skip a value that is the same as the one before it. That stops you from building the same group twice.
An everyday example
Choosing three dishes for a fixed budget
A menu lists dishes from cheapest to most expensive, and you want three dishes that cost exactly your budget. You pick the first dish, then look for two more using the cheap end and the expensive end of the rest of the menu, moving inward. When that runs out, you pick the next first dish. If the next dish has the same price as the one you just tried, you skip it, because it would give the same price groups again.
| In the example | In the code |
|---|---|
| The first dish you pick | nums[i] |
| The cheapest and most expensive of the rest | nums[left] and nums[right] |
| The total of the three dishes | nums[i] + nums[left] + nums[right] |
| Skipping a first dish with the same price | if (i > 0 && nums[i] === nums[i - 1]) continue |
| Writing down a group that fits the budget | result.push([nums[i], nums[left], nums[right]]) |
From the slow way to k-sum (three or more items with a target sum)
- Start with the obvious way for three items: three nested loops over
i < j < k, checking each triple's sum. For 1,000 numbers that is 166,167,000 triples. It isO(n^3), and on top of that, repeated values make the same group of numbers appear many times. - Notice that fixing one item turns the problem into one you already know. Once
nums[i]is chosen, you need two more items, afteri, that add up totarget - nums[i]. That is a pair-sum question. - If the list is sorted, the pair-sum question is the two-pointer walk:
leftjust afteri,rightat the end, moving inward by the usual rule. Each walk isO(n), and there are aboutnchoices ofi, so the whole search isO(n^2)instead ofO(n^3). - Sorting also solves the repeats, because equal values end up side by side. Skip an
iwhose value equals the previousi, and after saving a group, skipleftpast copies of the value just used. Sort, fix the first item, walk the pair, skip repeats: that loop is k-sum.
How it works
- Sort
numsin ascending order. - Loop
iover every index that leaves room for two more items. This fixes the first item. - If
nums[i]equalsnums[i - 1], skip it: every group starting with that value was already found. - Set
left = i + 1andrightto the last index, then do the two-pointer walk on the part afteri. - If the three items add up to less than
target, moveleftright; if more, moverightleft. - If they add up to
target, save the group, move both pointers inward, and skip values equal to the one just used atleft. - After every
iis done, return all saved groups.
Watch it run
This picture runs the code below on a real input. Press Step to move one step at a time, or Play to watch. The highlighted line is the one that just ran. Change the input to see other cases.
See every step at onceThe same run, laid out side by side
The code
Point at a note to light up the lines it explains.
function threeSum(nums, target) {nums.sort((a, b) => a - b);const result = [];for (let i = 0; i < nums.length - 2; i++) {if (i > 0 && nums[i] === nums[i - 1]) continue;let left = i + 1;let right = nums.length - 1;while (left < right) {const sum = nums[i] + nums[left] + nums[right];if (sum < target) {left++;} else if (sum > target) {right--;} else {result.push([nums[i], nums[left], nums[right]]);left++;right--;while (left < right && nums[left] === nums[left - 1]) left++;}}}return result;}
How fast it is
| Time | O(n²) |
|---|---|
| Extra memory | O(1) besides the output and the sort |
The outer loop runs n times and each pair walk is at most n steps. Sorting is O(n log n), which is smaller. The groups found are the only real memory used.
- Best case
O(n log n)All values equal, like 1,000 zeros: the sort dominates. The first walk saves
[0, 0, 0]and movesleftto the end, and every lateriis skipped as a repeat. In one test this took about 2,000 loop steps. - Average case
O(n^2)Mostly distinct values: about
nwalks of up tonsteps each. In one test with 1,000 distinct values, the loops took about 375,000 steps. - Worst case
O(n^2)Distinct values with few or no answers: every
igets a full walk until the pointers meet.
Common mistakes
- Forgetting to skip repeated values, which saves the same group many times, such as
[-1, 0, 1]twice. - Calling
nums.sort()with no compare function, which sorts numbers as text:[-1, -4, 0]stays in a wrong order. - After finding a group, moving only one pointer. The other pointer's item now needs a different partner, so the loop finds nothing new or repeats the group.
- Using a
Setof joined strings to remove repeat groups. It works but hides the real fix and uses extra memory; sorting and skipping is cleaner.
When to use it
Look for these clues in a problem:
- The question asks for three or four items whose sum equals, comes closest to, or stays below a target.
- It asks for all such groups, each listed once, so duplicates must be skipped.
- The order of the input doesn't matter for the answer, so sorting is allowed.
- An
O(n³)orO(n⁴)loop over every group is too slow, butO(n²)is fine.
Check yourself
Answer each one in your head first, then open it.
Why does the outer loop stop at nums.length - 2?
left starts at i + 1 and must be less than right, the last index, so i needs at least two items after it. The last useful i is nums.length - 3.
Why is skipping an i equal to nums[i - 1] safe?
The walk for i - 1 searched every item from i on, a superset of what i would search. Any group starting with that value was already found.
Why don't you need to skip copies at right after a save?
After left moves past every copy of its value, the new nums[left] is different, so the partner that makes the same sum must be different too. A repeat group can't form.
How many steps does the search take for 4 numbers?
At most 2 walks (i = 0 and i = 1), with at most 2 and 1 pointer steps, so 3 steps plus the sort. The O(n^2) growth shows only for bigger inputs.
Practice problems
Stuck on one? Tuto is in the playground too, and gives you hints, never the answer.
0 of 5 solved. Solve one to earn the bronze medal, half for silver, and all of them for gold.
Start practicingmedium
Go further
You don't need these to start practicing. Open one when you want more.
Why it works
The reasoning that makes it always right
Every answer group, written in sorted order, has a smallest index i, and its other two items come after i. So if, for each i, the pair walk finds every pair after i that adds up to target - nums[i], the outer loop finds every group. The walk does exactly that, by the same proof as the two-pointer pair search: each pointer move only drops an item that can't be in any pair with the current nums[i].
Skipping an i whose value equals nums[i - 1] loses nothing. The walk for i - 1 searched the items from i on, which include every item after i, so it already found every group that starts with that value. The same reasoning covers moving left past copies after a save: a second group with the same nums[left] value would need the same partner, so it would be the same group.
Everything ends: each walk moves a pointer inward every step, so it stops after at most n steps, and the outer loop visits each i once. When the outer loop ends, every possible smallest index has been tried, so result holds every group exactly once.
Going deeper
3 more ideas
Why fixing one item turns it into a pair search
Every group of three has a smallest index i. Once nums[i] is fixed, the other two items come from the part after i, and they must add up to target - nums[i]. That part is still sorted, so the two-pointer walk finds every pair with that sum in one pass. Trying every i therefore tries every group.
Why it takes O(n²) time
Sorting costs O(n log n). The outer loop runs about n times, and each two-pointer walk takes at most n steps, so the total is O(n²). For four items, there are two fixed loops and the walk, so O(n³). Each extra item adds one loop, so k-sum takes O(n^(k-1)).
Skipping duplicates in the right places
There are two places that can create a repeated group. The first item: skip i when nums[i] === nums[i - 1]. The pair: after saving a group, move left past every copy of the value just used. You don't need a third skip for right, because once nums[left] changes, the matching nums[right] must change too.
Work it by hand
Every step in a table, with why
Find every group of three that adds up to 0. After sorting, the value -3 appears twice, so one i is skipped.
nums = [5, -3, 1, -3, 2, 0, -2], target = 0; sorted: [-3, -3, -2, 0, 1, 2, 5]
| Step | i (nums[i]) | left | right | sum | What happens | Why |
|---|---|---|---|---|---|---|
| 1 | 0 (-3) | 1 | 6 | -1 | Too small: left++ | -3 + -3 + 5 = -1. A bigger middle item is needed. |
| 2 | 0 (-3) | 2 | 6 | 0 | Save [-3, -2, 5], move both | A match. Both pointers move inward, to left = 3 and right = 5. |
| 3 | 0 (-3) | 3 | 5 | -1 | Too small: left++ | -3 + 0 + 2 = -1. |
| 4 | 0 (-3) | 4 | 5 | 0 | Save [-3, 1, 2], move both | Another match. Now left = 5 and right = 4, so this walk ends. |
| 5 | 1 (-3) | - | - | - | Same as nums[0]: skip | Every group starting with -3 was already found when i was 0, so this i is skipped. |
| 6 | 2 (-2) | 3 | 6 | 3 | Too big: right-- | -2 + 0 + 5 = 3. A smaller last item is needed. |
| 7 | 2 (-2) | 3 | 5 | 0 | Save [-2, 0, 2], move both | A match. Both pointers move to 4, so this walk ends. |
| 8 | 3 (0) | 4 | 6 | 6 | Too big: right-- | 0 + 1 + 5 = 6. |
| 9 | 3 (0) | 4 | 5 | 3 | Too big: right-- | 0 + 1 + 2 = 3, still too big. The pointers meet and the walk ends. |
| 10 | 4 (1) | 5 | 6 | 8 | Too big: right-- | 1 + 2 + 5 = 8. This is the last i, because i < nums.length - 2 means i stops at 4. |
It returns [[-3, -2, 5], [-3, 1, 2], [-2, 0, 2]]. The outer loop ends at i = 4, the last index that leaves room for left and right.
Compared with other ways
When to pick something else
| Approach | Time | Extra memory | Choose it when |
|---|---|---|---|
| K-sum (three or more items with a target sum) | O(n^2) | O(1) | You need every group of three once, or the closest sum, and sorting the input is allowed. |
| Three nested loops | O(n^3) | O(1) | n is at most a few hundred, or you want a slow reference to test the fast version against. |
| Fixed item plus a hash set | O(n^2) | O(n) | Sorting isn't allowed or you need original positions; removing repeat groups then needs extra care. |
Extra memory: O(1) beyond the output and the sort. JavaScript's sort may use O(n) memory of its own, and the output can hold up to O(n^2) groups.
How the steps grow
- K-sum (three or more items with a target sum), Fixed item plus a hash set
O(n^2) - Three nested loops
O(n^3)
| Approach | n = 10 | n = 1,000 | n = 1,000,000 |
|---|---|---|---|
| K-sum (three or more items with a target sum) | 100 | 1 million | 1 trillion |
| Three nested loops | 1,000 | 1 billion | more than a quadrillion |
| Fixed item plus a hash set | 100 | 1 million | 1 trillion |
Inputs that need care
5 tricky inputs
- Fewer than three numbers:
nums = [1, 2] i < nums.length - 2is false at once, so it returns[].- All zeros:
nums = [0, 0, 0, 0] - It returns
[[0, 0, 0]]once. The skip after a save movesleftpast every extra zero, and laterivalues are skipped as repeats. - No group works:
nums = [1, 2, 3, 4],target = 100 - Every walk moves
leftuntil the pointers meet, and the result is[]. - Sorting without a compare function
[10, 9, -1, -4].sort()gives[-1, -4, 10, 9], because it compares text. The two-pointer rule then moves the wrong way. Always pass(a, b) => a - b.- The caller's array changes
nums.sortsorts in place, so the caller's array comes back reordered:[3, -1, -2]becomes[-2, -1, 3]. Sort a copy,[...nums].sort((a, b) => a - b), if that matters.
Variations and follow-ups
Twists interviewers add
Closest sum of three
Keep the sum with the smallest Math.abs(sum - target) instead of saving matches, and move the pointers by the same rule. Duplicate skipping becomes optional, because only one number is returned.
Four or more items
For four items, add another fixed loop: i, then j > i, then the pair walk, which is O(n^3). A general version fixes one item and calls itself with k - 1 and the remaining target, ending at the two-pointer walk when k is 2. Each extra item adds a factor of n.
Counting triples below a target
When nums[i] + nums[left] + nums[right] < target, every index from left + 1 to right also works with nums[i] and nums[left], so add right - left at once and move left. This counts in O(n^2) without listing the triples.
A hash set for the pair
For each i, scan the items after it and look up target - nums[i] - nums[j] in a Set of values seen so far in that scan. It is also O(n^2) but uses O(n) extra memory, and avoiding repeated groups is harder without sorting.
In an interview
Spot it, explain it, answer follow-ups
- Spot it from "three (or four) numbers that add up to", "all unique groups", or "closest sum of three". If the input order doesn't matter, sorting is the first move.
- Explain the reduction out loud: "I fix the smallest item with a loop, and the rest is a pair-sum search on the sorted part after it, which two pointers solve in
O(n)." - Spend extra care on repeats: say where the two skips go and why there is no third skip for
right. Then test by hand with all zeros and with a value that appears twice as a first item. - Expect the follow-up "what about four numbers, or
k?". The answer: one more fixed loop per extra item, or a recursive function that reduceskby one until it reaches the pair walk, forO(n^(k-1))time.
Where frontend code uses it
The same idea in real UI code
- A layout tool that picks three column widths to fill a fixed row width exactly can search widths with a fixed first width and a two-pointer pair.
- A shop's bundle builder that suggests three products whose total price is closest to a gift card amount.
- Checking a design system's spacing scale for three tokens that add up to a container's padding or gap.