15. 3Sum
題目 / Problem
中文:給你一個整數陣列 nums,請找出所有不重複的「三元組」[nums[i], nums[j], nums[k]],要求三個索引互不相同(i != j、i != k、j != 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;*pdereferences it to read the value there. - malloc / free:C 手動申請與釋放堆積記憶體的函式。
malloc(n)要n個位元組,用完要free以免記憶體洩漏。mallocrequests heap memory,freereturns it; forgettingfreeleaks memory. - vector / 向量 (C++):C++ 標準庫的動態陣列,能自動增長,
push_back在尾端加元素。Avectoris C++'s auto-growing array;push_backappends an element.
思路
最直接的想法是「暴力三層迴圈」:枚舉所有 i < j < k 的組合,檢查 nums[i]+nums[j]+nums[k] 是否為 0。這在邏輯上沒問題,但時間複雜度是 O(n³),當 n 達到 3000 時大約是 270 億次運算,必定超時;而且還得額外處理重複三元組的問題。觀察提示可知:如果先固定第一個數 x,問題就退化成「在剩下的數裡找兩個數,使它們的和等於 -x」,也就是經典的兩數之和。要讓兩數之和變快,關鍵動作是先把陣列排序。排序後,對固定的 i,我們在 i 右邊放兩個指針:left 從 i+1 出發、right 從尾端出發。計算 nums[i]+nums[left]+nums[right]:如果太小,說明需要更大的數,於是 left++;如果太大,需要更小的數,於是 right--;如果剛好等於 0,就記錄這組答案。因為陣列有序,這個「往中間夾」的過程保證每個有效組合都會被掃到,且每個 i 只需 O(n) 時間,整體變成 O(n²)。去重靠排序帶來的好處:固定的 i 若和前一個值相同就跳過(避免以同一個數開頭重複枚舉);找到一組答案後,也要讓 left 和 right 跳過與當前相同的值。一個小剪枝:排序後若 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. Herenisnums.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、找到答案後跳過重複的left與right。少了任何一處都會輸出重複組合。You must skip duplicates in three places (theivalue, andleft/rightafter a hit); missing any one produces duplicate output. - 整數溢位 / Integer overflow:本題數值上限
3 * 10^5,落在int範圍內,所以 C 版用int安全;但養成習慣對「三數相加」用更寬的型別(C++ 版用long)更穩妥。Values here fit inint, 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比較。Writinga - bin the comparator can overflow; use explicitifcomparisons instead. - 指針交錯邊界 / Pointer crossing:內層迴圈條件必須是
left < right(嚴格小於),否則left == right時會把同一個元素用兩次,違反索引互不相同的要求。Use strictleft < right; otherwiseleft == rightwould 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 withleft < rightbefore touchingnums[left+1]/nums[right-1]. - 忘記回傳長度 / Forgetting the size outputs (C):LeetCode 的 C 介面靠
*returnSize和*returnColumnSizes得知結果大小,忘了設定會讀到垃圾值。The C harness reads*returnSizeand*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.