Skip to content
QSWEQB
mediumCodingEntryMidSenior

Return every distinct triplet in the array that sums to zero.

Sort the array, fix each element as an anchor, then converge two pointers inwards from both ends of the remaining range. The sort is asymptotically free against the O of n squared scan, and it is what lets you skip duplicate values positionally instead of deduplicating a set of results afterwards.

4 min readUpdated 2026-07-26Asked at Amazon, Facebook, Adobe

What the interviewer is scoring

  • Whether the candidate can say what property of a sorted array makes a pointer move safe to commit to
  • That duplicate handling is positional rather than a hash set of triplets bolted on at the end
  • Does the candidate skip duplicates for the two inner pointers only after recording a hit, not before
  • Whether the sort is defended as free rather than apologised for
  • Does the candidate notice that the anchor loop can stop early once the anchor value turns positive

Answer

Converging pointers are not a sliding window

Both techniques use two indices, and that is where the resemblance stops. A sliding window keeps a contiguous range and both indices travel in the same direction, relying on the tested property being monotone as the window shrinks. Converging pointers start at opposite ends of a sorted range and travel towards each other, relying on something different: that moving one end changes the sum in a known direction.

That second property is the whole licence for the algorithm. With the range sorted, if the current sum is too small then no partner further left can help, because everything to the left of the left pointer is smaller still; the only way to increase the sum is to advance the left pointer. Symmetrically, a sum that is too large forces the right pointer down. Each comparison therefore eliminates an entire column or row of the candidate space rather than a single pair, which is why one pass over a sorted range finds every qualifying pair in linear time instead of quadratic.

Fix one element as an anchor and the problem becomes exactly that: find pairs in the remainder summing to the negation of the anchor. The anchor loop is O(n), the pair scan inside it is O(n), so the whole thing is O(n²).

Why sorting costs nothing here

Candidates often hesitate over the sort as though it were a concession. It is not. O(n log n) is strictly dominated by the O(n²) scan that follows, so the total remains O(n²) and the sort has bought you two things you cannot get otherwise: the directional argument above, and equal values sitting adjacent, which makes duplicate elimination an index comparison instead of a set membership test.

Auxiliary space is O(1) beyond the sort. Be precise about the sort's own cost if asked — Java's Arrays.sort on a primitive int[] is a dual-pivot quicksort, so it is in place but uses O(log n) stack, while sorting boxed objects uses a merge sort with O(n) auxiliary space. Claiming a flat O(1) without that qualifier is the sort of unexamined bound interviewers enjoy pulling on.

The implementation, with the duplicate logic in the right places

List<List<Integer>> threeSum(int[] nums) {
    Arrays.sort(nums);
    List<List<Integer>> out = new ArrayList<>();
    int n = nums.length;

    for (int i = 0; i + 2 < n; i++) {
        if (nums[i] > 0) break;                          // smallest of three is positive, so no sum can be 0
        if (i > 0 && nums[i] == nums[i - 1]) continue;    // this anchor value was already explored exhaustively

        int left = i + 1, right = n - 1;
        while (left < right) {
            int sum = nums[i] + nums[left] + nums[right];
            if (sum < 0) {
                left++;
            } else if (sum > 0) {
                right--;
            } else {
                out.add(List.of(nums[i], nums[left], nums[right]));
                left++;
                right--;
                while (left < right && nums[left] == nums[left - 1]) left++;    // skip after the move, not before
                while (left < right && nums[right] == nums[right + 1]) right--;
            }
        }
    }
    return out;
}

Three lines carry the deduplication and each guards a different kind of repeat. The anchor skip is conditioned on i > 0 so the first element is never skipped against a phantom predecessor. The two inner skips run only in the branch that recorded a match, and crucially they run after left and right have already moved, comparing against the value just consumed. Advancing the pointers past duplicates before recording the match would step over legitimate triplets: with [-2, 0, 0, 2, 2] the answer contains [-2, 0, 2] exactly once, and the two zeros and two twos must be collapsed after the hit rather than pre-emptively.

The early break is worth writing because it is a correctness-preserving statement about the sorted order, not a micro-optimisation. Once the anchor is positive, all three chosen values are positive and no remaining anchor can reach zero. On an input that is mostly positive this turns the outer loop from n iterations into a handful. It is also the first thing to abandon if the target stops being zero, and being able to say which of your shortcuts depend on the target being zero is exactly the kind of thing the follow-up questions are for.

What the hashing solution costs you

The obvious alternative fixes an anchor and then uses a hash set to find the complement of each second element, which is also O(n²) time but adds O(n) space. It works, and it does not need the array sorted. The reason it is the weaker answer here is that deduplication becomes genuinely awkward: without sorted order there is no positional notion of "the same value again", so the usual fix is to canonicalise each triplet by sorting its three values and pushing it into a Set, which is a hash of every result rather than a property of the traversal. That both costs memory proportional to the output and hides the reasoning. If an interviewer specifically forbids sorting, the hashing version is the right response; unprompted, reaching for a result set signals that the duplicate structure was never analysed.

One detail to raise about generalising rather than to code defensively: with a target other than zero, or with values near the limits of the integer range, the three-term addition can overflow. Ask what the constraints guarantee. Widening the accumulator to long is a one-word fix and reflexively reaching for it without asking is not better than ignoring it, because it is the constraint discussion that is being graded.

Likely follow-ups

  • Generalise this to k-sum. What is the recurrence for the running time?
  • The target is now an arbitrary value rather than zero. Which of your optimisations survive?
  • Solve it with hashing instead. Why is deduplication harder that way?
  • Find the triplet whose sum is closest to the target rather than equal to it.

Related questions

two-pointerssortingarraysdeduplication