進階字串演算法

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

範圍 — 比較重的字串工具:後綴結構、Z-algorithm、Manacher,以及字串 DP — 這些對主字串文件來說太專門的部分。 另見:string.md — 日常字串的題目目錄與模板;string_matching_kmp_rolling_hash.md — 專講子字串搜尋;palindrome.md — 回文題家族;trie.md — 前綴結構。

LeetCode 題目清單

總覽

進階字串演算法指的是基本操作之外、比較講究的字串處理技巧。這些演算法在模式比對、回文偵測和複雜字串操作上,能給出有理論保證的最佳解。

關鍵性質

  • 時間複雜度:最佳演算法通常是 O(n) 或 O(n + m)
  • 空間複雜度:預處理結構需要 O(n)
  • 核心想法:先把字串預處理過,之後查詢和比對就能很快
  • 什麼時候用:字串模式複雜、要查很多次、需要最佳化
  • 主要演算法:KMP、Manacher、Z-Algorithm、Rolling Hash、後綴陣列

共同特徵

  • 預處理:先建輔助結構,換取後續操作的速度
  • 辨識模式:找出重複出現的結構與週期
  • 線性時間:靠一些巧妙的技巧壓到最佳複雜度
  • 多次查詢:同一個字串反覆操作時特別划算
  • 理論基礎:建立在字串理論與自動機之上

題型分類

分類 1:模式比對

  • 描述:在文字中高效率地找出模式出現的位置
  • 例題:LC 28(Find Index of First Occurrence)、LC 459(Repeated Substring Pattern)
  • 模式:用 KMP、Z-Algorithm、Rolling Hash 做到 O(n + m)

分類 2:回文問題

  • 描述:找出所有回文,或最長的回文子字串
  • 例題:LC 5(Longest Palindromic Substring)、LC 647(Palindromic Substrings)
  • 模式:用 Manacher 演算法做到 O(n) 的回文偵測

分類 3:字串週期性

  • 描述:偵測重複模式與字串週期
  • 例題:LC 459(Repeated Substring Pattern)、LC 1316(Distinct Echo Substrings)
  • 模式:用 failure function 或 Z-array 做週期偵測

分類 4:後綴類問題

  • 描述:牽涉到字串後綴與字典序的問題
  • 例題:LC 1044(Longest Duplicate Substring)、LC 1316(Distinct Echo Substrings)
  • 模式:後綴陣列、最長共同前綴、rolling hash

模板與演算法

模板比較表

演算法 用途 時間複雜度 空間複雜度 什麼時候用
KMP 模式比對 O(n + m) O(m) 單一模式搜尋 → string_matching_kmp_rolling_hash.md
Manacher 所有回文 O(n) O(n) 回文類問題
Z-Algorithm 字串比對 O(n) O(n) 模式比對的各種變形
Rolling Hash 子字串比較 O(n) O(1) 多模式搜尋 → string_matching_kmp_rolling_hash.md
DFA/狀態機 格式驗證/斷詞 O(n) O(1) 規則亂成一團的 if/else 解析(LC 65)

KMP 與 Rolling Hash 的主場在字串比對那份

子字串搜尋只有一個主場:string_matching_kmp_rolling_hash.md。原本這裡的模板 1 與模板 4(KMP、rolling hash)跟它重複,所以已移除,其餘模板保留原本的編號。

你需要的 去哪裡
KMP 失敗函數、Rabin-Karp 搜尋 string_matching_kmp_rolling_hash.md § 1-2) Templates
用雙重雜湊降低碰撞 § 1-3) Double Hashing
對長度二分搜尋 + rolling hash(LC 1044、LC 718) § 2-3) LC 1044、§ 1-6) LC 718

留在這裡的是那份文件沒有涵蓋的工具:Manacher、Z-algorithm、後綴陣列與字串自動機。

模板 2:Manacher 演算法 — LC 5

