KMPアルゴリズムとは何ですか:
KMPは、DEKnuth、JHMorris、VRPrattの3頭の大きな牛によって同時に発見されました。それらの最初のものは「コンピュータプログラミングの芸術」の著者です!!
KMPアルゴリズムによって解決される問題は、文字列(メイン文字列とも呼ばれます)内のパターンを見つけることです。簡単に言えば、私たちが通常言うキーワード検索です。パターン文字列はキーワード(以下Pと呼びます)です。メイン文字列(以下Tと呼びます)にある場合はその特定の位置を返し、そうでない場合は-1(一般的に使用される手段)を返します。
まず、この問題について非常に簡単な考えがあります。左から右に1つずつ一致させます。プロセスで一致しない文字がある場合は、戻ってパターン文字列を右に移動します。これについて何がそんなに難しいのですか?
次のように初期化できます。
その後、iポインタが指す文字がjポインタが指す文字と一致しているかどうかを比較するだけで済みます。それらが一貫している場合は、以下に示すように、一貫していない場合は後方に移動します。
AとEが等しくない場合は、iポインタを最初の位置に戻し(添え字が0から始まると仮定)、jをパターン文字列の0番目の位置に移動して、次の手順を再開します。
この考えに基づいて、次のプログラムを取得できます。
/**
* 暴力破解法
* @param ts 主串
* @param ps 模式串
* @return 如果找到,返回在主串中第一个字符出现的下标,否则为-1
*/
public static int bf(String ts, String ps) {
char[] t = ts.toCharArray();
char[] p = ps.toCharArray();
int i = 0; // 主串的位置
int j = 0; // 模式串的位置
while (i < t.length && j < p.length) {
if (t[i] == p[j]) {
// 当两个字符相同,就比较下一个
i++;
j++;
} else {
i = i - j + 1; // 一旦不匹配,i后退
j = 0; // j归0
}
}
if (j == p.length) {
return i - j;
} else {
return -1;
}
}
上記のプログラムは問題ありませんが、十分ではありません。
人工的に検索した場合、最初のAを除いて、メインストリングの一致する失敗位置の前にAがないため、最初の場所に戻ることは絶対にありません。メインストリングの前にAが1つしかないことがわかるのはなぜですか。 ?最初の3文字が一致していることはすでにわかっているからです。(これは非常に重要です)。過去の移動は間違いなく一致しません!以下に示すように、私は移動できません。jを移動するだけでよいという考えがあります。
上記の状況はまだ比較的理想的であり、せいぜいもう一度比較します。しかし、メイン文字列「SSSSSSSSSSSSSA」で「SSSSB」を検索すると、最後の文字列が比較されるまで一致しないことがわかります。その後、バックトラックすると、これの効率は明らかに最低です。
大きな牛は「ブルートフォースクラッキング」の非効率的な方法に耐えられなかったので、3人はKMPアルゴリズムを開発しました。考え方は上で見たものと同じです。「部分的に一致した有効な情報を使用して、iポインターがバックトラックしないようにし、jポインターを変更することで、パターン文字列を可能な限り有効な位置に移動します。」
したがって、KMP全体のポイントは、特定の文字がメインストリングと一致しない場合、jポインターをどこに移動するかを知る必要があるということです。
次に、jの移動法則を自分で発見しましょう。
図に示すように、CとDが一致しない場合、jをどこに移動しますか?明らかにナンバーワン。どうして?フロントAは同じなので:
同じ状況が次の図に示されています。
前の2つの文字が同じであるため、jポインターを2番目の位置に移動できます。
この時点で、おそらく手がかりを見ることができます。一致が失敗すると、jが移動する次の位置kになります。そのような特性があります:最初のk文字はjの前の最後のk文字と同じです。
数式を使ってこのように表現すると
P[0 ~ k-1] == P[j-k ~ j-1]
これは非常に重要です。覚えにくい場合は、次の図から理解できます。
これを理解すると、jを位置kに直接移動できる理由を理解できるはずです。
理由:
当T[i] != P[j]时
有T[i-j ~ i-1] == P[0 ~ j-1]
由P[0 ~ k-1] == P[j-k ~ j-1]
必然:T[i-k ~ i-1] == P[0 ~ k-1]
公式は退屈です、あなたはそれを読んで理解することができます、あなたはそれを覚える必要はありません。
この段落は、前のk文字を比較せずにjをkに直接移動できる理由を証明するためのものです。
さて、次のステップはポイントです。この(これらの)kをどのように見つけるのですか?Pの各位置で不一致が発生する可能性があるため、つまり、各位置jに対応するkを計算する必要があるため、保存する次の配列next [j] = kを使用します。これは、T [i]!= P [j]の場合、jポインタの次の位置。
多くの教科書やブログ投稿は、この場所ではかなり曖昧であるか、まったく言及されていないか、コードの一部を投稿しているのですが、なぜ彼らはそれを求めるのですか?どうすればこれを求めることができますか?はっきりしていません。そして、これがまさにアルゴリズム全体の最も重要な部分です。
public static int[] getNext(String ps) {
char[] p = ps.toCharArray();
int[] next = new int[p.length];
next[0] = -1;
int j = 0;
int k = -1;
while (j < p.length - 1) {
if (k == -1 || p[j] == p[k]) {
next[++j] = ++k;
} else {
k = next[k];
}
}
return next;
}
次の配列を見つけるためのこのバージョンのアルゴリズムは、最も広く普及しているはずであり、コードは非常に簡潔です。しかし、それは本当に紛らわしいです。この計算の根拠は何ですか?
さて、これはさておき、私たち自身のアイデアを導き出しましょう。next[j](つまり、k)の値は、P [j]!= T [i]の場合、jポインタが意味することを常に覚えておく必要があります。次に、位置を移動します。
最初のものを見てみましょう。jが0の場合、この時点で一致するものがない場合はどうなりますか?
上の写真の場合、jはすでに左端にあり、移動できません。このとき、iポインタは後方に移動するはずです。したがって、next [0] = -1があります。このコードの初期化です。
jが1の場合はどうなりますか?
明らかに、jポインタは0の位置に戻す必要があります。目の前にこの場所しかないので~~~
以下が最も重要です。下の図を参照してください。
これら2つの図を注意深く比較してください。
ルールが見つかりました:
当P[k] == P[j]时,
有next[j+1] == next[j] + 1
実際、これは証明することができます:
因为在P[j]之前已经有P[0 ~ k-1] == p[j-k ~ j-1]。(next[j] == k)
这时候现有P[k] == P[j],我们是不是可以得到P[0 ~ k-1] + P[k] == p[j-k ~ j-1] + P[j]。
即:P[0 ~ k] == P[j-k ~ j],即next[j+1] == k + 1 == next[j] + 1。
ここでの式はあまりわかりにくいですが、写真を見ればわかりやすくなります。
P [k]!= P [j]の場合はどうなりますか?たとえば、次の図に示すように:
この場合、コードを見ると、次の文になっているはずです。k= next [k];なぜこのようになっているのですか?以下をご覧ください。
これで、k = next [k]である理由がわかります。上記の例のように、最長のサフィックス文字列[A、B、A、B]は見つかりませんが、[A、B]や[B]などのプレフィックス文字列は引き続き見つかります。したがって、このプロセスは、Cがメインストリングと異なる場合(つまり、kの位置が異なる場合)、ストリング[A、B、A、C]を配置しているように見えます。もちろん、ポインターはnext [k]に移動します。 。
次の配列では、すべてが簡単です。KMPアルゴリズムを記述できます。
public static int KMP(String ts, String ps) {
char[] t = ts.toCharArray();
char[] p = ps.toCharArray();
int i = 0; // 主串的位置
int j = 0; // 模式串的位置
int[] next = getNext(ps);
while (i < t.length && j < p.length) {
if (j == -1 || t[i] == p[j]) {
// 当j为-1时,要移动的是i,当然j也要归0
i++;
j++;
} else {
// i不需要回溯了
// i = i - j + 1;
j = next[j]; // j回到指定位置
}
}
if (j == p.length) {
return i - j;
} else {
return -1;
}
}
ブルートフォースクラッキングと比較して、4箇所が変更されました。重要な点は、私が後戻りする必要がないということです。
最後に、上記のアルゴリズムの欠陥を見てみましょう。最初の例を見てください:
明らかに、上記のアルゴリズムによって取得された次の配列が[-1、0、0、1]である必要がある場合
したがって、次のステップは、jを最初の要素に移動することです。
このステップが完全に無意味であることを見つけるのは難しいことではありません。後者のBはもう一致しないため、前者のBも一致してはなりません。同じ状況が実際には2番目の要素Aでも発生します。
明らかに、問題の理由はP [j] == P [next [j]]です。
したがって、判断条件を追加するだけで済みます。
public static int[] getNext(String ps) {
char[] p = ps.toCharArray();
int[] next = new int[p.length];
next[0] = -1;
int j = 0;
int k = -1;
while (j < p.length - 1) {
if (k == -1 || p[j] == p[k]) {
if (p[++j] == p[++k]) {
// 当两个字符相等时要跳过
next[j] = next[k];
} else {
next[j] = k;
}
} else {
k = next[k];
}
}
return next;
}