← 題庫 / Archive
2026-05-24 TI150 Medium ArrayTwo PointersSorting

15. 3Sum

題目 / Problem

中文:給你一個整數陣列 nums,請找出所有不重複的「三元組」[nums[i], nums[j], nums[k]],要求三個索引互不相同(i != ji != kj != k),且三個數字相加等於 0。注意:回傳的結果集合不能包含重複的三元組(例如 [-1,0,1][0,1,-1] 視為同一組,只能出現一次)。輸出的順序、以及每個三元組內部數字的順序都不影響判定。

English: Given an integer array nums, return all unique triplets [nums[i], nums[j], nums[k]] where the three indices are distinct and nums[i] + nums[j] + nums[k] == 0. The result set must not contain duplicate triplets (e.g. [-1,0,1] and [0,1,-1] count as the same triplet and may appear only once). The order of the output and the order of numbers inside each triplet do not matter.

Constraints / 限制 - 3 <= nums.length <= 3000 - -10^5 <= nums[i] <= 10^5

Worked example / 範例 - Input: nums = [-1,0,1,2,-1,-4] - Output: [[-1,-1,2],[-1,0,1]] - Why: (-1) + 0 + 1 = 0 and (-1) + (-1) + 2 = 0. The triplet [-1,2,-1] is the same as [-1,-1,2], so it is not listed twice.

名詞解釋 / Glossary

  • 三元組 / Triplet:由陣列中三個元素組成的一組值,例如 [-1, 0, 1]。A triplet is a group of three values taken from the array.
  • 排序 / Sorting:把陣列由小到大重新排列。一旦排序,相等的數字會排在一起,方便去除重複,也讓我們能用「往中間靠攏」的策略搜尋。Sorting reorders the array from smallest to largest; equal numbers become neighbors, which makes deduplication and the two-pointer search possible.
  • 雙指針 / Two pointers:用兩個索引(一個從左、一個從右)在已排序的區段上同時移動,根據當前的和往內收縮,把一個 O(n²) 的兩數搜尋降到 O(n)。Two pointers use one index from the left and one from the right of a sorted range; they move inward based on the current sum, turning a two-number search into a linear scan.
  • 不變式 / Invariant:在迴圈每一輪都保持成立的條件。這裡的不變式是「左指針左邊、右指針右邊的元素都已被排除,答案只可能在 [left, right] 之間」。An invariant is a condition that stays true on every loop iteration; here it is "everything outside [left, right] has been ruled out."
  • 去重 / Deduplication:跳過與前一個相同的數字,避免產生一模一樣的三元組。Skipping over numbers equal to the previous one so we never emit an identical triplet twice.
  • 指標 / Pointer (C):在 C 裡指標是「存放記憶體位址的變數」。int *p 指向一個整數,*p 表示取出那個位址裡的值(解參考)。In C a pointer is a variable holding a memory address; *p dereferences it to read the value there.
  • malloc / free:C 手動申請與釋放堆積記憶體的函式。malloc(n)n 個位元組,用完要 free 以免記憶體洩漏。malloc requests heap memory, free returns it; forgetting free leaks memory.
  • vector / 向量 (C++):C++ 標準庫的動態陣列,能自動增長,push_back 在尾端加元素。A vector is C++'s auto-growing array; push_back appends an element.

思路

最直接的想法是「暴力三層迴圈」:枚舉所有 i < j < k 的組合,檢查 nums[i]+nums[j]+nums[k] 是否為 0。這在邏輯上沒問題,但時間複雜度是 O(n³),當 n 達到 3000 時大約是 270 億次運算,必定超時;而且還得額外處理重複三元組的問題。觀察提示可知:如果先固定第一個數 x,問題就退化成「在剩下的數裡找兩個數,使它們的和等於 -x」,也就是經典的兩數之和。要讓兩數之和變快,關鍵動作是先把陣列排序。排序後,對固定的 i,我們在 i 右邊放兩個指針:lefti+1 出發、right 從尾端出發。計算 nums[i]+nums[left]+nums[right]:如果太小,說明需要更大的數,於是 left++;如果太大,需要更小的數,於是 right--;如果剛好等於 0,就記錄這組答案。因為陣列有序,這個「往中間夾」的過程保證每個有效組合都會被掃到,且每個 i 只需 O(n) 時間,整體變成 O(n²)。去重靠排序帶來的好處:固定的 i 若和前一個值相同就跳過(避免以同一個數開頭重複枚舉);找到一組答案後,也要讓 leftright 跳過與當前相同的值。一個小剪枝:排序後若 nums[i] > 0,因為後面只會更大,三數之和不可能為 0,可直接結束。