python
def manacher_algorithm(s):
    """Manacher's algorithm to find all palindromes in O(n)"""

    def preprocess(s):
        """Preprocess string to handle even-length palindromes"""
        # Transform "abba" -> "^#a#b#b#a#$"
        result = "^"
        for c in s:
            result += "#" + c
        result += "#$"
        return result

    processed = preprocess(s)
    n = len(processed)
    p = [0] * n  # p[i] = radius of palindrome centered at i
    center = right = 0

    for i in range(1, n - 1):
        # Mirror of i with respect to center
        mirror = 2 * center - i

        # If i is within the right boundary, use previously computed values
        if i < right:
            p[i] = min(right - i, p[mirror])

        # Try to expand palindrome centered at i
        while processed[i + p[i] + 1] == processed[i - p[i] - 1]:
            p[i] += 1

        # If palindrome centered at i extends past right, adjust center and right
        if i + p[i] > right:
            center, right = i, i + p[i]

    return p, processed

def manacher_applications():
    """Various applications of Manacher's algorithm"""

    def longest_palindromic_substring(s):
        """Find longest palindromic substring (LC 5)"""
        if not s:
            return ""

        p, processed = manacher_algorithm(s)
        max_len = 0
        center_index = 0

        for i in range(1, len(p) - 1):
            if p[i] > max_len:
                max_len = p[i]
                center_index = i

        # Convert back to original string coordinates
        start = (center_index - max_len) // 2
        return s[start:start + max_len]

    def count_palindromic_substrings(s):
        """Count all palindromic substrings (LC 647)"""
        if not s:
            return 0

        p, processed = manacher_algorithm(s)
        count = 0

        for i in range(1, len(p) - 1):
            # Each palindrome of radius r contributes (r+1)/2 palindromes
            count += (p[i] + 1) // 2

        return count

    def is_palindrome_range(s, left, right):
        """Check if substring s[left:right+1] is palindrome"""
        p, processed = manacher_algorithm(s)

        # Convert to processed string coordinates
        center = left + right + 2
        radius = right - left

        return p[center] >= radius

    def all_palindromic_substrings(s):
        """Get all palindromic substrings with their positions"""
        p, processed = manacher_algorithm(s)
        palindromes = []

        for i in range(1, len(p) - 1):
            for r in range(p[i] + 1):
                # Convert back to original coordinates
                start = (i - r - 1) // 2
                end = (i + r - 1) // 2
                if start <= end:
                    palindromes.append((start, end, s[start:end + 1]))

        return palindromes

    return {
        'longest': longest_palindromic_substring,
        'count': count_palindromic_substrings,
        'is_palindrome': is_palindrome_range,
        'all_palindromes': all_palindromic_substrings
    }

模板 3:Z-Algorithm — LC 459

python
def z_algorithm(s):
    """Z-algorithm: compute Z array where Z[i] = length of longest substring
    starting from s[i] which is also a prefix of s"""

    n = len(s)
    z = [0] * n
    left = right = 0

    for i in range(1, n):
        if i <= right:
            # Use previously computed values
            z[i] = min(right - i + 1, z[i - left])

        # Try to extend match
        while i + z[i] < n and s[z[i]] == s[i + z[i]]:
            z[i] += 1

        # Update window if we extended past right boundary
        if i + z[i] - 1 > right:
            left, right = i, i + z[i] - 1

    return z

def z_algorithm_applications():
    """Applications of Z-algorithm"""

    def pattern_search_z(text, pattern):
        """Pattern searching using Z-algorithm"""
        combined = pattern + "$" + text
        z = z_algorithm(combined)
        pattern_len = len(pattern)
        matches = []

        for i in range(pattern_len + 1, len(combined)):
            if z[i] == pattern_len:
                matches.append(i - pattern_len - 1)

        return matches

    def find_all_occurrences(s, pattern):
        """Find all occurrences of pattern in string"""
        return pattern_search_z(s, pattern)

    def longest_prefix_suffix(s):
        """Find longest prefix which is also suffix"""
        z = z_algorithm(s)
        n = len(s)

        for i in range(n - 1, 0, -1):
            if z[i] == n - i:
                return s[:n - i]

        return ""

    def period_detection(s):
        """Find the period of string using Z-algorithm"""
        z = z_algorithm(s)
        n = len(s)

        for period in range(1, n):
            if period + z[period] == n:
                return period

        return n  # String has no period shorter than itself

    return {
        'search': pattern_search_z,
        'find_all': find_all_occurrences,
        'prefix_suffix': longest_prefix_suffix,
        'period': period_detection
    }

模板 5:後綴陣列建構 — LC 1044

