3121. Count the Number of Special Characters II
題目 / Problem
給你一個字串 word。如果一個字母 c 同時以小寫和大寫出現在 word 中,而且 c 的每一個小寫出現位置都在它第一個大寫出現位置之前,那麼這個字母 c 就稱為特殊(special) 的。請回傳 word 中特殊字母的數量。
You are given a string word. A letter c is called special if it appears both in lowercase and uppercase in word, and every lowercase occurrence of c appears before the first uppercase occurrence of c. Return the number of special letters in word.
Constraints / 限制:
- 1 <= word.length <= 2 * 10^5
- word 只由大小寫英文字母組成 / consists of only lowercase and uppercase English letters.
Worked example / 範例: word = "aaAbcBC" → output 3. 特殊字母是 'a'、'b'、'c',因為每個字母的所有小寫都排在它第一個大寫之前。
名詞解釋 / Glossary
- 計數陣列 / counting array:一個固定大小的陣列(這裡用大小 26,對應 26 個英文字母),用整數索引直接記錄某個資訊。比用雜湊表更快、更省記憶體,因為字母種類有限。
- 索引 / index:字元在字串中的位置(從 0 開始算)。
word[0]是第一個字元。 - 大小寫判斷 / case check:用
c >= 'a' && c <= 'z'判斷是否小寫;'a'、'A'等字元在 C/C++ 中其實是整數(ASCII 碼),可以直接比較大小。 - 字母對應到 0–25 / mapping a letter to 0–25:用
c - 'a'(小寫)或c - 'A'(大寫)把字母轉成 0 到 25 的索引,當作陣列下標使用。 - 哨兵值 / sentinel value:一個「不可能是真實答案」的特殊初始值(這裡用
-1),用來表示「還沒看到過」。
思路
最直接的暴力法是:對每個字母(a–z 共 26 種),掃過整個字串,找出它所有小寫的位置和所有大寫的位置,再檢查條件。但這樣每個字母都掃一次字串,總共要掃 26 次,雖然也能過,但其實沒必要。我們真正需要知道的,並不是「所有」位置,而是兩個關鍵點:這個字母最後一次出現小寫的位置,以及它第一次出現大寫的位置。為什麼是這兩個?因為「每個小寫都在第一個大寫之前」這個條件,等價於「最晚的那個小寫,也在最早的那個大寫之前」。只要最後一個小寫的索引小於第一個大寫的索引,所有小寫自然都在前面。於是我們開兩個大小為 26 的計數陣列:lastLower 記錄每個字母最後一次小寫的索引,firstUpper 記錄每個字母第一次大寫的索引,都初始化為 -1 表示尚未出現。只掃一遍字串就能把它們全部填好——遇到小寫就更新(覆蓋)lastLower,遇到大寫就只在第一次寫入 firstUpper。最後對 26 個字母逐一檢查:兩者都出現過(不是 -1),且 lastLower < firstUpper,就算一個特殊字母。整個過程只掃字串一次,是線性時間。
The brute-force idea is: for each of the 26 letters, scan the whole string to collect every lowercase position and every uppercase position, then check the rule. That re-scans the string 26 times, which is wasteful. The insight is that we don't need all positions — only two: the last index where the letter appears lowercase, and the first index where it appears uppercase. The condition "every lowercase comes before the first uppercase" is exactly equivalent to "the latest lowercase comes before the earliest uppercase." If the last lowercase index is smaller than the first uppercase index, then every lowercase is necessarily before it. So we keep two size-26 counting arrays: lastLower[c] and firstUpper[c], both initialized to -1 (meaning "not seen"). A single pass fills them — on a lowercase letter we overwrite lastLower (so it ends up holding the last one), and on an uppercase letter we set firstUpper only the first time. Finally we check all 26 letters: a letter is special when both values exist (not -1) and lastLower < firstUpper. One pass over the string makes this linear time.
逐步走查 / Walkthrough
Input word = "aaAbcBC". Index/字元對照:0:'a' 1:'a' 2:'A' 3:'b' 4:'c' 5:'B' 6:'C'. 初始 lastLower 和 firstUpper 全為 -1。
| i | char | 動作 / action | lastLower 變化 | firstUpper 變化 |
|---|---|---|---|---|
| 0 | a |
小寫,更新 lastLower[a] | a → 0 | — |
| 1 | a |
小寫,覆蓋 lastLower[a] | a → 1 | — |
| 2 | A |
大寫,第一次寫 firstUpper[a] | — | a → 2 |
| 3 | b |
小寫,更新 lastLower[b] | b → 3 | — |
| 4 | c |
小寫,更新 lastLower[c] | c → 4 | — |
| 5 | B |
大寫,第一次寫 firstUpper[b] | — | b → 5 |
| 6 | C |
大寫,第一次寫 firstUpper[c] | — | c → 6 |
最後逐字母檢查 / final per-letter check:
- a: lastLower=1, firstUpper=2 → 都存在且 1 < 2 ✅ special
- b: lastLower=3, firstUpper=5 → 3 < 5 ✅ special
- c: lastLower=4, firstUpper=6 → 4 < 6 ✅ special
- 其餘字母兩者皆為 -1,跳過 / all other letters stay -1, skipped.
計數 / count = 3. ✅
Solution — C
// 演算法 / Algorithm:
// 掃一遍字串,記錄每個字母「最後一次小寫」和「第一次大寫」的索引。
// One pass: record each letter's last-lowercase index and first-uppercase index.
// 若兩者都存在且 lastLower < firstUpper,該字母即為 special。
// A letter is special if both exist and lastLower < firstUpper.
int numberOfSpecialChars(char* word) {
// lastLower[k] = 字母 k 最後一次以小寫出現的索引;-1 表示尚未出現
// lastLower[k] = index of the last lowercase occurrence of letter k; -1 = not seen
int lastLower[26];
// firstUpper[k] = 字母 k 第一次以大寫出現的索引;-1 表示尚未出現
// firstUpper[k] = index of the first uppercase occurrence of letter k; -1 = not seen
int firstUpper[26];
// 初始化兩個陣列為 -1(哨兵值,代表「還沒看到」)
// Initialize both arrays to -1 (sentinel meaning "not yet seen")
for (int k = 0; k < 26; k++) {
lastLower[k] = -1; // 尚無小寫 / no lowercase yet
firstUpper[k] = -1; // 尚無大寫 / no uppercase yet
}
// 逐字元掃描;word[i] != '