字串演算法 — 實戰題解

陣列與字串優先度 4/5 — 高價值 — 這裡有缺口就會掉關高價值 更新於 Nov 12, 2026
章節優先度優先度 5/5 — 必備 — 幾乎每一輪面試都會出現必備優先度 4/5 — 高價值 — 這裡有缺口就會掉關高價值優先度 3/5 — 值得會 — 多半是必備模式的變形值得會優先度 2/5 — 冷門 — 讀過一次即可,除非目標公司已知會問冷門只標在真正需要的章節上 —— 沒標的是背景/參考資料。

範圍 — 收錄不是模板錨點題的字串 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)就不開節,它們的錨點題在核心卡裡已經解完。先讀模板,再看題目卡。

索引

住在別處 — 完整解法在別的文件;列在這裡,讓搜尋這個題號的人有地方落腳。

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
# 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
// 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);
}
text
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
# 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
// 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;
}
text
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
# 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
// 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
# 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
text
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
# 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
# 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
// 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();
}
text
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
# 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
// 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
# 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
# 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
# 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
text
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
# 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
// 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 就不可能出現。