python
def suffix_array_construction(s):
    """Construct suffix array using counting sort (O(n log n))"""

    def counting_sort(arr, key_func, max_val):
        """Stable counting sort"""
        count = [0] * (max_val + 1)
        for item in arr:
            count[key_func(item)] += 1

        for i in range(1, len(count)):
            count[i] += count[i - 1]

        result = [0] * len(arr)
        for i in range(len(arr) - 1, -1, -1):
            key = key_func(arr[i])
            count[key] -= 1
            result[count[key]] = arr[i]

        return result

    n = len(s)
    if n == 0:
        return []

    # Initial ranking based on first character
    suffixes = list(range(n))
    rank = [ord(c) for c in s]

    k = 1
    while k < n:
        # Sort by (rank[i], rank[i + k])
        def sort_key(i):
            return (rank[i], rank[i + k] if i + k < n else -1)

        # Find maximum rank for counting sort
        max_rank = max(rank) + 1

        # Sort by second key first, then by first key
        suffixes = counting_sort(suffixes, lambda i: rank[i + k] if i + k < n else -1, max_rank)
        suffixes = counting_sort(suffixes, lambda i: rank[i], max_rank)

        # Update ranks
        new_rank = [0] * n
        for i in range(1, n):
            if (rank[suffixes[i]], rank[suffixes[i] + k] if suffixes[i] + k < n else -1) == \
               (rank[suffixes[i - 1]], rank[suffixes[i - 1] + k] if suffixes[i - 1] + k < n else -1):
                new_rank[suffixes[i]] = new_rank[suffixes[i - 1]]
            else:
                new_rank[suffixes[i]] = i

        rank = new_rank
        k *= 2

    return suffixes

def lcp_array(s, suffix_array):
    """Compute LCP (Longest Common Prefix) array"""
    n = len(s)
    if n == 0:
        return []

    # Inverse of suffix array
    rank = [0] * n
    for i in range(n):
        rank[suffix_array[i]] = i

    lcp = [0] * (n - 1)
    h = 0

    for i in range(n):
        if rank[i] > 0:
            j = suffix_array[rank[i] - 1]
            while i + h < n and j + h < n and s[i + h] == s[j + h]:
                h += 1
            lcp[rank[i] - 1] = h
            if h > 0:
                h -= 1

    return lcp

模板 6:DFA/狀態機(字串驗證) — LC 65 優先度 4/5 — 高價值 — 這裡有缺口就會掉關

關鍵想法:當一個字串格式的規則全是「這個只能接在那個後面」這種糾纏在一起的條件時, 別再寫巢狀 if 了,改寫轉移表。先把每個字元歸到少數幾個字元類別, 再讓一個 state 整數在手刻的 DFA 上跑。 一趟掃完、O(1) 記憶體,而且所有規則都集中在一張看得懂的表裡。

什麼時候該拿出來用:格式驗證/斷詞(LC 65 Valid Number)這種題, 臨時拼湊的 if/else 解法正是多數人在邊界情況上翻船的地方("."、"4e+"、"3."、".9")。 這跟 KMP 底下的自動機是同一個概念(LPS 陣列本身就是一台比對自動機)— 差別只在這裡的自動機是手寫的,不是從模式推導出來的。

做法

  1. 列出字元類別(這裡是 digit、sign、dot、exp)— 其他字元一律直接拒絕。
  2. 列出狀態,每個狀態代表一種「我目前合法看過的東西」。
  3. 填表;-1 代表死狀態。
  4. 標出接受狀態;字串合法的充要條件是它停在其中之一。
java
// java
// LC 65 - Valid Number
// IDEA: hand-built DFA — classify char, follow transition table, accept only in a terminal state
// time = O(N), space = O(1)   (table is a fixed 8x4 constant)
public class ValidNumber {
    // rows = states, cols = char class {0:digit, 1:sign, 2:dot, 3:exp}, -1 = dead
    private static final int[][] DFA = {
        //  d   s   .   e
        {   2,  1,  3, -1 },  // 0 start
        {   2, -1,  3, -1 },  // 1 after leading sign
        {   2, -1,  4,  5 },  // 2 integer digits          (ACCEPT)
        {   4, -1, -1, -1 },  // 3 dot with no digit yet
        {   4, -1, -1,  5 },  // 4 fraction digits         (ACCEPT)
        {   7,  6, -1, -1 },  // 5 just saw 'e' / 'E'
        {   7, -1, -1, -1 },  // 6 sign after 'e'
        {   7, -1, -1, -1 },  // 7 exponent digits         (ACCEPT)
    };

