1. Two Sum
題目 / Problem
中文
給定一個整數陣列 nums 和一個整數 target,請回傳「兩個數字的索引」,使得這兩個數字相加等於 target。
- 你可以假設每個輸入「剛好只有一組解」。
- 你不可以重複使用同一個元素(同一個索引不能用兩次)。
- 答案的順序不限(
[0,1]和[1,0]都算對)。
English
Given an integer array nums and an integer target, return the indices of the two numbers that add up to target.
- Each input has exactly one solution.
- You may not use the same element twice.
- You may return the answer in any order.
Constraints / 限制
- 2 <= nums.length <= 10^4
- -10^9 <= nums[i] <= 10^9
- -10^9 <= target <= 10^9
- Only one valid answer exists. / 只有一組有效答案。
Worked example / 範例
Input: nums = [2,7,11,15], target = 9
Output: [0,1]
Explanation: nums[0] + nums[1] = 2 + 7 = 9 → 回傳索引 [0,1]
名詞解釋 / Glossary
- 索引 / index:元素在陣列中的位置編號,從 0 開始。
nums[0]是第一個元素。An index is the position number of an element in an array; counting starts at 0. - 互補值 / complement:對某個數字
x,它的互補值就是target - x。只要找到這個互補值,兩者相加就剛好等於target。For a numberx, its complement istarget - x; if both exist, they sum totarget. - 雜湊表 / hash map:一種把「鍵 (key)」對應到「值 (value)」的資料結構,查詢、插入平均只要 O(1) 時間。這裡我們用「數值 → 索引」的對應。A hash map stores key→value pairs with average O(1) lookup and insert. Here we map number→index.
- 暴力法 / brute force:最直接、不耍聰明的解法,通常是把所有可能組合都試一遍。The most direct solution that simply tries every possibility.
- O(n) / 線性時間:執行時間和輸入大小
n成正比。n 越大,時間等比例增加。Running time grows proportionally to input sizen.
思路
最直覺的做法是暴力法:用兩層迴圈,外層固定第一個數字 nums[i],內層掃描它後面的每一個數字 nums[j],檢查 nums[i] + nums[j] 是否等於 target。這一定能找到答案,但要檢查的組合數量大約是 n²/2,當 n 達到 10^4 時就會很慢,時間複雜度是 O(n²)。問題的核心是:對每一個 nums[i],我們其實只在找一個「特定的數字」——也就是 target - nums[i](稱為互補值)。暴力法慢的原因,是每次都要重新掃描整個陣列才能找到這個互補值。於是優化的關鍵就是:能不能用一個資料結構,讓「查某個數字在不在、它的索引是多少」變成瞬間完成?答案是雜湊表。我們只掃一遍陣列,對每個數字 nums[i],先算出它需要的互補值 target - nums[i],到雜湊表裡查這個互補值之前有沒有出現過;有的話就直接拿到那兩個索引、回傳。如果沒有,就把「當前數字 → 當前索引」存進雜湊表,繼續往下走。這樣只需要一次遍歷,每次查詢平均 O(1),總時間降到 O(n)。一個重要的不變量是:我們永遠是「先查、後存」,所以查到的互補值一定是排在當前元素前面的元素,因此不會把同一個元素用兩次。
The brute-force approach uses two nested loops: the outer loop fixes nums[i], the inner loop scans every later element nums[j], checking whether nums[i] + nums[j] == target. It always works, but it tests about n²/2 pairs, which is too slow at n = 10^4 — that's O(n²) time. The key insight is that for each nums[i] we are really searching for one specific value: target - nums[i], called the complement. Brute force is slow because it rescans the whole array each time to find that complement. So the optimization is to use a data structure that makes "is this value present, and at what index?" instant — a hash map. We scan the array once; for each nums[i] we compute its needed complement target - nums[i] and look it up in the map. If it's there, we've found a number seen earlier plus the current one, so we return both indices. If not, we store nums[i] → i in the map and move on. One pass, average O(1) per lookup, total O(n) time. A crucial invariant: we always look up before inserting, so any complement we find belongs to an element strictly before the current one — guaranteeing we never reuse the same element twice.
逐步走查 / Walkthrough
Input: nums = [2,7,11,15], target = 9. We keep a hash map seen (value → index), initially empty. / 我們維護一個雜湊表 seen(數值 → 索引),一開始是空的。
| i | nums[i] | complement = target - nums[i] | seen 裡有 complement 嗎? / in map? | 動作 / action |
|---|---|---|---|---|
| 0 | 2 | 9 - 2 = 7 | No(map 為空 / map empty) | 存入 7? 不,存當前數值 2 → 索引 0;seen = {2:0} / store 2→0 |
| 1 | 7 | 9 - 7 = 2 | Yes!2 在 seen,索引是 0 |
找到答案,回傳 [seen[2], 1] = [0, 1] / return [0,1] |
The algorithm stops at i = 1 and returns [0,1]. / 演算法在 i = 1 停止並回傳 [0,1]。
Solution — C
// 演算法 / Algorithm:
// 用雜湊表(key=數值, value=索引)掃一遍陣列;對每個元素先查它的互補值是否已出現,
// 有就回傳兩個索引,否則把當前元素存入。平均 O(n) 時間。
// One pass with a hash map (number→index): for each element look up its complement;
// if found, return both indices, else insert the current element. Average O(n) time.
#include <stdlib.h> // malloc / free 等記憶體函式 / memory functions
// 雜湊表的一個節點:存一個 (數值, 索引),並用鏈結串列處理碰撞
// One hash-table node: holds a (value, index) pair; uses a linked list for collisions
typedef struct Node {
long long key; // 數值本身 / the number itself
int idx; // 它在 nums 裡的索引 / its index in nums
struct Node *next; // 指向同一個桶裡的下一個節點 / next node in same bucket
} Node;
// LeetCode 規定:回傳的陣列要用 malloc 配置,並透過 returnSize 告知長度
// LeetCode requires: return a malloc'd array and report its length via returnSize
int* twoSum(int* nums, int numsSize, int target, int* returnSize) {
// 答案一定是 2 個索引,先配置 2 個 int 的空間 / answer is always 2 indices
int* result = (int*)malloc(2 * sizeof(int));
*returnSize = 2; // 透過指標把長度寫回給呼叫者 / write length back via pointer
// 桶的數量;取大於 numsSize 的數可減少碰撞。+1 避免 numsSize 為 0 時除以 0
// Number of buckets; larger than numsSize to reduce collisions. +1 avoids divide-by-0
int buckets = numsSize * 2 + 1;
// 配置一個指標陣列當作桶;calloc 會把所有指標初始化為 NULL(空桶)
// Allocate a pointer array as buckets; calloc zero-initializes them to NULL (empty)
Node** table = (Node**)calloc(buckets, sizeof(Node*));
for (int i = 0; i < numsSize; i++) {
// 用 long long 避免 target - nums[i] 在極端值時溢位 / avoid overflow with long long
long long complement = (long long)target - nums[i];
// 把互補值映射到某個桶;取絕對值與取餘數確保落在 [0, buckets) 範圍
// Map complement to a bucket; abs + modulo keeps it within [0, buckets)
int h = (int)(llabs(complement) % buckets);
// 走訪這個桶的鏈結串列,找有沒有等於 complement 的節點
// Walk the linked list in this bucket, looking for a node equal to complement
for (Node* p = table[h]; p != NULL; p = p->next) {
if (p->key == complement) { // 找到互補值了 / complement found
result[0] = p->idx; // 較早出現的那個索引 / the earlier index
result[1] = i; // 當前索引 / the current index
// 提早回傳;此處不釋放 table 以保持範例精簡(程式結束後 OS 會回收)
// Early return; we skip freeing table here for brevity (OS reclaims on exit)
return result;
}
}
// 沒找到互補值,就把「當前數值 → 當前索引」插入它自己的桶
// Not found: insert "current value → current index" into its own bucket
long long key = nums[i]; // 當前數值 / current value
int hi = (int)(llabs(key) % buckets); // 算出當前數值該放哪個桶 / its bucket
Node* node = (Node*)malloc(sizeof(Node)); // 配置一個新節點 / allocate a node
node->key = key; // 填入數值 / set value
node->idx = i; // 填入索引 / set index
node->next = table[hi]; // 頭插法:接到桶原本的串列前面 / push front
table[hi] = node; // 讓桶指向新節點 / bucket now points here
}
// 題目保證一定有解,理論上不會走到這裡 / problem guarantees a solution exists
return result;
}
Solution — C++
// 演算法 / Algorithm:
// 用 unordered_map(數值→索引)掃一遍;對每個元素先查互補值是否已存在,
// 有就回傳兩索引,否則把當前元素存入。平均 O(n) 時間。
// One pass with an unordered_map (value→index): look up each complement first;
// return both indices if present, else insert the current element. Average O(n).
#include <vector> // std::vector,動態陣列 / dynamic array
#include <unordered_map> // std::unordered_map,雜湊表 / hash map
class Solution {
public:
std::vector<int> twoSum(std::vector<int>& nums, int target) {
// seen 把「看過的數值」對應到「它的索引」/ maps each seen value to its index
std::unordered_map<int, int> seen;
// range-for:依序取出每個索引 i,從 0 到 nums.size()-1
// range-based loop over indices 0 .. nums.size()-1
for (int i = 0; i < (int)nums.size(); i++) {
// 算互補值;nums[i] 與 target 都在 int 範圍,差值也在 int 範圍,不會溢位
// complement fits in int because both operands are within int range
int complement = target - nums[i];
// find 在雜湊表裡查 complement;找不到會回傳 end() 這個特殊迭代器
// find() looks up complement; returns end() iterator if absent
auto it = seen.find(complement);
if (it != seen.end()) { // 找到互補值 / complement exists
// it->second 是該互補值存的索引;連同當前 i 一起回傳
// it->second is the stored index; return it together with current i
return { it->second, i }; // 用大括號直接建構回傳的 vector / brace-init vector
}
// 沒找到就記錄當前數值與索引,供後面的元素查詢
// Not found: record current value→index for later elements to look up
seen[nums[i]] = i;
}
// 題目保證有解,這行只是讓編譯器滿意 / guaranteed solution; satisfies the compiler
return {};
}
};
複雜度 / Complexity
- Time: O(n) — 我們只把陣列從頭到尾掃一遍(n 是
nums的長度),每個元素做一次雜湊表查詢加一次插入,平均都是 O(1),所以總時間和 n 成正比。We scan the array once; each element does one O(1) average lookup plus one O(1) insert, so total time is proportional to n. - Space: O(n) — 最壞情況下(答案在最後才找到),雜湊表會存進幾乎所有 n 個元素,因此額外空間和 n 成正比。In the worst case the map stores almost all n elements, so extra space grows with n.
Pitfalls & Edge Cases
- 重複使用同一元素 / Reusing the same element:必須「先查、後存」。如果先把當前元素存進表再查互補值,當
target == 2*nums[i]時會查到自己。本解法先查再存,保證互補值來自更早的索引。Look up before inserting, otherwisetarget = 2*nums[i]would match the element with itself. - 重複數字 / Duplicate values(如
[3,3]):第二個 3 查互補值 3 時,表裡已存了第一個 3 的索引 0,於是回傳[0,1],完全正確。不要因為值相同就誤以為衝突。The second 3 finds the first 3's index; values being equal is fine. - 整數溢位 / Integer overflow:
target和nums[i]都可達 ±10^9,在 C 裡算target - nums[i]兩個 int 相減最大約 2×10^9 會超出int(上限約 2.1×10^9,雖接近但不安全),故 C 版用long long計算互補值最穩妥。Use a wider type for the complement to stay safe near int limits. - 回傳值約定 / Return-value convention(C):LeetCode 的 C 介面要求用
malloc配置回傳陣列並設定*returnSize;忘記設*returnSize = 2會讓判題讀到錯誤長度。Forgetting*returnSize = 2makes the grader read the wrong length. - 找不到解 / No solution found:題目保證恰有一組解,所以理論上迴圈內必定回傳;末尾的回傳只是防禦性寫法,避免編譯警告。The trailing return is defensive only; the problem guarantees a hit inside the loop.