字串演算法 — 實戰題解
範圍 — 收錄不是模板錨點題的字串 LeetCode 實戰題,每題都寫成一張題目卡,歸檔到它所歸約的 string.md 模板底下;概念、模式卡與錨點題本身都留在主字串文件。 另見:string.md — 母文件,每張卡歸約到的模板都在那裡;string_operations.md — 語言層級的字串 API,從同一份檔案拆出;palindrome.md — 回文家族的深入版;string_matching_kmp_rolling_hash.md — 子字串搜尋;sliding_window.md — 字元視窗類題目;2_pointers_examples.md — 雙指標題目集,LC 524 與 647 住在那裡(LC 165 是 2_pointers.md 模板 9 的錨定題)。
LeetCode 題目清單
總覽
下面每一節對應 string.md 的一個模板,順序和那份文件一樣;每一題都是一張題目卡:先寫關鍵,再寫它歸約到哪張卡、差在哪一點,最後是一份標準解。這裡沒有題目的模板(5 到 8)就不開節,它們的錨點題在核心卡裡已經解完。先讀模板,再看題目卡。
索引
| string.md 裡的卡 | 題目(易 → 難) |
|---|---|
| Template 1:雙指標掃描與原地反轉 | LC 917 |
| Template 2:字元頻率與 anagram 簽章 | LC 1794 · LC 720 · LC 828 |
| Template 3:連續段分組 | LC 38 · LC 809 |
| Template 4:分階段解析與重建 | LC 13 · LC 482 · LC 468 · LC 816 · LC 273 |
住在別處 — 完整解法在別的文件;列在這裡,讓搜尋這個題號的人有地方落腳。
| LC | 題目 | 所在 |
|---|---|---|
| 14 | Longest Common Prefix | python_trick.md — 前綴縮短法;或直向掃描,把每個字的第 i 欄拿來比 |
| 72 | Edit Distance | dp.md § Template 8 |
| 165 | Compare Version Numbers | 2_pointers.md § Template 9 — 每個字串一個指標,每輪解析一段版本號 |
| 205 · 890 | Isomorphic Strings · Find and Replace Pattern | hash_map.md § Template 7 — 兩張 map,s→t 和 t→s |
| 394 | Decode String | stack_expression_parsing.md |
| 445 | Add Two Numbers II | add_x_sum.md |
| 459 · 686 · 796 | Repeated Substring Pattern · Repeated String Match · Rotate String | string_matching_kmp_rolling_hash.md |
| 524 | Longest Word in Dictionary through Deleting | 2_pointers_examples.md — 對每個單字跑一次 LC 392 的子序列檢查 |
| 647 | Palindromic Substrings | 2_pointers_examples.md — 中心擴張;Manacher 在 palindrome.md |
| 738 | Monotone Increasing Digits | greedy_examples.md — 遇到下降就借位,後綴全部填 9 |
| 953 | Verifying an Alien Dictionary | hash_map.md § Template 6 |
| 1143 | Longest Common Subsequence | dp.md § Template 9 |
一行解 — 不需要開卡,一行就講得完。
| LC | 題目 | 一行思路 | 卡 |
|---|---|---|---|
| 383 | Ransom Note | 數雜誌的字母,信裡每個字母扣一次,扣到負數就失敗 | Template 2 |
| 819 | Most Common Word | 轉小寫、依非字母切開、跳過禁用字集合,取次數最多的 | Template 2 |
| 299 | Bulls and Cows | 一趟:位置相同且數字相同是 bulls;其餘各自累加到兩個計數陣列,cows = sum(min(cntS[d], cntG[d])) |
Template 2 |
| 937 | Reorder Data in Log Files | split(" ", 1) 拆成 id 和內容;字母 log 依 (內容, id) 排序,數字 log 保持原順序(穩定排序) |
Template 6 |
雙指標掃描範例
Reverse Only Letters — LC 917
關鍵 — 非字母永遠不動,所以它們是兩個指標要跨過去的牆;每一次交換都把左邊數來第 k 個字母和右邊數來第 k 個字母配對,這正好就是只反轉字母。
歸約到:string.md § Template 1 — 就是 LC 125 的掃描,只是把比較換成交換,跳過的是非字母而不是非英數字元。
# python
# LC 917 - Reverse Only Letters
# IDEA: left/right walk inward; a non-letter on either side is stepped over, two letters swap
# time = O(n), space = O(n) for the char list (strings are immutable)
def reverseOnlyLetters(s):
chars = list(s)
left, right = 0, len(chars) - 1
while left < right:
if not chars[left].isalpha(): # left is not a letter: it stays where it is
left += 1
elif not chars[right].isalpha(): # same on the right
right -= 1
else: # two letters: swap them and move both inward
chars[left], chars[right] = chars[right], chars[left]
left += 1
right -= 1
return "".join(chars)
// java
// LC 917 - Reverse Only Letters
// IDEA: left/right walk inward; a non-letter on either side is stepped over, two letters swap
// time = O(n), space = O(n) for the char array
public String reverseOnlyLetters(String s) {
char[] chars = s.toCharArray();
int left = 0, right = chars.length - 1;
while (left < right) {
if (!Character.isLetter(chars[left])) {
left++;
} else if (!Character.isLetter(chars[right])) {
right--;
} else {
char tmp = chars[left];
chars[left] = chars[right];
chars[right] = tmp;
left++;
right--;
}
}
return new String(chars);
}
s = "ab-cd"
[a b - c d] left=0 right=4 both letters swap -> [d b - c a]
[d b - c a] left=1 right=3 both letters swap -> [d c - b a]
left=2 right=2 stop -> "dc-ba"
陷阱
- 兩個內層跳過迴圈之後直接交換、沒有再檢查
left < right:"7_28]"(沒有字母)會把兩個非字母對調。像上面那樣每輪只移動一個指標就不會出事。 - 用
isalnum取代isalpha:"a1b"會把1也搬走。只有字母會動。
相似題 — LC 344(整個反轉)· LC 345(只交換母音)· LC 151(反轉單字順序)
字元頻率與簽章範例
Count Pairs of Equal Substrings With Minimum Difference — LC 1794
關鍵 — 任何一對相等的子字串都能縮成它的第一個字元,而
j - a不會變大,所以只需要看單一字元;對每個字母,最佳配對就是它在firstString的第一個索引配上它在secondString的最後一個索引。
歸約到:string.md § Template 2 — 每個字串各一張以字母表為索引的表,只是格子裡放的是索引(第一次或最後一次出現),不是次數。
# python
# LC 1794 - Count Pairs of Equal Substrings With Minimum Difference
# IDEA: char -> FIRST index in firstString, char -> LAST index in secondString; minimise the gap
# time = O(m + n), space = O(1) (at most 26 keys per map)
def countQuadruples(firstString, secondString):
first_idx = {} # char -> its FIRST index in firstString
for i in range(len(firstString)):
ch = firstString[i]
if ch not in first_idx:
first_idx[ch] = i
last_idx = {} # char -> its LAST index in secondString
for a in range(len(secondString)):
last_idx[secondString[a]] = a # later indices overwrite earlier ones
min_diff = float('inf')
count = 0
for ch in first_idx:
if ch not in last_idx: # a char missing from secondString forms nothing
continue
diff = first_idx[ch] - last_idx[ch]
if diff < min_diff: # a strictly smaller diff restarts the count
min_diff = diff
count = 1
elif diff == min_diff: # an equal diff is one more quadruple
count += 1
return count
// java
// LC 1794 - Count Pairs of Equal Substrings With Minimum Difference
// IDEA: int[26] of last positions (+1, so 0 means absent); scan firstString once, first hits only
// time = O(m + n), space = O(1)
public int countQuadruples(String firstString, String secondString) {
int[] last = new int[26]; // last[c] = last index of c in secondString, plus 1
for (int a = 0; a < secondString.length(); a++) {
last[secondString.charAt(a) - 'a'] = a + 1;
}
boolean[] seen = new boolean[26]; // only the FIRST occurrence in firstString counts
int minDiff = Integer.MAX_VALUE, count = 0;
for (int i = 0; i < firstString.length(); i++) {
int c = firstString.charAt(i) - 'a';
if (seen[c]) {
continue;
}
seen[c] = true;
if (last[c] == 0) { // 0 means c never appears in secondString
continue;
}
int diff = i - (last[c] - 1);
if (diff < minDiff) {
minDiff = diff;
count = 1;
} else if (diff == minDiff) {
count++;
}
}
return count;
}
firstString = "abcd", secondString = "bccda"
char first i last a i - a
a 0 4 -4 <- minimum
b 1 0 1
c 2 2 0
d 3 3 0 -> only 'a' reaches -4 -> answer 1
陷阱
- 把每一對差值相同的子字串都算進去:答案數的是達到最小值的四元組,而每個字母剛好貢獻一個,所以
("abcd", "bccda")是 1,不會更多。 - 在
firstString留最後一個索引,或在secondString留第一個索引:兩者都會讓i - a變大,就錯過最小值。
相似題 — LC 1624(第一次/最後一次出現的跨度)· LC 387(第一個不重複的字元)
Longest Word in Dictionary — LC 720
關鍵 — 排序之後,一個單字少一個字元的前綴一定比它先被處理,所以只要拿直接前綴去查「已經能蓋出來的單字」集合就夠了:用歸納法,更短的前綴在那個前綴放進集合時早就檢查過。
歸約到:string.md § Template 2 — 用整個字串當 key 做雜湊查詢,而不是用字母次數;跨多個單字查前綴的一般解是 trie(trie.md),這裡用排序後的集合取代它,因為每個單字只會問一個前綴。
# python
# LC 720 - Longest Word in Dictionary
# IDEA: sort, then a word is buildable iff it has length 1 or word[:-1] is already buildable
# time = O(sum(L) log n) for the sort + O(sum(L)) for the slices, space = O(sum(L))
def longestWord(words):
words = sorted(words) # a prefix sorts before every word that extends it
buildable = set()
best = ""
for word in words:
if len(word) == 1 or word[:-1] in buildable:
buildable.add(word)
if len(word) > len(best): # strictly longer: ties keep the earlier, smaller word
best = word
return best
// java
// LC 720 - Longest Word in Dictionary
// IDEA: sort, then a word is buildable iff it has length 1 or its one-shorter prefix is buildable
// time = O(sum(L) log n), space = O(sum(L))
public String longestWord(String[] words) {
Arrays.sort(words);
Set<String> buildable = new HashSet<>();
String best = "";
for (String word : words) {
if (word.length() == 1 || buildable.contains(word.substring(0, word.length() - 1))) {
buildable.add(word);
if (word.length() > best.length()) {
best = word;
}
}
}
return best;
}
陷阱
- 更新
best時用>=而不是>:["a","b"]會回傳"b";排序已經讓字典序較小的字先到,所以只有嚴格更長的字才能取代它。 - 先把單字加進
buildable再檢查前綴:只有["banana"]時就會從一個不是單一字母的字開始一條鏈。
相似題 — LC 648(取代單字,trie)· LC 1858(所有前綴都在的最長單字,trie)
Count Unique Characters of All Substrings of a Given String — LC 828
關鍵 — 把總和翻過來算:不要去數每個子字串裡有幾個唯一字母,而是對每個位置數「這個字母在其中只出現一次」的子字串有幾個。那就是(到前一個相同字母的距離)×(到下一個相同字母的距離)。
歸約到:array.md § Template 5: Index Contribution Counting — 同樣是「每個元素貢獻 左邊選法 × 右邊選法」的總和,跨度由同一個字母的前後兩次出現夾住;每個字母記住最後兩個位置的那張表,就是 Template 2 的字母表,只是存的是索引。
# python
# LC 828 - Count Unique Characters of All Substrings of a Given String
# IDEA: per letter keep its last two positions; when a third arrives, the middle one's
# contribution (prev1 - prev2) * (i - prev1) is final
# time = O(n), space = O(1) (26 letters)
def uniqueLetterString(s):
last_two = {} # char -> [second-to-last index, last index]
total = 0
for i, ch in enumerate(s):
prev2, prev1 = last_two.get(ch, [-1, -1])
# the occurrence at prev1 is unique in substrings that start in (prev2, prev1]
# and end in [prev1, i): (prev1 - prev2) * (i - prev1) of them
if prev1 != -1:
total += (prev1 - prev2) * (i - prev1)
last_two[ch] = [prev1, i]
n = len(s)
for prev2, prev1 in last_two.values(): # settle each char's final occurrence
total += (prev1 - prev2) * (n - prev1)
return total
s = "ABA" (n = 3)
A at 0: prev = -1, next = 2 -> (0 - (-1)) * (2 - 0) = 2
B at 1: prev = -1, next = 3 -> (1 - (-1)) * (3 - 1) = 4
A at 2: prev = 0, next = 3 -> (2 - 0) * (3 - 2) = 2
total = 8
陷阱
- 忘了最後那一趟:迴圈結束時,每個字母的最後一次出現都還沒結算,所以
"ABC"會回傳 0 而不是 10。 - 列舉所有子字串再用集合數:最好也是 O(n²),而 n ≤ 10⁵。
相似題 — LC 2063(所有子字串的母音數)· LC 907(子陣列最小值總和)· LC 2262(字串的總吸引力)
連續段分組範例
Count and Say — LC 38
關鍵 — 每一項就是前一項壓縮成組、再念成
次數 + 字元;整個序列只需要分組走訪,跑 n − 1 次。
歸約到:string.md § Template 3 — 同一個分組走訪,每組輸出 str(count) + char,而不是把相鄰組長合併。
# python
# LC 38 - Count and Say
# IDEA: n - 1 times, walk the term in groups of equal digits and write count then digit
# time = O(n * L) where L is the length of the longest term, space = O(L)
def countAndSay(n):
term = "1"
for _ in range(n - 1):
parts = []
i = 0
while i < len(term):
j = i
while j < len(term) and term[j] == term[i]: # j ends the group that starts at i
j += 1
parts.append(str(j - i))
parts.append(term[i])
i = j
term = "".join(parts)
return term
陷阱
- 在走訪裡用
+=拼出這一項:對項長是平方時間;先加進 list,最後 join 一次。 - 在迴圈外手動收最後一組:
i = j的走法會自己收掉每一組,包括最後一組,所以n = 2不用特例就回傳"11"。
Expressive Words — LC 809 優先度 4/5 — 高價值 — 這裡有缺口就會掉關
關鍵 — 一個單字能對上
s,若且唯若兩個字串的組序列相同,而且每一對組要嘛長度相等,要嘛s那組比較長而且至少 3。
歸約到:string.md § Template 3 — 兩個字串的組,各用一個指標同步走,逐對比較組長,而不是累加 min(prev, cur)。
# python
# LC 809 - Expressive Words
# IDEA: walk s and the word together one group at a time; compare each pair of group lengths
# time = O(len(words) * len(s) + sum(len(w))): each word rescans s, space = O(1)
def expressiveWords(s, words):
def stretchy(word):
i = j = 0
while i < len(s) and j < len(word):
if s[i] != word[j]:
return False
ch = s[i]
run_s = 0
while i < len(s) and s[i] == ch: # length of this group in s
run_s += 1
i += 1
run_w = 0
while j < len(word) and word[j] == ch: # length of the same group in word
run_w += 1
j += 1
if run_s < run_w: # stretching only grows a group
return False
if run_s != run_w and run_s < 3: # a grown group must reach 3
return False
return i == len(s) and j == len(word) # both used up: same group sequence
count = 0
for word in words:
if stretchy(word):
count += 1
return count
// java
// LC 809 - Expressive Words
// IDEA: walk s and the word together one group at a time; compare each pair of group lengths
// time = O(len(words) * len(s) + sum(len(w))): each word rescans s, space = O(1)
public int expressiveWords(String s, String[] words) {
int count = 0;
for (String word : words) {
if (isStretchy(s, word)) {
count++;
}
}
return count;
}
private boolean isStretchy(String s, String word) {
int i = 0, j = 0;
while (i < s.length() && j < word.length()) {
if (s.charAt(i) != word.charAt(j)) {
return false;
}
char ch = s.charAt(i);
int runS = 0;
while (i < s.length() && s.charAt(i) == ch) {
runS++;
i++;
}
int runW = 0;
while (j < word.length() && word.charAt(j) == ch) {
runW++;
j++;
}
if (runS < runW) { // stretching only grows a group
return false;
}
if (runS != runW && runS < 3) { // a grown group must reach 3
return false;
}
}
return i == s.length() && j == word.length();
}
s = "heeellooo"
word "hello": h 1|1 e 3|1 (>= 3, ok) l 2|2 o 3|1 (>= 3, ok) both used up -> match
word "helo": h 1|1 e 3|1 (ok) l 2|1 (2 < 3, fail) -> no
word "hi": h 1|1 then 'e' != 'i' -> no
answer = 1
s 的組 vs 單字的組 |
s 的組 ≥ 3? |
結果 |
|---|---|---|
| 相等 | — | 對上 |
s 較短 |
— | 不行:拉長永遠不會變短 |
s 較長 |
是 | 對上:單字那組被拉長了 |
s 較長 |
否 | 不行:長度 2 的組不可能是拉長出來的 |
陷阱
- 少了最後的
i == len(s) and j == len(word):s = "abcd"、單字"abc"會對上迴圈看到的每一組,卻把d留著沒讀。 - 允許
run_s < run_w:s = "aaa"、單字"aaaa"不是 stretchy;延伸只會變長。 - 把長度 2 的組當成可以拉長:
s = "aa"、單字"a"不合法;拉長後的組一定要到 3。
相似題 — LC 443(字串壓縮)· LC 1446(最長的組)· LC 925(長按鍵入:同樣的同步走訪,沒有 ≥ 3 的規則)
解析與重建範例
Roman to Integer — LC 13 優先度 4/5 — 高價值 — 這裡有缺口就會掉關
關鍵 — 從右往左讀:比右邊符號小的符號是減法前綴(
IV、CM),所以要減;其他一律加。一個比較就取代六種特殊組合。
歸約到:string.md § Template 4 — LC 12 數值表的解析方向:一個索引,每個字元一條規則,規則由已經讀過的鄰居決定。
# python
# LC 13 - Roman to Integer
# IDEA: scan right to left; subtract a symbol smaller than the one after it, else add
# time = O(n), space = O(1)
def romanToInt(s):
value = {"I": 1, "V": 5, "X": 10, "L": 50, "C": 100, "D": 500, "M": 1000}
total = 0
prev = 0 # value of the symbol to the right
for i in range(len(s) - 1, -1, -1):
cur = value[s[i]]
if cur < prev: # a smaller symbol before a larger one subtracts
total -= cur
else:
total += cur
prev = cur
return total
// java
// LC 13 - Roman to Integer
// IDEA: scan right to left; subtract a symbol smaller than the one after it, else add
// time = O(n), space = O(1)
public int romanToInt(String s) {
Map<Character, Integer> value = new HashMap<>();
value.put('I', 1);
value.put('V', 5);
value.put('X', 10);
value.put('L', 50);
value.put('C', 100);
value.put('D', 500);
value.put('M', 1000);
int total = 0, prev = 0;
for (int i = s.length() - 1; i >= 0; i--) {
int cur = value.get(s.charAt(i));
if (cur < prev) {
total -= cur;
} else {
total += cur;
}
prev = cur;
}
return total;
}
陷阱
- 相等時也減(
cur <= prev):"III"會回傳 1 而不是 3。 - 把六種組合(
IV、IX、XL、…)寫死成兩字元 token 也能過,但面試官會追問;一個比較就涵蓋全部。
相似題 — LC 12(重建方向,在核心卡裡)· LC 273(下面)
License Key Formatting — LC 482
關鍵 — 只有第一組可能比較短,所以從右邊開始建:每 k 個字元放一個破折號,最後反轉一次。左邊完全不需要算組大小。
歸約到:string.md § Template 4 — 重建方向:讀的時候丟掉舊分隔符,依固定規則寫入新的,從尾端掃是因為規則錨定在尾端。
# python
# LC 482 - License Key Formatting
# IDEA: scan right to left, skip dashes, upper-case, insert '-' each time a group of k fills
# time = O(n), space = O(n)
def licenseKeyFormatting(s, k):
out = [] # built right to left
in_group = 0
for i in range(len(s) - 1, -1, -1):
if s[i] == '-':
continue
if in_group == k: # the group on the right is full: open a new one
out.append('-')
in_group = 0
out.append(s[i].upper())
in_group += 1
return "".join(reversed(out))
陷阱
- 組滿時在字元之後才寫破折號:
"a-a-a-a-"、k = 1 會多出開頭的-。像上面那樣在下一個字元之前插入。 - 輸入只有破折號(
"---")時必須回傳"",不是"-"。
Validate IP Address — LC 468
關鍵 — 分隔符的數量決定文法(3 個點 → IPv4,7 個冒號 → IPv6),接著每個欄位對照一張短的規則清單;第一條被打破的規則就回答
"Neither"。
歸約到:string.md § Template 4 — 依明確規則解析,只是階段變成「用分隔符切出的欄位」,而不是用一個索引走過的字元類別。
# python
# LC 468 - Validate IP Address
# IDEA: count separators to choose IPv4 / IPv6, split, validate each field against its rules
# time = O(n), space = O(n) for the split
def validIPAddress(queryIP):
def is_ipv4(parts):
for part in parts:
if not 1 <= len(part) <= 3:
return False
if not all('0' <= c <= '9' for c in part):
return False
if len(part) > 1 and part[0] == '0': # no leading zero
return False
if int(part) > 255:
return False
return True
def is_ipv6(parts):
hexdigits = "0123456789abcdefABCDEF"
for part in parts:
if not 1 <= len(part) <= 4:
return False
if not all(c in hexdigits for c in part):
return False
return True
if queryIP.count('.') == 3 and is_ipv4(queryIP.split('.')):
return "IPv4"
if queryIP.count(':') == 7 and is_ipv6(queryIP.split(':')):
return "IPv6"
return "Neither"
陷阱
- 漏掉空欄位檢查:
"1.1..1"點的數量對(3 個),但有一個空欄位;len(part) >= 1會擋掉它。 "01.01.01.01"是不合法的 IPv4(前導零),而"0db8"是合法的 IPv6 欄位:前導零規則只屬於 IPv4。str.isdigit()會接受"²";要跟'0'…'9'比。
相似題 — LC 93(復原 IP 位址:同樣的欄位規則,改成列舉)
Ambiguous Coordinates — LC 816
關鍵 — 兩個互相獨立的選擇:逗號放哪裡,以及每一半的小數點放哪裡(或不放)。一半合法的條件是整數部分沒有前導零、小數部分沒有尾隨零,所以答案就是兩半合法格式的笛卡兒積。
歸約到:string.md § Template 4 — 和解析數字一樣的前導零/格式規則,只是套用到每個切割位置,而不是單一個給定字串。
# python
# LC 816 - Ambiguous Coordinates
# IDEA: for every comma position, cross the valid formats of the left and right halves
# time = O(n^4): O(n) commas x O(n) formats per half on each side x O(n) to build each string,
# space = O(n^4) for the output
def ambiguousCoordinates(s):
def formats(part):
out = []
n = len(part)
if n == 1 or part[0] != '0': # whole number: no leading zero
out.append(part)
for i in range(1, n): # decimal: '.' after each interior position
whole, frac = part[:i], part[i:]
if len(whole) > 1 and whole[0] == '0':
continue # "01.5": leading zero in the whole part
if frac[-1] == '0':
continue # "1.50": trailing zero in the fraction
out.append(whole + "." + frac)
return out
digits = s[1:-1]
result = []
for i in range(1, len(digits)): # the comma goes after digits[:i]
for left in formats(digits[:i]):
for right in formats(digits[i:]):
result.append("(" + left + ", " + right + ")")
return result
s = "(0123)" -> digits = "0123"
"0" | "123" -> ["0"] x ["123", "1.23", "12.3"] -> 3
"01" | "23" -> ["0.1"] x ["23", "2.3"] -> 2
"012" | "3" -> ["0.12"] x ["3"] -> 1
total 6
陷阱
- 單獨的
"0"是合法的整數,"00"和"01"不是:要先判斷n == 1再套前導零規則,否則"(00011)"會漏掉(0, 0.011)。 - 任何以
0結尾的小數部分都不合法,包括"1.0";否則"1.0"和"1"會同時出現。
相似題 — LC 93(復原 IP 位址)· LC 282(給表達式加運算子)
Integer to English Words — LC 273 優先度 4/5 — 高價值 — 這裡有缺口就會掉關
關鍵 — 英文以 3 位數為一塊來念數字,每塊後面接它的單位(Thousand、Million、Billion);1–999 只要拼一次、每塊重用,而全為零的那塊什麼都不說。
歸約到:string.md § Template 4 — 重建方向,就像 LC 12 的貪婪表:這裡的表是單位清單,每一塊再用 below-20 和十位數兩張表拼出來。每次剝掉最大單位的遞迴寫法在 recursion.md。
# python
# LC 273 - Integer to English Words
# IDEA: split into 3-digit chunks low to high, spell each, tag with its scale, prepend
# time = O(1) (at most 4 chunks for a 32-bit int), space = O(1)
BELOW_20 = ["", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight",
"Nine", "Ten", "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen",
"Sixteen", "Seventeen", "Eighteen", "Nineteen"]
TENS = ["", "", "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety"]
SCALES = ["", "Thousand", "Million", "Billion"]
def numberToWords(num):
if num == 0:
return "Zero"
def three(n): # spell 1..999 as a list of words
if n == 0:
return []
if n < 20:
return [BELOW_20[n]]
if n < 100:
return [TENS[n // 10]] + three(n % 10)
return [BELOW_20[n // 100], "Hundred"] + three(n % 100)
parts = []
scale = 0
while num > 0:
chunk = num % 1000
if chunk != 0: # an all-zero chunk says nothing, not even its scale
words = three(chunk)
if scale > 0:
words.append(SCALES[scale])
parts = words + parts # chunks arrive low to high: prepend
num //= 1000
scale += 1
return " ".join(parts)
// java
// LC 273 - Integer to English Words
// IDEA: split into 3-digit chunks low to high, spell each, tag with its scale, prepend
// time = O(1) (at most 4 chunks for a 32-bit int), space = O(1)
private static final String[] BELOW_20 = {"", "One", "Two", "Three", "Four", "Five",
"Six", "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve", "Thirteen",
"Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen"};
private static final String[] TENS = {"", "", "Twenty", "Thirty", "Forty", "Fifty",
"Sixty", "Seventy", "Eighty", "Ninety"};
private static final String[] SCALES = {"", "Thousand", "Million", "Billion"};
public String numberToWords(int num) {
if (num == 0) {
return "Zero";
}
List<String> parts = new ArrayList<>();
int scale = 0;
while (num > 0) {
int chunk = num % 1000;
if (chunk != 0) { // an all-zero chunk says nothing
List<String> words = three(chunk);
if (scale > 0) {
words.add(SCALES[scale]);
}
parts.addAll(0, words); // chunks arrive low to high: prepend
}
num /= 1000;
scale++;
}
return String.join(" ", parts);
}
private List<String> three(int n) { // spell 1..999
List<String> out = new ArrayList<>();
if (n == 0) {
return out;
}
if (n < 20) {
out.add(BELOW_20[n]);
} else if (n < 100) {
out.add(TENS[n / 10]);
out.addAll(three(n % 10));
} else {
out.add(BELOW_20[n / 100]);
out.add("Hundred");
out.addAll(three(n % 100));
}
return out;
}
陷阱
1000010必須念成"One Million Ten":中間那塊是 0,把它的單位也輸出就會變成"One Million Thousand Ten"。num == 0是唯一會寫出"Zero"的地方;內部的零什麼都不貢獻。- 10 到 19 是不規則的字:要在十位數分支之前先處理
n < 20,否則 15 會變成"Ten Five"。 - 用
" "拼字串再 trim 容易出現雙空白;把單字放進 list 再 join 就不可能出現。