    public boolean isNumber(String s) {
        int state = 0;
        for (char c : s.toCharArray()) {
            int cls;
            if (c >= '0' && c <= '9') cls = 0;
            else if (c == '+' || c == '-') cls = 1;
            else if (c == '.') cls = 2;
            else if (c == 'e' || c == 'E') cls = 3;
            else return false;                 // illegal character
            state = DFA[state][cls];
            if (state == -1) return false;     // illegal transition
        }
        return state == 2 || state == 4 || state == 7;   // accepting states
    }
}
python
# python
# LC 65 - Valid Number
# IDEA: hand-built DFA — classify char, follow transition table, accept only in a terminal state
# time = O(N), space = O(1)
DFA = [
    #  d   s   .   e
    [  2,  1,  3, -1],   # 0 start
    [  2, -1,  3, -1],   # 1 after leading sign
    [  2, -1,  4,  5],   # 2 integer digits          (ACCEPT)
    [  4, -1, -1, -1],   # 3 dot with no digit yet
    [  4, -1, -1,  5],   # 4 fraction digits         (ACCEPT)
    [  7,  6, -1, -1],   # 5 just saw 'e' / 'E'
    [  7, -1, -1, -1],   # 6 sign after 'e'
    [  7, -1, -1, -1],   # 7 exponent digits         (ACCEPT)
]

def isNumber(s):
    state = 0
    for c in s:
        if c.isdigit():
            cls = 0
        elif c in "+-":
            cls = 1
        elif c == ".":
            cls = 2
        elif c in "eE":
            cls = 3
        else:
            return False              # illegal character
        state = DFA[state][cls]
        if state == -1:               # illegal transition
            return False
    return state in (2, 4, 7)         # accepting states

為什麼是狀態 2/4/7、而不是別的 — 一個合法數字只有三種結尾方式:

text
"53"      0 -d-> 2                     accept (integer)
"53.5"    0 -d-> 2 -.-> 4 -d-> 4       accept (fraction)
"3."      0 -d-> 2 -.-> 4              accept ("digits then dot" is legal)
".9"      0 -.-> 3 -d-> 4              accept (dot first needs a digit after)
"."       0 -.-> 3                     REJECT (state 3 is not accepting)
"53e-9"   0 -d-> 2 -e-> 5 -s-> 6 -d-> 7  accept
"4e+"     0 -d-> 2 -e-> 5 -s-> 6       REJECT (exponent needs a digit)
"99e2.5"  ... state 7 -.-> -1          REJECT (no dot in the exponent)

面試小技巧:先在白板上把狀態圖畫出來,再抄成表。這張表讓解法可以自己被檢查 — 面試官讀一格就能驗證一條規則。

依模式分類的題目

模式比對類

題目 LC # 最佳演算法 時間複雜度 難度
Find Index of First Occurrence 28 KMP O(n + m) Medium
Repeated Substring Pattern 459 KMP/Z-Algorithm O(n) Easy
Shortest Palindrome 214 KMP + 反轉 O(n) Hard

回文類

題目 LC # 最佳演算法 時間複雜度 難度
Longest Palindromic Substring 5 Manacher O(n) Medium
Palindromic Substrings 647 Manacher O(n) Medium
Shortest Palindrome 214 Manacher/KMP O(n) Hard

進階字串類

題目 LC # 最佳演算法 時間複雜度 難度
Longest Duplicate Substring 1044 Rolling Hash + 二分搜尋 O(n log n) Hard
Distinct Echo Substrings 1316 Rolling Hash O(n²) Hard
Find All Anagrams 438 Rolling Hash O(n) Medium

LC 範例

這份文件以前解過的題目,現在各有主場;上面的技巧章節只保留真正需要它們的題目(LC 459 用 Z-algorithm、LC 1044 用後綴陣列、LC 65 用 DFA)。