The naive approach is three nested loops over every i < j < k, checking whether the three values sum to zero. It is correct but runs in O(n³); with n up to 3000 that is tens of billions of operations and times out, and it still leaves the duplicate problem unsolved. The key insight from the hints: if we fix the first number x, we only need to find two other numbers summing to -x — the classic two-sum. To make two-sum fast, we first sort the array. After sorting, for each fixed index i we place two pointers to its right: left at i+1 and right at the end. We look at nums[i]+nums[left]+nums[right]. If the sum is too small we need a bigger value, so move left rightward; if too big we need a smaller value, so move right leftward; if it equals zero we record the triplet. Because the array is sorted, this inward squeeze is guaranteed to visit every valid pair while spending only O(n) per i, giving O(n²) overall. Sorting also makes deduplication easy: skip an i whose value equals the previous one (so we never start a triplet with the same number twice), and after recording a hit, advance left/right past any equal neighbors. One handy pruning step: once sorted, if nums[i] > 0 every later number is even larger, so no triplet can reach zero and we can stop early.

逐步走查 / Walkthrough

Example: nums = [-1,0,1,2,-1,-4].

Step 0 — Sort / 先排序: the array becomes [-4, -1, -1, 0, 1, 2] (indices 0..5).

We loop i over the sorted array. For each i, left = i+1, right = 5, and target sum = 0.

i nums[i] left,right nums[left],nums[right] sum 動作 / Action
0 -4 1,5 -1, 2 -3 sum < 0 → left++
0 -4 2,5 -1, 2 -3 sum < 0 → left++
0 -4 3,5 0, 2 -2 sum < 0 → left++
0 -4 4,5 1, 2 -1 sum < 0 → left++
0 -4 5,5 left == right, 結束此 i / stop this i
1 -1 2,5 -1, 2 0 命中! / hit! record [-1,-1,2]; left++, right--
1 -1 3,4 0, 1 0 命中! / hit! record [-1,0,1]; left++, right--
1 -1 4,3 left > right, 結束此 i / stop this i
2 -1 nums[2] == nums[1],與前一個相同 → 跳過 / duplicate, skip
3 0 4,5 1, 2 3 sum > 0 → right--
3 0 4,4 left == right, 結束 / stop
4 1 nums[4] = 1 > 0,剪枝結束整個迴圈 / prune, stop everything

Final answer / 最終答案: [[-1,-1,2], [-1,0,1]]. ✅

Solution — C

// 演算法 / Algorithm:
//   先排序,再固定第一個數 nums[i],在其右側用雙指針找另外兩數使三數和為 0。
//   Sort first, then fix nums[i] and use two pointers on its right to find a pair summing to -nums[i].
//   排序讓我們可以跳過重複值來去重,整體 O(n^2)。
//   Sorting lets us skip duplicates; overall O(n^2).

#include <stdlib.h>  // 提供 malloc/free/qsort / provides malloc, free, qsort

// qsort 需要的比較函式:回傳負/零/正代表 a<b / a==b / a>b
// Comparator required by qsort: returns negative/zero/positive for a<b / a==b / a>b
static int cmp(const void *a, const void *b) {
    int x = *(const int *)a;  // 把 void* 轉回 int* 再解參考取值 / cast void* back to int* and dereference
    int y = *(const int *)b;  // 同上,取第二個值 / same for the second value
    // 不用 (x - y),因為大數相減可能整數溢位 / avoid (x - y): subtraction can overflow for large ints
    if (x < y) return -1;     // x 較小排前面 / x goes first
    if (x > y) return 1;      // x 較大排後面 / x goes later
    return 0;                 // 相等 / equal
}

/**
 * 回傳值是 int**(指向多個 int* 的指標,即「陣列的陣列」)。
 * Return type is int** : a pointer to many int* rows, i.e. a 2D array.
 * returnSize       : 寫回三元組的數量 / number of triplets we produce
 * returnColumnSizes: 寫回每一列的長度(這裡每列都是 3)/ length of each row (always 3 here)
 */
