1977-Boyer-Moore 字串搜尋演算法#
案件摘要#
1977 年,Robert Boyer 與 J Strother Moore 在《Communications of the ACM》發表〈A Fast String Searching Algorithm〉,揭開一樁「搜尋可以比線性更快」的懸案。同年的 KMP 演算法從左往右比對、保證線性時間;Boyer-Moore 卻反其道而行,從模式串的右端開始比對。破案時刻在於:失配不是壞事,而是情報——它告訴你模式串可以安全地滑多遠,讓主串指標大步跳躍,達到實務上的亞線性(sublinear)速度。
前因 -- 為什麼會有這個案子#
- 樸素演算法每次失配只前進一字元,最壞情況 \(O(nm)\) ,在長文件上慢得難以忍受。
- 文字編輯器、編譯器掃描與 grep 的前身工具,需要的是「實務上快」;KMP(1977 同年發表)雖保證 \(O(n+m)\) ,但每個字元至少比對一次,實務常數不小、平均速度不見得快。
- Boyer 當時在德州大學奧斯汀分校研究定理證明器(NQTHM 前身),需要快速字串搜尋,於是從被眾人忽略的「比對順序」角度重新偵查。
線索與推理 -- 數學式、程式、理論#
線索一:從右往左比對#
模式串 \(P[1..m]\) 與主串 \(T[1..n]\) 對齊後,先比 \(P[m]\) 與窗口最右端 \(T[i]\) 。若失配,情報立刻到手: \(T[i]\) 不在窗口內,接下來有大範圍可以排除。
線索二:壞字元規則(bad character rule)#
失配時,把模式串滑到主串中該「壞字元」 \(c\) 在模式串內最後出現的位置對齊;若 \(c\) 不在 \(P\) 中,直接滑過整個窗口:
線索三:好後綴規則(good suffix rule)#
比對到右端某點失配時,右端已匹配的後綴 \(u\) 是重要線索:找出 \(u\) (或與之邊框相符的 \(P\) 前綴)在模式串中的下一次出現位置並對齊:
實際滑動量取兩規則較大者: \(\text{shift} = \max(\text{shift}_{bc},\ \text{shift}_{gs})\) 。
線索四:亞線性的由來#
每個窗口至少比對一個字元、至多跳 \(m\) 格,平均而言主串指標以 \(O(n/m)\) 量級前進——主串越長,相對越快,這正是亞線性的破案時刻。原始版在週期性文字(如 \(T = a^n\) 、 \(P = a^m\) )上會退化為 \(O(nm)\) ;Galil(1979)加入「記住上一輪已匹配段、只比對新增部分」的規則,證得最壞情況線性 \(O(n+m)\) 。
程式偵查:Python 實測比對次數#
def build_gs(pat):
m, gs = len(pat), []
for k in range(m): # 在索引 k 失配,已匹配後綴 u = pat[k+1:]
u = pat[k + 1:]
if not u:
gs.append(1); continue
best = m
for d in range(1, m + 1): # 情況一:u 在模式串中的另一次出現
if k + 1 - d >= 0 and pat[k + 1 - d:m - d] == u:
best = d; break
else: # 情況二:P 的前綴與 u 的後綴相符(邊框)
for l in range(len(u) - 1, 0, -1):
if pat[:l] == u[len(u) - l:]:
best = m - l; break
gs.append(best)
return gs
def boyer_moore(text, pat):
n, m = len(text), len(pat)
last = {c: j for j, c in enumerate(pat)} # 壞字元表
gs = build_gs(pat) # 好後綴表
count, s = 0, 0
while s <= n - m:
j = m - 1
while j >= 0 and pat[j] == text[s + j]:
j -= 1; count += 1
if j < 0:
return s, count
count += 1
bc = max(1, j - last.get(text[s + j], -1))
s += max(bc, gs[j])
return -1, count
def naive_count(text, pat):
n, m = len(text), len(pat)
count = 0
for s in range(n - m + 1):
j = 0
while j < m and text[s + j] == pat[j]:
j += 1; count += 1
count += 1
if j == m:
return s, count
return -1, count
if __name__ == "__main__":
t = "AB" * 5000 + "FINDME" + "XY" * 5000 # 主串約 2 萬字元
s1, c1 = boyer_moore(t, "FINDME")
s2, c2 = naive_count(t, "FINDME")
print(f"Boyer-Moore: 位置 {s1}, 比對 {c1} 次")
print(f"樸素演算法: 位置 {s2}, 比對 {c2} 次")
實測結果:Boyer-Moore 只需約 6-20 次比對,樸素演算法需上千次——亞線性當場現形。
結案 -- 後果與影響#
- Boyer-Moore 成為許多系統 grep 與文字編輯器搜尋的基礎或預設實作之一;「實務亞線性」第一次被清楚示範,演算法評估從純理論轉向理論與實務並重。
- KMP(理論線性、每字元必比)與 Boyer-Moore(實務亞線性、指標跳躍)成為字串搜尋的雙璧對照組。
- Horspool(1980)提出簡化版 Boyer-Moore-Horspool,只留壞字元規則,表格小、速度快,廣泛用於實務;Galil(1979)的修補證明最壞線性,補齊理論拼圖。
- Boyer 本人在定理證明器(與 Moore 合作的 NQTHM 歸納證明系統)上的工作使他獲 1988 年圖靈獎——本案只是他偵探生涯的副線。
關鍵人物與文獻(條列,含真實文獻書目)#
- Robert S. Boyer, J Strother Moore (1977). A Fast String Searching Algorithm. Communications of the ACM, 20(10), 762–772.
- Donald E. Knuth, James H. Morris, Jr., Vaughan R. Pratt (1977). Fast Pattern Matching in Strings. SIAM Journal on Computing, 6(2), 323–350.
- Zvi Galil (1979). On Improving the Worst Case Running Time of the Boyer-Moore String Matching Algorithm. SIAM Journal on Computing, 8(4), 612–631.
- R. Nigel Horspool (1980). Practical Fast Searching in Strings. Software: Practice and Experience, 10(6), 501–506.
- Robert S. Boyer, J Strother Moore (1979). A Computational Logic. Academic Press(NQTHM 定理證明器理論基礎).