383. Ransom Note
題目 / Problem
中文: 給定兩個字串 ransomNote(勒索信)和 magazine(雜誌)。如果 ransomNote 裡的每一個字母都可以從 magazine 裡「剪」出來拼湊而成,就回傳 true,否則回傳 false。注意:magazine 裡的每個字母只能用一次。換句話說,對於每一個字母,ransomNote 需要的數量不能超過 magazine 擁有的數量。
English: You are given two strings, ransomNote and magazine. Return true if ransomNote can be built entirely from the letters available in magazine, otherwise return false. Each letter in magazine may be used at most once. In plain terms: for every letter, the count you need in ransomNote must not exceed the count you have in magazine.
約束 / Constraints:
- 1 <= ransomNote.length, magazine.length <= 10^5
- 兩個字串都只由小寫英文字母組成 / both strings consist only of lowercase English letters.
範例 / Worked example:
- ransomNote = "aa", magazine = "aab" → true,因為 magazine 有兩個 a,剛好夠拼出 "aa"。/ magazine has two as, exactly enough to spell "aa".
名詞解釋 / Glossary
- 計數陣列 / Counting array (frequency array): 一個固定大小的陣列,用每個位置(index)代表一個字母,陣列裡存的數字就是「這個字母出現了幾次」。因為只有 26 個小寫字母,我們用一個長度 26 的整數陣列即可。A fixed-size array where each index stands for one letter and the stored value is how many times that letter appears. With only 26 lowercase letters, a length-26 int array is enough.
- 雜湊表 / Hash map: 一種「鍵 → 值」的容器,可以用任意鍵(這裡是字元)快速查到對應的值(出現次數)。C++ 的
unordered_map就是雜湊表。A "key → value" container that lets you look up a value (here, a count) by an arbitrary key (here, a character) in roughly constant time. C++'sunordered_mapis a hash map. - ASCII 值 / ASCII value: 每個字元在電腦裡其實是一個整數。小寫字母
'a'到'z'的整數值是連續的,所以c - 'a'會得到 0 到 25 的索引。Each character is internally an integer. Lowercase'a'–'z'are consecutive, soc - 'a'maps a letter to an index 0–25. - O(n) 時間複雜度 / O(n) time complexity: 執行時間大致和輸入長度
n成正比;掃過字串一遍就是 O(n)。Running time grows roughly in proportion to the input lengthn; scanning the string once is O(n).
思路
最直覺的暴力法是:對於 ransomNote 裡的每一個字母,去 magazine 裡找一個還沒用過的相同字母,找到就把它「劃掉」。這需要對每個字元都掃一遍 magazine,時間複雜度是 O(n × m),在長度達到 10^5 時太慢,而且還要額外標記哪些字母已被用掉,很麻煩。更聰明的觀察是:我們其實不在乎字母的「位置」,只在乎「數量」。只要 magazine 裡某個字母的數量 ≥ ransomNote 裡需要的數量,就一定拼得出來。因此我們先用一個長度 26 的計數陣列統計 magazine 裡每個字母出現幾次;接著掃 ransomNote,每遇到一個字母就把對應計數減一,如果減之前發現該字母的計數已經是 0,代表雜誌裡不夠用,立刻回傳 false。能掃完整個 ransomNote 都沒出問題,就回傳 true。為什麼用 c - 'a' 當索引?因為字母是連續的整數,這樣能把字元直接對應到 0–25 的格子,查表 O(1)。整個過程只掃兩遍字串,時間 O(n),空間是固定的 26 格 O(1)。
The brute-force idea is: for each letter in ransomNote, scan magazine for an unused matching letter and cross it off. That costs O(n × m) time and needs bookkeeping to track used letters — too slow at length 10^5. The key insight is that positions don't matter, only counts do. If magazine contains at least as many of each letter as ransomNote needs, the note can always be assembled. So we first tally magazine into a length-26 counting array. Then we walk through ransomNote; for each letter we decrement its count, but if the count is already 0 before decrementing, the magazine has run out of that letter and we return false immediately. If we finish the whole note without running short, we return true. We use c - 'a' as the index because lowercase letters are consecutive integers, giving an O(1) slot lookup. The whole thing scans each string once, so it's O(n) time and O(1) space (a fixed 26 slots).
逐步走查 / Walkthrough
範例 / Example: ransomNote = "aa", magazine = "aab".
Step 1 — 統計 magazine / Tally the magazine. 掃過 "aab":
| 字元 / char | 動作 / action | count['a'] | count['b'] |
|---|---|---|---|
a |
count[0]++ | 1 | 0 |
a |
count[0]++ | 2 | 0 |
b |
count[1]++ | 2 | 1 |
統計結果 / Result: a → 2, b → 1。
Step 2 — 掃 ransomNote / Scan the note "aa".
| 字元 / char | 檢查 count 是否為 0? / count == 0? | 動作 / action | count['a'] 之後 / after |
|---|---|---|---|
a |
count[0]=2,不為 0 / not 0 | count[0]-- | 1 |
a |
count[0]=1,不為 0 / not 0 | count[0]-- | 0 |
掃完整個 ransomNote 都沒有遇到不足的情況 / finished the note without running short → 回傳 true / return true. ✅
Solution — C
// 演算法 / Algorithm:
// 用長度 26 的計數陣列記錄 magazine 每個字母的數量,
// 再掃 ransomNote 逐一扣減;若某字母不夠用就回傳 false。
// Tally magazine's letters in a 26-slot array, then decrement per
// note letter; return false the moment a letter runs out.
#include <stdbool.h> // 讓我們能用 bool / true / false / lets us use bool, true, false
bool canConstruct(char* ransomNote, char* magazine) {
int count[26] = {0}; // 26 個字母的計數,全部初始化為 0 / counts for 26 letters, all zeroed
// {0} 會把整個陣列填 0 / {0} zero-initializes the whole array
// 第一遍:統計 magazine 裡每個字母出現幾次 / Pass 1: count each letter in magazine
for (int i = 0; magazine[i] != '