int** threeSum(int* nums, int numsSize, int* returnSize, int** returnColumnSizes) {
    // 先排序,雙指針法的前提 / sort first — the two-pointer method relies on it
    qsort(nums, numsSize, sizeof(int), cmp);

    int capacity = 16;  // 結果陣列初始容量,不夠時再加倍 / initial capacity, doubled when full
    // 申請存放「每個三元組指標」的空間 / allocate space for the row pointers
    int **res = (int **)malloc(sizeof(int *) * capacity);
    // 同步申請每列長度的陣列 / allocate the parallel array of row lengths
    *returnColumnSizes = (int *)malloc(sizeof(int) * capacity);
    int count = 0;      // 目前已找到的三元組數量 / how many triplets found so far

    // 固定第一個數 nums[i];只需走到倒數第三個 / fix nums[i]; stop at the third-to-last
    for (int i = 0; i < numsSize - 2; i++) {
        // 剪枝:排序後若 nums[i] > 0,後面都更大,和不可能為 0 / prune: if smallest > 0, no zero sum possible
        if (nums[i] > 0) break;
        // 去重:i 與前一個值相同則跳過,避免重複開頭 / skip duplicate i to avoid repeated triplets
        if (i > 0 && nums[i] == nums[i - 1]) continue;

        int left = i + 1;          // 左指針從 i 右邊開始 / left pointer starts just right of i
        int right = numsSize - 1;  // 右指針從陣列尾端開始 / right pointer starts at the end

        while (left < right) {     // 兩指針未交錯時持續 / continue while pointers have not crossed
            // 用 int 相加可能溢位嗎?最大 3*10^5,遠小於 int 上限,安全 / sum fits in int (max ~3*10^5)
            int sum = nums[i] + nums[left] + nums[right];

            if (sum < 0) {
                left++;            // 和太小,需要更大的數 → 左指針右移 / too small, need larger → move left up
            } else if (sum > 0) {
                right--;           // 和太大,需要更小的數 → 右指針左移 / too big, need smaller → move right down
            } else {
                // 命中!容量不足就把陣列加倍 / hit! grow arrays if full
                if (count == capacity) {
                    capacity *= 2;
                    res = (int **)realloc(res, sizeof(int *) * capacity);
                    *returnColumnSizes = (int *)realloc(*returnColumnSizes, sizeof(int) * capacity);
                }
                // 為這一組三元組申請 3 個 int / allocate 3 ints for this triplet
                int *triplet = (int *)malloc(sizeof(int) * 3);
                triplet[0] = nums[i];      // 第一個數 / first number
                triplet[1] = nums[left];   // 第二個數 / second number
                triplet[2] = nums[right];  // 第三個數 / third number
                res[count] = triplet;              // 存入結果列 / store the row
                (*returnColumnSizes)[count] = 3;   // 這列長度為 3 / this row has length 3
                count++;                           // 計數加一 / increment count

                // 去重:跳過與當前相同的 left / skip duplicates of nums[left]
                while (left < right && nums[left] == nums[left + 1]) left++;
                // 去重:跳過與當前相同的 right / skip duplicates of nums[right]
                while (left < right && nums[right] == nums[right - 1]) right--;

                left++;   // 移到下一個不同的左值 / advance to next distinct left
                right--;  // 移到下一個不同的右值 / advance to next distinct right
            }
        }
    }

    *returnSize = count;  // 把三元組數量寫回給呼叫者 / report the count back to the caller
    return res;           // 回傳結果陣列 / return the 2D result
}

Solution — C++

// 演算法 / Algorithm:
//   排序後固定 nums[i],在右側用雙指針找另外兩數使三數和為 0;
//   Sort, fix nums[i], then two-pointer search on its right for a pair summing to -nums[i];
//   靠跳過相同值來去除重複三元組,整體 O(n^2)。
//   Skip equal values to drop duplicate triplets; overall O(n^2).

#include <vector>     // std::vector 動態陣列 / dynamic array
#include <algorithm>  // std::sort 排序 / sorting