題目 主場
LC 28 Find the Index of the First Occurrence in a String(KMP) string_matching_kmp_rolling_hash.md § 2-1),Python 與 Java
LC 686 Repeated String Match(先界定 haystack 長度,再搜尋) string_matching_kmp_rolling_hash.md § 1-4)
LC 5 Longest Palindromic Substring(從中心擴展) 2_pointers.md § Template 3;Manacher 是上面的模板 2
LC 1044 Longest Duplicate Substring(二分搜尋 + Rabin-Karp) string_matching_kmp_rolling_hash.md § 2-3),Python 與 Java
LC 718 Maximum Length of Repeated Subarray(同一招,用在兩個整數陣列之間) string_matching_kmp_rolling_hash.md § 1-6)

進階技巧

多模式比對

python
class AhoCorasick:
    """Aho-Corasick algorithm for multiple pattern matching"""

    def __init__(self):
        self.trie = {}
        self.failure = {}
        self.output = {}

    def add_pattern(self, pattern, pattern_id):
        """Add pattern to trie"""
        node = self.trie
        for char in pattern:
            if char not in node:
                node[char] = {}
            node = node[char]
        node['$'] = pattern_id

    def build_failure_function(self):
        """Build failure function for Aho-Corasick"""
        from collections import deque

        queue = deque()
        self.failure[id(self.trie)] = self.trie

        # Initialize first level
        for char, child in self.trie.items():
            if char != '$':
                self.failure[id(child)] = self.trie
                queue.append((child, char))

        # Build failure links using BFS
        while queue:
            current, char = queue.popleft()
            current_id = id(current)

            for next_char, next_node in current.items():
                if next_char == '$':
                    continue

                queue.append((next_node, next_char))

                # Find failure link
                failure_node = self.failure[current_id]
                while failure_node != self.trie and next_char not in failure_node:
                    failure_node = self.failure[id(failure_node)]

                if next_char in failure_node:
                    self.failure[id(next_node)] = failure_node[next_char]
                else:
                    self.failure[id(next_node)] = self.trie

    def search_all(self, text):
        """Find all pattern occurrences in text"""
        current = self.trie
        results = []

        for i, char in enumerate(text):
            # Follow failure links
            while current != self.trie and char not in current:
                current = self.failure[id(current)]

            if char in current:
                current = current[char]

                # Check for matches
                temp = current
                while temp != self.trie:
                    if '$' in temp:
                        pattern_id = temp['$']
                        results.append((i, pattern_id))
                    temp = self.failure[id(temp)]

        return results

字串雜湊的最佳化

雙重雜湊(兩個底數、兩個模數,比較這一對值)只寫在一個地方:string_matching_kmp_rolling_hash.md § 1-3)。

效能最佳化建議

演算法選擇指南

python
def choose_string_algorithm(problem_characteristics):
    """Guide for choosing optimal string algorithm"""

    if problem_characteristics['type'] == 'pattern_matching':
        if problem_characteristics['single_pattern']:
            return "KMP or Z-Algorithm"
        else:
            return "Aho-Corasick"

    elif problem_characteristics['type'] == 'palindromes':
        if problem_characteristics['all_palindromes']:
            return "Manacher's Algorithm"
        else:
            return "Expand around centers"

    elif problem_characteristics['type'] == 'substring_queries':
        if problem_characteristics['many_queries']:
            return "Rolling Hash or Suffix Array"
        else:
            return "Simple comparison"

    elif problem_characteristics['type'] == 'string_matching':
        if problem_characteristics['approximate']:
            return "Edit distance DP"
        else:
            return "KMP or Rolling Hash"

這個進階演算法真的划算嗎? 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現

面試官其實很少想看到 Manacher 或後綴陣列。他們要的是直觀的解法、寫對寫乾淨, 再加一句「如果限制真的逼我,我會這樣把它壓下來」。先給預期的答案,再把進階版當 follow-up 提出來。

