樸素字串比對(naive string matching)
想像你把要找的字詞做成一張小鏤空板,沿著頁面一次一個字元地滑過去,每停一處就檢查板子底下露出的字母是不是拼出你的字。這就是樸素字串比對的精髓:把模式對著文字試遍每一種可能的對齊位置,並逐一直接檢驗。這是任何人都會最先想到的方法,而且它永遠正確——只是不見得永遠快。
具體來說,對每一個位移 s 從 0 到 n-m,把模式 P 對齊到文字位置 s 起始,然後比較 P[0] 與 T[s]、P[1] 與 T[s+1],依此類推。若全部 m 個字元都吻合,就把 s 記為一次匹配。一旦有任何字元不合,就放棄這個位移,向右滑一步(s 變成 s+1),並從模式的開頭重新開始比較。要試的位移約有 n-m+1 個,每個最多花 m 次比較,得到最壞情況 O((n-m+1)*m),通常寫成 O(n*m)。痛點情況是像 "aaaa...a" 這樣的文字配上模式 "aaa...ab":在每個位移你都幾乎比完整個 P 才碰到最後的不匹配,幾乎所有工作都白費了。
樸素比對是衡量那些巧妙演算法時誠實的基準,而它的缺陷正是那些演算法所教的功課。在一次部分匹配失敗之後,樸素比對把它剛學到的一切全部丟棄,重新讀它早已看過的字元。Knuth-Morris-Pratt 演算法、Z 演算法和 Boyer-Moore 之所以快,正是因為它們記住那份資訊,永不把同一個文字字元重新檢查超過常數次。不過,對短模式或短文字而言,樸素比對簡單、對快取友善,在實務上往往完全夠用——那個 O(n*m) 的最壞情況在自然語言文字上很少真的發生。
T = "abcabd",P = "abd"。位移 0:a=a、b=b、c 對 d 不合——滑動。位移 1:b 對 a 不合——滑動。位移 2:c 對 a 不合——滑動。位移 3:a=a、b=b、d=d——在 3 處匹配。注意在位移 0 我們已讀過 'abc',到位移 3 卻又重讀了同樣那些區域;正是這種重讀,被更聰明的方法消除掉了。
每次不匹配都讓模式從第一個字元重來——這正是浪費掉的 O(n*m) 工作的來源。
最壞情況 O(n*m) 確實存在,但屬病態情形;在一般文字上,不匹配通常發生在頭一兩個字元,所以樸素比對往往表現得像 O(n)。若你的模式和文字都很短,不必過度設計。