cv.cm

コーディング · アルゴリズム

KMP法(文字列検索)とは?仕組み・コード・計算量をわかりやすく解説

約8分で読めます

結論

クヌース–モリス–プラット法(KMP法)は、長さ n のテキストの中から、長さ m のパターンの出現位置をすべて O(n + m) 時間で見つけるアルゴリズムです。まずパターンについてプレフィックス関数(失敗関数テーブルとも呼ばれます)を作ります。これは各位置について、接尾辞にもなっている最長の真の接頭辞の長さを表します。不一致が起きると、KMP法はこの表を使って、テキスト側を後戻りすることなくパターンをずらします。

正規表現テストを開く

KMP法が解く問題

文字列検索は、短いパターンが長いテキストのどこに現れるかを求める問題です。素朴な方法では、すべての開始位置を試して文字を1つずつ比較します。通常のテキストでは高速ですが、繰り返しの多い入力では遅くなります。aaaaaaaaaa から aaaab を探すと、各位置でパターンのほぼ全体を比較することになり、計算量は O(n × m) です。

KMP法は、不一致の後にテキストの文字を読み直すことがありません。ドナルド・クヌース(Donald Knuth)、ジェームズ・H・モリス(James H. Morris)、ヴォーン・プラット(Vaughan Pratt)が、1977年に "Fast Pattern Matching in Strings" で発表しました。

考え方を言葉で説明すると

パターン ABABC を探していて、ABAB まで一致し、次のテキストの文字が C ではなかったとします。いま一致した ABAB は AB で終わっており、これはパターンの先頭と同じです。つまり、次の試行はすでに2文字が一致した状態から始まるとわかっているので、最初からやり直さず、パターンの位置2から比較を続けられます。

プレフィックス関数は、これらの数を保存します。pi[i] は、pattern[0..i] の真の接頭辞(真とは、文字列全体ではないという意味です)のうち、その接尾辞にもなっている最長のものの長さです。

i0123456
pattern[i]ABABACA
pi[i]0012301

たとえば、ABABA は ABA で始まり ABA で終わるので pi[4] = 3 です。ABABAC の接頭辞で C で終わるものはないので pi[5] = 0 です。

具体例で一歩ずつ確認

テキスト ABABABC から、パターン ABABC(プレフィックス関数は 0, 0, 1, 2, 0)を探します。変数 k は、現在一致しているパターンの文字数を数えます。

テキストの位置テキストの文字変更前の k動作変更後の k
0–3A B A B04文字連続で一致4
4A4pattern[4] = C と不一致:k = pi[3] = 2 に設定し、その後 A が pattern[2] と一致3
5B3pattern[3] と一致4
6C4pattern[4] と一致:6 で終わる完全一致なので、開始位置は 6 − 5 + 1 = 25 → pi[4] = 0

テキスト側のポインタは7回前進し、一度も後戻りしませんでした。素朴な方法では位置1からやり直し、いくつかの文字をもう一度比較することになります。

Pythonでの実装

def prefix_function(p):
    pi = [0] * len(p)
    k = 0
    for i in range(1, len(p)):
        while k > 0 and p[i] != p[k]:
            k = pi[k - 1]          # fall back to a shorter border
        if p[i] == p[k]:
            k += 1
        pi[i] = k
    return pi

def kmp_search(text, pattern):
    """Return the start index of every occurrence (overlaps included)."""
    if not pattern:
        return []
    pi, k, hits = prefix_function(pattern), 0, []
    for i, ch in enumerate(text):
        while k > 0 and ch != pattern[k]:
            k = pi[k - 1]
        if ch == pattern[k]:
            k += 1
        if k == len(pattern):
            hits.append(i - k + 1)
            k = pi[k - 1]          # keep going to find overlapping matches
    return hits

print(prefix_function("ABABACA"))      # [0, 0, 1, 2, 3, 0, 1]
print(kmp_search("ABABABC", "ABABC"))  # [2]
print(kmp_search("AAAAA", "AA"))       # [0, 1, 2, 3]

時間計算量と空間計算量

段階時間空間理由
プレフィックス関数の構築O(m)O(m)k は1ステップごとに高々1しか増えず、フォールバックのたびに減るので、フォールバックの合計は高々 m 回です。
検索O(n)追加で O(1)テキストについても同じ議論で、増加は高々 n 回なので、フォールバックも高々 n 回です。
合計O(n + m)O(m)最悪の場合も最良の場合と定数倍しか違わず、悪い入力は存在しません。

素朴なアルゴリズムは最悪 O(n × m) ですが、自然言語のテキストでは O(n) に近いことが多く、KMP法の利点が目立つのは主に繰り返しの多いデータです。

バリエーションとよくある間違い

  • フォールバックでの1つのずれ:不一致の後は k = pi[k] ではなく k = pi[k - 1] にします。pi は、最後に一致した位置で添字を付けるためです。
  • フォールバックに while ではなく if を使う:文字が一致するまでに、連続して何度かフォールバックが必要になることがあります。
  • 完全一致の後に k をリセットし忘れる:重なる一致も見つけるには k = pi[k - 1] に、重ならない一致だけなら k = 0 に設定します。
  • 連結のテクニック:pattern + separator + text のプレフィックス関数を計算します。区切り文字には、どちらにも現れない文字を使います。値が m に等しい位置はすべて、一致の終端です。
  • Zアルゴリズムは、これとよく似た配列を計算し、同じく O(n + m) で検索できます。

使う場面と実世界での用途

アルゴリズム最適な用途
KMP法パターンが1つの場合に線形時間が保証される。後戻りしないので、ストリーミングされるテキストにも向く。
Boyer-Moore法とその変種大きなアルファベット上の長いパターン。多くの文字を読み飛ばせることが多く、grepなどのツールで使われる。
Rabin-Karp法同じ長さの多数のパターン、またはローリングハッシュを使った盗用検出。
Aho-Corasick法多数のパターンを同時に検索する。KMP法の失敗リンクをトライ木に一般化したもの。
接尾辞配列または接尾辞オートマトン同じ固定のテキストに対する、多数の異なるクエリ。
  • コーディング面接の問題:strStr() の実装、最短の回文(s + "#" + reversed(s) のプレフィックス関数)、繰り返し部分文字列パターン、文字列の最小周期(m - pi[m - 1])。
  • 実世界:DNA配列やネットワークパケットのストリームの検索など、最悪時の線形時間が重要な場面。

よくある質問

KMP法のプレフィックス関数(失敗関数テーブル)とは何ですか?

パターンの各位置 i について、pattern[0..i] の真の接頭辞のうち、その接尾辞にもなっている最長のものの長さを保存します。不一致が起きたとき、KMP法はこれを使って、まだ何文字が一致しているかを知ります。

KMP法の時間計算量はどれくらいですか?

O(n + m) です。プレフィックス関数の構築に O(m)、テキストの走査に O(n) かかります。最悪の場合でも線形です。

KMP法はPythonのstr.findより速いですか?

いいえ。str.find は最適化された検索アルゴリズムでCで実装されており、実際にはずっと高速です。KMP法の価値は、線形時間が保証されること、学習に向くこと、文字列以外の系列にも使えることにあります。

KMP法は重なる一致も見つけられますか?

はい。完全一致の後に、0 ではなく k = pi[k - 1] に設定すれば見つけられます。たとえば AAAAA の中の AA は、位置 0、1、2、3 で見つかります。

コーディング講座の一覧