題目 面試官預期的解法 進階選項 值得換嗎?
LC 28 Find First Occurrence 滑動比對,O(N·M) KMP,O(N+M) 值得 — 這題就是在考 KMP;用暴力解會被讀成「不會」
LC 5 Longest Palindromic Substring 中心擴展,O(N²) Manacher,O(N) 不值得 — 只有被要求 O(N) 才寫 Manacher。中心擴展就是被接受的答案(見 palindrome.md)
LC 647 Palindromic Substrings 中心擴展,O(N²) Manacher,O(N) 不值得 — 理由同上
LC 686 Repeated String Match b in a*k 加上界限論證 KMP 搜尋 很少需要 — 重點是界限的證明,不是比對器
LC 718 Max Length of Repeated Subarray LCS 型 DP,O(N·M) 二分搜尋 + rolling hash,O((N+M)·log N) 只在 follow-up 時 — 先 DP,再提這個
LC 1044 Longest Duplicate Substring (沒有簡單解 — O(N²) 會 TLE) 二分搜尋 + Rabin-Karp 值得 — 進階解是唯一過得了的解
LC 1316 Distinct Echo Substrings — Rolling hash 值得 — 雜湊本來就是這題預設的工具
LC 214 Shortest Palindrome 反轉 + 前綴檢查,O(N²) 在 s + '#' + rev(s) 上跑 KMP 的 prefix function 被要求 O(N) 時值得 — 見 palindrome.md
LC 10 / LC 44 Regex & Wildcard Matching 二維 DP — 不需要字串自動機 自己建 NFA/DFA 不值得 — DP 就是答案;見 dp_pattern.md、recursion_to_dp.md
LC 65 Valid Number 東拼西湊的 flag 大亂鬥 DFA 表(模板 6) 值得 — 表格版更短,而且可以證明它涵蓋了所有邊界情況

經驗法則:在下列情況才動用進階結構 — (a) 暴力解的複雜度在題目給的限制下真的會 TLE、 (b) 同一個字串要被查很多次,預處理攤提得下來,或 © 進階版其實更容易寫對(LC 65 的 DFA、LC 28 的 KMP)。 其他時候它就是個包袱 — 更多程式碼、更多 bug、更少時間講話。

鄰近的模式各自住在哪裡

下面這些屬於其他 cheatsheet,在這裡只做交叉引用,讓這頁專心處理後綴結構、自動機、雜湊和線性時間比對:

  • LC 336 Palindrome Pairs — 反轉單字的 trie/雜湊表拆分 → trie.md、palindrome.md
  • LC 208 / 211 / 212 / 472 / 648(Trie、Add & Search Words、Word Search II、Concatenated Words、Replace Words)→ trie.md
  • LC 10 / 44(Regular Expression & Wildcard Matching) — 兩個字串的布林 DP 表格 → dp_pattern.md、recursion_to_dp.md
  • LC 3 / 438(Longest Substring Without Repeating Characters、Find All Anagrams) — 視窗 + 計數,不需要雜湊 → sliding_window.md、string.md
  • LC 8 / 12 / 13 / 43 / 68 / 273 / 443(解析、格式化、大數字串) — 一般的字串操作 → string.md
  • LC 20 / 224 / 227 / 394 / 1249(括號與運算式解析) — 堆疊機,不是字串演算法 → stack.md、string.md

總結與速查

演算法複雜度比較

演算法 時間複雜度 空間複雜度 最適合的場景
KMP O(n + m) O(m) 單一模式搜尋
Manacher O(n) O(n) 所有回文
Z-Algorithm O(n) O(n) 模式比對的各種變形
Rolling Hash O(n) O(1) 攤提 多次查詢
後綴陣列 O(n log n) O(n) 複雜的字串操作

常見錯誤與建議

🚫 常見錯誤:

  • 沒處理空字串或空模式
  • 索引計算的差一錯誤
  • 只用單一雜湊,碰撞問題沒解決
  • failure function 算錯

✅ 最佳實務:

  • 一定要先驗證輸入字串
  • 用雙雜湊來抵抗碰撞
  • 預期會多次查詢時就先預處理
  • 依題目限制挑演算法
  • 用邊界情況和長字串測過

面試建議

  1. 先辨認核心需求:是模式比對、回文,還是多次查詢
  2. 挑對演算法:依時間/空間限制決定
  3. 處理邊界情況:空字串、單一字元
  4. 考慮預處理:查詢次數多的時候
  5. 實作要小心:索引管理是關鍵
  6. 測試要徹底:各種長度與模式的字串都試過

這份進階字串演算法 cheatsheet 收錄了字串處理與模式比對上最講究的那些技巧。