1つ:背景
メイン文字列(Sに置き換えられた)とパターン文字列(Pに置き換えられた)が与えられた場合、S内のPの位置を見つける必要があります。これは文字列のパターンマッチング問題です。
Knuth-Morris-Prattアルゴリズム(KMPと呼ばれる)は、この問題を解決するために一般的に使用されるアルゴリズムの1つです。このアルゴリズムは、1974年にDonald Ervin KnuthとVaughan Prattによって考案されました。同年、James H.モリスもアルゴリズムを独立して設計し、最後に3つは1977年に共同で公開されました。
:先に進む前に、ここでは二つの概念を導入する必要がある真の接頭辞と適切な接尾辞を。
上の図から、「True Prefix」はそれ自体を除く文字列のすべての先頭の組み合わせを指し、「True Suffix」はそれ自体を除く文字列のすべての末尾の組み合わせを指します。(インターネット上の多くのブログ、私が以前に書いたものを含むほとんどすべてのブログは「プレフィックス」であると言われるべきです。厳密に言えば、「真のプレフィックス」と「プレフィックス」は異なります。 !)
2:単純な文字列照合アルゴリズム
最初の遭遇文字列のパターンマッチング問題、私たちの頭の中での最初の反応は単純な文字列マッチング(いわゆるブルートフォースマッチング)で、コードは次のとおりです。
/* 字符串下标始于 0 */
int NaiveStringSearch(string S, string P)
{
int i = 0; // S 的下标
int j = 0; // P 的下标
int s_len = S.size();
int p_len = P.size();
while (i < s_len && j < p_len)
{
if (S[i] == P[j]) // 若相等,都前进一步
{
i++;
j++;
}
else // 不相等
{
i = i - j + 1;
j = 0;
}
}
if (j == p_len) // 匹配成功
return i - j;
return -1;
}
ブルートフォースマッチングの時間の複雑さは\(O(nm)\)です。ここで、\(n \)はSの長さ、\(m \)はPの長さです。明らかに、そのような時間の複雑さは私たちのニーズを満たすのが困難です。
次に、トピックを入力します:時間の複雑さを備えたKMPアルゴリズム\(Θ(n + m)\)。
3:KMP文字列照合アルゴリズム
3.1アルゴリズムの流れ
以下は、 Ruan Yifengの文字列照合のKMPアルゴリズムからのもので、わずかに変更されています。
(1)
まず、メイン文字列「BBC ABCDAB ABCDABCDABDE」の最初の文字とパターン文字列「ABCDABD」の最初の文字を比較します。BとAが一致しないため、パターン文字列は1ビット戻ります。
(2)
BとAが再び一致しないため、パターン文字列は後ろに移動します。
(3)
このように、メイン文字列が文字になるまでは、パターン文字列の最初の文字と同じです。
(4)
次に、メイン文字列とパターン文字列の次の文字を比較します。それでも同じです。
(5)
メイン文字列に文字があるまで、パターン文字列に対応する文字は同じではありません。
(6)
このとき、最も自然な反応は、パターン文字列を1ビット後方に移動し、最初から1つずつ比較することです。これは実現可能ですが、「検索位置」を比較した位置に移動して比較する必要があるため、効率は非常に悪いです。
(7)
基本的な事実は、スペースがDと一致しない場合、最初の6文字が「ABCDAB」であることはすでにわかっています。KMPアルゴリズムのアイデアは、「検索位置」を比較された位置に戻す代わりに、この既知の情報を使用しようとするが、引き続き後方に移動することであり、これにより効率が向上します。
(8)
私 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
パターン文字列 | あ | B | C | D | あ | B | D | '\ 0' |
次[i] | -1 | 0 | 0 | 0 | 0 | 1 | 2 | 0 |
これを行う方法?パターン文字列にジャンプ配列を設定することができますが、int next[]
ここで使用する限り、この配列の計算方法については後で説明します。
(9)
スペースがDと一致しないことがわかっている場合、最初の6文字の「ABCDAB」が一致します。ジャンプ配列によると、不一致時のDの次の値は2であるため、パターン文字列のインデックスが2である位置からマッチングが開始されます。
(10)
スペースはCと一致しないため、Cの次の値は0なので、パターン文字列はインデックス0から一致します。
(11)
スペースはAと一致しないため、ここでの次の値は-1です。これは、パターン文字列の最初の文字が一致しないため、直接1ビット後ろに移動することを意味します。
(12)
CとDが一致しないことがわかるまで、少しずつ比較します。次に、次のステップは、添え字2のマッチングから始まります。
(13)
パターン文字列の最後のビットが完全に一致するまで少しずつ比較して、検索を完了します。
3.2次のアレイを見つける方法
次の配列は、「真のプレフィックス」と「真のサフィックス」に基づいて解決されます。これは、同じ真のプレフィックスとサフィックスnext[i]
のP[0]...P[i - 1]
最長の長さに等しくなります(以下で説明するように、iが0の場合は無視してください)。上の例はまだ例として使用していますが、読みやすいように以下にコピーしました。
私 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
パターン文字列 | あ | B | C | D | あ | B | D | '\ 0' |
次[私] | -1 | 0 | 0 | 0 | 0 | 1 | 2 | 0 |
-
i = 0、パターン文字列の最初の文字は、として統一され
next[0] = -1
ます。 -
i = 1、前の文字列は
A
、最長かつ同じ真の接頭辞と接尾辞の長さは0、つまりnext[1] = 0
; -
i = 2、前の文字列が
AB
最も長く、同じ真の接尾辞の長さは0、つまりnext[2] = 0
; -
i = 3、前の文字列は
ABC
、最長で同じ真の接頭辞と接尾辞の長さは0、つまりnext[3] = 0
; -
i = 4、前の文字列は
ABCD
最も長く、同じ真の接頭辞と接尾辞の長さは0、つまりnext[4] = 0
; -
i = 5、前の文字列は
ABCDA
最も長く、同じ真のプレフィックスとサフィックスA
、つまりnext[5] = 1
; -
私は、文字列の前6 =
ABCDAB
最長のサフィックスで、前と同じでありAB
、すなわちnext[6] = 2
、 -
i = 7、前の文字列は
ABCDABD
、最長かつ同じ真のサフィックスの長さは0、つまりnext[7] = 0
です。
では、最長で同じ真のサフィックスの長さに基づいて不一致が発生した場合、なぜジャンプできるのでしょうか。代表的な例については、次の場合i = 6
一致していない、我々は文字列の前にその位置を知っているABCDAB
文字列注意深い観察、頭と尾持ってAB
以来、i = 6
私たちは直接リンクしない理由でDの不一致、i = 2
Cを取得して比較を続けます。1つAB
あります。これAB
はABCDAB
最長で同じ真のサフィックスであり、その長さ2はジャンプの添え字の位置にすぎません。
一部の読者は疑問を抱くかもしれません。i = 5
私が説明した方法によると、その時点で一致が失敗した場合、i = 1
比較を続けるためにその場所の文字を引き継ぐ必要がありますが、これら2つの位置の文字は同じB
です。どちらも同じなので、やって来ても無駄じゃないですか。実際、これは私が説明した問題でも、このアルゴリズムの問題でもありませんが、アルゴリズムが最適化されていません。問題については以下で詳しく説明しますが、読者がここで苦労してこれをスキップしないことをお勧めします。以下で自然に理解できます。
アイデアはとてもシンプルなので、次のステップは次のようにコードを実装することです:
/* P 为模式串,下标从 0 开始 */
void GetNext(string P, int next[])
{
int p_len = P.size();
int i = 0; // P 的下标
int j = -1;
next[0] = -1;
while (i < p_len)
{
if (j == -1 || P[i] == P[j])
{
i++;
j++;
next[i] = j;
}
else
j = next[j];
}
}
びっくりするような顔ですね。。。上記のコードは、パターン文字列の各位置のnext[]
値を解決するために使用されます。
次の具体的な分析では、コードを2つの部分に分けました。
(1):iとjの役割は何ですか?
iとjは2つの「ポインタ」のようなものです。1つずつ移動して、最長の同一の真のサフィックスを見つけます。
(2):if ... else ...ステートメントで何が行われますか?
iとjの位置が上の図のようnext[i] = j
になっていると仮定すると、位置iの場合、セクション[0、i-1]の最も長い同一の真のサフィックスは[0、j-1]と[i-j 、i-1]、つまり、2つのセクションの内容は同じです。
アルゴリズムフローによればif (P[i] == P[j])
、i++; j++; next[i] = j;
;等しくない場合j = next[j]
は、次の図を参照してください。
next[j]
[0、j-1]セクション内の同一の最も長い真のサフィックスの長さを表します。図に示すように、左側の2つの長い楕円は、最長かつ同じ真のサフィックスを表すために使用されます。つまり、2つの楕円は同じセグメントコンテンツを表します。同じように、右側には2つの同じ楕円があります。したがって、elseステートメントは、最初の楕円と4番目の楕円の同じ内容を使用して、[0、i-1]セクションの同じ真の接頭辞と接尾辞の長さを高速化します。
注意深い友人はj == -1
、ifステートメントの意味を尋ねます。まず、プログラムが実行されているとき、jは最初に-1に設定され、直接のP[i] == P[j]
判断は間違いなく境界をオーバーフローします;次に、elseステートメントj = next[j]
では、jが後退で-1の値が割り当てられている場合、jは常に後退しています(つまり、j = next[0]
)、P[i] == P[j]
境界は判断でオーバーフローします。2点をまとめると、特別な境界判断という意味です。
4:完全なコード
#include <iostream>
#include <string>
using namespace std;
/* P 为模式串,下标从 0 开始 */
void GetNext(string P, int next[])
{
int p_len = P.size();
int i = 0; // P 的下标
int j = -1;
next[0] = -1;
while (i < p_len)
{
if (j == -1 || P[i] == P[j])
{
i++;
j++;
next[i] = j;
}
else
j = next[j];
}
}
/* 在 S 中找到 P 第一次出现的位置 */
int KMP(string S, string P, int next[])
{
GetNext(P, next);
int i = 0; // S 的下标
int j = 0; // P 的下标
int s_len = S.size();
int p_len = P.size();
while (i < s_len && j < p_len) // 因为末尾 '\0' 的存在,所以不会越界
{
if (j == -1 || S[i] == P[j]) // P 的第一个字符不匹配或 S[i] == P[j]
{
i++;
j++;
}
else
j = next[j]; // 当前字符匹配失败,进行跳转
}
if (j == p_len) // 匹配成功
return i - j;
return -1;
}
int main()
{
int next[100] = { 0 };
cout << KMP("bbc abcdab abcdabcdabde", "abcdabd", next) << endl; // 15
return 0;
}
5:KMP最適化
私 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
パターン文字列 | あ | B | C | D | あ | B | D | '\ 0' |
次[i] | -1 | 0 | 0 | 0 | 0 | 1 | 2 | 0 |
例として3.2の表(上記をコピー)を取り上げi = 5
ます。そのときに一致が失敗した場合、3.2のコードに従ってi = 1
、その場所の文字を引き継いで比較を続行する必要がありますが、これら2つの位置の文字は同じですB
。同じなので入手しても無駄じゃないですか?これは3.2で説明しましたが、これはKMPが最適化されていないためです。この問題を解決するためにどのように書き直すことができますか?とても簡単です。
/* P 为模式串,下标从 0 开始 */
void GetNextval(string P, int nextval[])
{
int p_len = P.size();
int i = 0; // P 的下标
int j = -1;
nextval[0] = -1;
while (i < p_len)
{
if (j == -1 || P[i] == P[j])
{
i++;
j++;
if (P[i] != P[j])
nextval[i] = j;
else
nextval[i] = nextval[j]; // 既然相同就继续往前找真前缀
}
else
j = nextval[j];
}
}
6:参照
- Yan Weimin。データ構造(C言語バージョン)
- Ruan Yifeng。文字列マッチングのためのKMPアルゴリズム
セブン:謝辞
-この記事は、シニアEthsonLiuの助けに特に感謝しています!