- 物理機リージョン/AZ
- ハイパーバイザVM exit で捕まえる
- VMゲストOS
- コンテナnamespace / cgroup
- プロセスアプリ / DB今ここ
1つのプロセスの中で、長い文字列から短い文字列を探す処理です。今回は探すパターン自身の構造を使います。
まず結論
素朴な文字列照合は、食い違うたびに本文を戻って読み直します。KMP 法はパターンの「頭と尻の重なり」を前もって表(失敗関数)にしておき、本文を一度も戻りません。
使っている構造は本文の性質ではなく、パターン自身の中の繰り返しです。本文 "A"*n+"B" での実測では、素朴法が約 n×11 回比べるところを、KMP は 2n 未満で済みました。
Python 3.14 の str.find や in 演算子(部分文字列の検索)の C 実装は、どんな手法を使っているでしょうか。
当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。
素朴法は、一致した分を捨てて戻る
本文の中からパターンを探す一番素直な方法は、本文の各位置を開始点にして、パターンを先頭から1文字ずつ比べることです。食い違ったら開始点を1つずらし、また先頭から比べます。
困るのは一致しかけてから食い違うときです。パターン AAAAAAAAAAB を本文 AAAA…AB で探すと、どの開始点でも A を10文字一致させたあと B で食い違います。そして開始点を1つずらすと、いま一致を確かめた9文字をもう一度読み直します。
ここで捨てているのは「直前に何文字一致したか」という情報です。食い違った時点で、直前の本文がパターンの頭の何文字と同じだったかは分かっています。本文を読み直さなくても、パターンだけを見れば次にどこから比べればよいかが決まります。
KMP 法はこれを前もって表にします。パターンの各位置について「そこまでの部分の、頭と尻が一致する最長の長さ」を並べたものが失敗関数です。照合中に守る約束(不変式)は1つです。
- ・ABABA の尻3文字 ABA は、パターンの頭3文字と同じ
- ・この「頭と尻の重なり」はパターンだけで決まり、本文を見なくても前もって計算できる
失敗関数の表と、比較回数の実測
ABABAC の失敗関数を計算すると次のとおりです(Python 3.14.6 / macOS 26.5.2 arm64、2026-10-07 実行)。
prefix function of ABABAC:i : [0, 1, 2, 3, 4, 5]P[i]: ['A', 'B', 'A', 'B', 'A', 'C']pi : [0, 0, 1, 2, 3, 0]
位置4までの ABABA は、頭の ABA と尻の ABA が重なるので値が3です。最後の C は頭の A と合わないので0に戻ります。
次に、素朴法が苦手な本文 "A"*n+"B" とパターン "A"*10+"B" で、文字を比べた回数を数えました。KMP の列は走査中の比較で、表を作る比較(19回、n に依らない)は含みません。
n naive_cmp kmp_cmp(scan) kmp_table_cmp 2n kmp<=2n found_at100 1001 191 19 202 True [90]1000 10901 1991 19 2002 True [990]10000 109901 19991 19 20002 True [9990]
素朴法は n が10倍になるたびに約10倍、1か所あたり11回です。 これはパターン長 m に比例する分で、全体は約 n×m 回です。KMP は 2n 未満に収まり、n に比例して増えるだけです。 見つけた位置は3つとも str.find の結果と一致することを確かめてあります。
ここで線を引いておきます。「2n 以下」は本教材の実測と下の説明であって、KMP の原論文の文言として引くものではありません(論文本文は取得できていません)。出版社の抄録が書いているのは次の範囲です。
An algorithm is presented which finds all occurrences of one given string within another, in running time proportional to the sum of the lengths of the strings.
パターン ABABAC の失敗関数は [0, 0, 1, 2, 3, 0] です。本文と ABABA の5文字まで一致し、6文字目で食い違いました。KMP は次にどうしますか。
使っているのは、パターン自身の繰り返し
なぜ 2n 未満に収まるのかは、一致済みの長さ q の増減で説明できます。q が増えるのは本文を1文字読むときに最大1だけです。失敗関数で戻るときは必ず減ります。0より下には行かないので、戻った総量は進んだ総量を超えません。 1回ごとの戻りが長い場面はあっても、合計は本文長の2倍で抑えられます。第11期の『ならし計算量は、平均ではない』で見た「合計を数える」測り方と同じです。
KMP が前提にしているのは本文の性質ではなく、パターンの中の繰り返しです。だから本文が意地悪でも崩れません。逆に、パターンに頭と尻の重なりがなければ失敗関数は全部0で、得をする場面もありません。本文が1文字目ですぐ食い違うなら素朴法も各開始点で1回で終わります。KMP が直しているのは「一致しかけては外れる」場面だけです。
対比例 — 窓のハッシュを転がす Rabin-Karp 法
別の構造で解くのが Rabin-Karp 法です。本文の長さ m の窓ごとにハッシュ値を作り、パターンのハッシュと比べます。窓を1文字ずらすときは、出ていく1文字と入ってくる1文字の分だけを直して次の窓のハッシュを得ます(ローリングハッシュ)。m 文字を読み直しません。
ハッシュが違えば確実に別の文字列で、同じなら文字を比べて確かめます。違う文字列が同じハッシュになる(衝突)と、確かめが無駄になります。論文の題 "Efficient randomized pattern-matching algorithms" が示すとおり、乱数を使う側の手法です。ハッシュの選び方に乱数を混ぜれば、入力を作る側は衝突を前もって狙えません。速さは最悪ではなく期待値で語られる——第3回で見た期待値と同じ型です。
実際の標準ライブラリはどうか。CPython 3.14 の Objects/stringlib/fastsearch.h の冒頭です(2026-10-07 取得)。
fast search/count implementation, based on a mix between boyer-moore and horspool, with a few more bells and whistles on the top.
If the strings are long enough, use Crochemore and Perrin's Two-Way algorithm, which has worst-case O(n) runtime and best-case O(n/k).
KMP そのものではありません。 ファイル内に KMP・Knuth・Morris の語はありません。Two-Way は、コメントの言う O(m) の準備(startup cost)を先に払って最悪を線形に抑える点で、KMP と同じ発想の別手法です。どちらを使うかは長さで切り替えます(n は本文長、m はパターン長)。
if (n < 2500 || (m < 100 && n < 30000) || m < 6) {
return STRINGLIB(default_find)(s, n, p, m, maxcount, mode);
}
else if ((m >> 2) * 3 < (n >> 2)) {
/* 33% threshold, but don't overflow. */
短いうちは default_find(boyer-moore と horspool の混合)、条件を超えると _two_way_find、どちらでもなければ途中で Two-Way へ移る adaptive_find です。この分岐は mode != FAST_RSEARCH(逆方向の検索でない)ときだけで、rfind は Two-Way を使いません。導入は 3.10 で、What's New はこう書いています。
Substring search functions such as
str1 in str2andstr2.find(str1)now sometimes use Crochemore & Perrin’s “Two-Way” string searching algorithm to avoid quadratic behavior on long strings. (Contributed by Dennis Sweeney in bpo-41972)
Rabin-Karp 法で、窓のハッシュがパターンのハッシュと一致したのに、さらに文字を1つずつ比べるのはなぜですか。
KMP 法
パターンだけから失敗関数を作り、食い違っても本文の位置を戻しません。前提はパターン側にあるので、どんな本文でも走査の比較は本文長の2倍に収まります。乱数は使いません。
Rabin-Karp 法
窓のハッシュを1文字ずつ更新して比べ、一致したときだけ文字を確かめます。前提はハッシュが衝突しにくいことで、乱数を混ぜて入力側から狙えなくし、期待値で速さを言います。
攻撃者が本文を自由に作れるサービスで部分文字列を探します。KMP と Rabin-Karp のどちらの前提が、入力によって崩されにくいでしょうか。
それぞれの速さが「本文」「パターン」「乱数」のどれに依存しているかを書き出してください。
この章のまとめ
- 素朴法は、一致しかけて外れるたびに本文を戻って読み直す。
"A"*n+"B"とパターン"A"*10+"B"では約 n×11 回(n=10000 で 109,901 回)。 - KMP はパターンの「頭と尻の重なり」を失敗関数にしておき、本文を戻らない。
ABABACの失敗関数は [0, 0, 1, 2, 3, 0]。走査の比較は 2n 未満(n=10000 で 19,991 回)。 - 線形に収まる理由は合計で数えると分かる。 一致済みの長さは1文字ごとに最大1しか増えず、戻る総量は進んだ総量を超えない。
- 使っている構造はパターン自身の繰り返し。 Rabin-Karp はハッシュの更新と衝突の少なさを使い、速さを期待値で言う。
- CPython の
in/findは KMP ではない。 boyer-moore と horspool の混合が基本で、長いときは Two-Way(3.10 から)。切替は長さの条件で決まり、rfindは Two-Way を使わない。 - 既存クイズでは、「アルゴリズムとデータ構造 III(発展編)」の algorithm-3-1(KMP)・algorithm-3-15(ローリングハッシュ)がこの章の結論の一行版です。
- CLRS(『アルゴリズムイントロダクション』)の目次や索引で string matching(文字列照合) を探すと、この回の内容に対応する章や節が見つかります。章の題と番号は版で変わるので、手元の目次で確かめてください。
自分の現場で確かめる
読んだだけでは身につきません。手元の環境で実際に見てみてください。
理解度チェック
読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。
本文 "A"*n+"B"、パターン "A"*10+"B" で比較回数を数えました(n=10000)。結果として妥当な組はどれですか。
用語集(4語)
- 失敗関数(prefix function)
- パターンの各位置 i について、先頭から i 文字目までの部分の「頭と尻が一致する最長の長さ」(全体そのものは除く)を並べた表。KMP は食い違ったとき、この値の分だけ一致済みとして照合を続ける。
- 素朴法
- 本文の各開始位置でパターンを先頭から比べ、食い違ったら開始位置を1つずらしてやり直す方法。一致しかけてから食い違う本文では、同じ文字を何度も読み直す。
- ローリングハッシュ
- 長さ m の窓を1文字ずらすとき、出ていく1文字と入ってくる1文字の分だけを直して次の窓のハッシュを得る作り方。Rabin-Karp 法が使う。
- Two-Way
- Crochemore と Perrin の文字列照合手法。CPython は 3.10 から、長い文字列の部分文字列検索で使う。fastsearch.h のコメントは worst-case O(n) と書く。
出典(一次ソース)
- Knuth, Morris, Pratt — Fast Pattern Matching in Strings(SIAM J. Comput. 6(2), 323–350, 1977) ↗
- Karp, Rabin — Efficient randomized pattern-matching algorithms(IBM J. Res. Dev. 31(2), 249–260, 1987) ↗
- CPython 3.14 — Objects/stringlib/fastsearch.h ↗
- What's New In Python 3.10 — Optimizations ↗
- CPython issue #86138(bpo-41972)— bytes.find consistently hangs in a particular scenario ↗