class Solution {
public:
    // 回傳「向量的向量」,每個內層 vector 是一組三元組 / returns a vector of triplets
    std::vector<std::vector<int>> threeSum(std::vector<int>& nums) {
        std::sort(nums.begin(), nums.end());  // 由小到大排序 / sort ascending
        std::vector<std::vector<int>> res;    // 存放答案 / holds the answer
        int n = nums.size();                  // 陣列長度 / array length

        // 固定第一個數,i 只需到倒數第三個 / fix the first number, i stops at n-2
        for (int i = 0; i < n - 2; ++i) {
            if (nums[i] > 0) break;                       // 剪枝:最小值已 >0 / prune: smallest already > 0
            if (i > 0 && nums[i] == nums[i - 1]) continue; // 去重 i / skip duplicate i

            int left = i + 1;   // 左指針 / left pointer
            int right = n - 1;  // 右指針 / right pointer

            while (left < right) {
                // 用 long 相加避免理論溢位(雖然這裡 int 也夠)/ use long to be safe against overflow
                long sum = (long)nums[i] + nums[left] + nums[right];

                if (sum < 0) {
                    ++left;          // 和太小 → 左指針右移 / too small → move left right
                } else if (sum > 0) {
                    --right;         // 和太大 → 右指針左移 / too big → move right left
                } else {
                    // 命中!emplace_back 直接在尾端就地建構這個三元組 / hit! build the triplet in place at the end
                    res.push_back({nums[i], nums[left], nums[right]});

                    // 跳過重複的 left 與 right / skip duplicate left and right values
                    while (left < right && nums[left] == nums[left + 1]) ++left;
                    while (left < right && nums[right] == nums[right - 1]) --right;

                    ++left;   // 前進到下一個不同左值 / move to next distinct left
                    --right;  // 前進到下一個不同右值 / move to next distinct right
                }
            }
        }
        return res;  // 回傳所有三元組 / return all triplets
    }
};

複雜度 / Complexity

  • Time: O(n²) — 排序花 O(n log n);外層 for 固定 i 是 O(n),對每個 i 內層雙指針最多掃過剩餘元素一次是 O(n),相乘為 O(n²),主導了排序成本。Sorting is O(n log n); the outer loop runs n times and the inner two-pointer pass is O(n) each, giving O(n²), which dominates the sort. Here n is nums.length.
  • Space: O(1)O(log n) — 不計輸出空間,演算法只用了幾個指針/索引變數;額外空間主要來自排序的遞迴堆疊(O(log n))。Ignoring the output array, we use only a few index variables; the extra space comes from the sort's recursion stack (O(log n)). The output itself can be O(n²) in the worst case but that is required by the problem.

Pitfalls & Edge Cases

  • 重複三元組 / Duplicate triplets:最常見的錯誤。必須在三個地方去重——跳過重複的 i、找到答案後跳過重複的 leftright。少了任何一處都會輸出重複組合。You must skip duplicates in three places (the i value, and left/right after a hit); missing any one produces duplicate output.
  • 整數溢位 / Integer overflow:本題數值上限 3 * 10^5,落在 int 範圍內,所以 C 版用 int 安全;但養成習慣對「三數相加」用更寬的型別(C++ 版用 long)更穩妥。Values here fit in int, but summing three numbers in a wider type (long) is a safe habit for larger constraints.
  • 比較函式不要用相減 / Don't subtract in the comparator:C 的 qsort 比較函式若寫 return a - b,當數值很大或一正一負時可能溢位導致排序錯誤。改用 if 比較。Writing a - b in the comparator can overflow; use explicit if comparisons instead.
  • 指針交錯邊界 / Pointer crossing:內層迴圈條件必須是 left < right(嚴格小於),否則 left == right 時會把同一個元素用兩次,違反索引互不相同的要求。Use strict left < right; otherwise left == right would reuse one element, breaking the distinct-index rule.
  • 去重時的越界 / Out-of-bounds while skipping:跳過重複值的 while 迴圈一定要先檢查 left < right,再存取 nums[left+1] / nums[right-1],否則可能讀到陣列外。Guard the skip loops with left < right before touching nums[left+1]/nums[right-1].
  • 忘記回傳長度 / Forgetting the size outputs (C):LeetCode 的 C 介面靠 *returnSize*returnColumnSizes 得知結果大小,忘了設定會讀到垃圾值。The C harness reads *returnSize and *returnColumnSizes; leaving them unset yields garbage.
  • 空結果 / Empty result:像 [0,1,1] 沒有任何合法三元組,回傳空陣列即可(C 版 count 為 0,C++ 版回傳空 vector),這是正常情況而非錯誤。Inputs like [0,1,1] legitimately yield an empty result — not a bug.