← 題庫 / Archive
2026-06-08 TI150 Easy ArrayHash Table

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 number x, its complement is target - x; if both exist, they sum to target.
  • 雜湊表 / 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 size n.

思路

最直覺的做法是暴力法:用兩層迴圈,外層固定第一個數字 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, otherwise target = 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 overflowtargetnums[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 = 2 makes the grader read the wrong length.
  • 找不到解 / No solution found:題目保證恰有一組解,所以理論上迴圈內必定回傳;末尾的回傳只是防禦性寫法,避免編譯警告。The trailing return is defensive only; the problem guarantees a hit inside the loop.