文字列照合アルゴリズム - KMP アルゴリズム

BFアルゴリズム

文字列のマッチングに関して最初に思い浮かぶのは、パターン文字列がメイン文字列と 1 文字ずつ一致することです。一致しない文字が見つかった場合、パターン文字列は 1 つ戻り、パターンの最初の位置から継続されます。 string. マッチングの開始. このマッチング アルゴリズムは、 「総当たりマッチング アルゴリズム」であるBF アルゴリズムとも呼ばれます。時間計算量は O(n*m) です。n と m はメイン文字列とパターン文字列の長さを表します。

BM アルゴリズムと KMP アルゴリズムはどちらも BF アルゴリズムに基づいて最適化されています。つまり、マッチング プロセス中に、より多くの文字をスキップすることが期待されます。

KMPアルゴリズムのアイデア

文字列の比較が失敗したときにどの文字が読み取られたかはすでにわかっているので、「次の文字に戻って再度一致させる」という手順を回避することはできるでしょうか?

KMP アルゴリズムの基本的な考え方は、一致しない文字に遭遇したときに、総当たりアルゴリズムの「バックアップ」ステップを回避するために走査されたパターン文字列を使用することです。言い換えれば、メイン文字列のポインタをデクリメントして、それを永遠に前進させたくないのです。

栗をあげます:

メイン文字列のパターン文字列 - ABAB C - ABAB A BCAA と一致します。

 照合の際、部分文字列の最後の文字が主文字列の対応する文字と一致しないが、主文字列の接尾辞が部分文字列の接頭辞と一致することが判明した場合は、2 文字をスキップし、部分文字列を 2 つ後ろに移動できます。場所を決めて試合を続行します。

では、一致しない文字に遭遇するたびにスキップする文字の数をどのようにして知ることができるのでしょうか?

ここでは、KMP アルゴリズムで定義された次の配列が使用されます。 

栗をあげます:

ここでは、次の配列がどのように生成されるかについては気にしません。まず、その機能と用途を見てみましょう。

KMP アルゴリズムは文字の不一致を検出すると、最後に一致した文字に対応する次の値を調べます。

この例では、最後に一致した文字に対応する次の値は 2 であるため、サブ文字列を 2 つ後方に移動して、メイン文字列内の 2 文字をスキップして一致を継続できるようにします。

 次の配列要素の値は、部分文字列が「一致をスキップ」できる文字数を表します。

ロールバック ポインタが必要なく、マッチングを完了するためにメイン文字列を 1 回走査するだけで済むため、当然、効率はブルート フォース アルゴリズムよりもはるかに高くなります。

public int kmpSearch(char[] txt, char[] patt){ // txt代表主串,patt代表模式串
		int[] next =buider_nexts(patt); // 假设已经计算出了next数组
		int i = 0; // 主串中的指针
		int j = 0; // 子串中的指针
		while (true){
			if (i == txt.length) return -1;
			if (txt[i] == patt[j]) { // 字符匹配,指针后移一位继续匹配
				i++;
				j++;
			} else if (j > 0) { // 字符不匹配,则根据next数值跳过子串前几个字符的匹配
				j = next[j-1];
			}else { // 子串的第一个字符就不匹配,则直接后移一位
				++i;
			}
			if (j == patt.length) return i-j; // 如果j已经达到子串末尾,则匹配成功,返回匹配的起始位置
		}
	}

 メイン文字列ポインタ i は決して減少しないことに注意してください。これは KMP アルゴリズムの本質でもあります。

次の配列の生成

前述したように、次の配列の値は、一致が失敗した場合に部分文字列内でスキップできる一致文字の数を表します。しかし、なぜそんなことが可能なのでしょうか?

前の例から、一致した最後の 2 つの AB は、スキップされた最初の 2 つの AB と同じであることがわかります。

つまり、部分文字列の最初の 4 文字には共通の接頭辞と接尾辞 AB があり、長さは 2 です。

次の配列の本質は、部分文字列内の「同じサフィックスの長さ」を見つけることであり、それは最長のサフィックスである必要があります。 

栗をあげます:

 この部分文字列では、プレフィックスとサフィックス A は同じですが、最長ではなく、ABA が同じプレフィックスとサフィックスの中で最長であるため、次の値は 3 になります。

ただし、サフィックスを部分文字列そのものにすることはできませんので、部分文字列自体の長さの文字数を省略すると意味がありません。

次の配列の計算

次の配列を計算するために、部分文字列 ABABC を例として取り上げます。

  • 最初の文字は当然同じサフィックスとサフィックスは存在せず、次は0です
  • 最初の 2 文字には同じサフィックスとサフィックスはなく、次は 0
  • 最初の 3 文字は同じプレフィックスとサフィックス A を持ち、次は 1 です。
  • 最初の 4 文字は同じプレフィックスとサフィックス AB を持ち、次は 2 です。
  • 同じサフィックスとサフィックスを持たない最初の 5 文字の場合、次は 0 です。

しかし、アルゴリズムをどのように実装するのでしょうか?

for ループを使用して問題を激しく解決することもできますが、効率が低すぎます。実際、再帰的手法を使用すると、次の配列をすばやく解決することができ、その賢い点は、既知の情報を継続的に使用して、繰り返しの操作を回避することです。

栗をあげます:

現在の共通サフィックスとサフィックスが既知で、長さが 2 であると仮定すると、下方一致を継続するには 2 つの状況があります。

1. 次の文字が同じ場合は、より長い共通接尾辞が形成され、長さは前の長さ + 1 になります。

2. 次の文字が同じでない場合は、より短い同一の接尾辞があるかどうかを見つける必要があります。

では、より短い同一の接尾辞を見つけるにはどうすればよいでしょうか?

前の計算で、一致しない文字 B の前に同じサフィックスと長さ 3 のサフィックスがあることがすでにわかっています。左側の共通のサフィックスを直接探すことができ、左側の最長のサフィックスの長さを求めることができます。テーブルを参照して取得します。1 の場合は、元のステップに戻って次の文字が同じかどうかを確認します。同じであれば、+1 の長さのより長いサフィックスを構築できます。

アニメーションを比較して理解してください。

 

KMP アルゴリズムの次の配列生成コード: 

private static int[] buider_nexts(char[] patt) {
	int[] next = new int[patt.length]; // next数组,且第一个元素为0
    next[0] = 0;
	int prefix_len = 0; // 当前公共前后缀的长度
	int i = 1;
	while (i < patt.length){
		if (patt[prefix_len] == patt[i]){ // 字符匹配,则将prefix_len+1,存入对应next数组中
			prefix_len++;
			next[i] = prefix_len;
		}else{
			if (prefix_len == 0){	// 如果不存在相同前后缀则直接把next设为0
				next[i] = 0;
				i++;
			}else{
				prefix_len = next[prefix_len-1];	// 字符不匹配则直接查表看看存不存在更短的共同前后缀
			}
		}
	}
	return next;
}

 完全なコード:

public int kmpSearch(char[] txt, char[] patt){
		int[] next =buider_nexts(patt); // 假设已经计算出了next数组
		int i = 0; // 主串中的指针
		int j = 0; // 子串中的指针
		while (true){
			if (i == txt.length) return -1;
			if (txt[i] == patt[j]) { // 字符匹配,指针后移一位继续匹配
				i++;
				j++;
			} else if (j > 0) { // 字符不匹配,则根据next数值跳过子串前几个字符的匹配
				j = next[j-1];
			}else { // 子串的第一个字符就不匹配,则直接后移一位
				++i;
			}
			if (j == patt.length) return i-j; // 如果j已经达到子串末尾,则匹配成功,返回匹配的起始位置
		}
	}

	private int[] buider_nexts(char[] patt) {
		int[] next = new int[patt.length]; // next数组,且第一个元素为0
		next[0] = 0;
		int prefix_len = 0; // 当前公共前后缀的长度
		int i = 1;
		while (i < patt.length){
			if (patt[prefix_len] == patt[i]){ // 字符匹配,则将prefix_len+1,存入对应next数组中
				prefix_len++;
				next[i] = prefix_len;
                i++;
			}else{
				if (prefix_len == 0){	// 如果不存在相同前后缀则直接把next设为0
					next[i] = 0;
					i++;
				}else{
					prefix_len = next[prefix_len-1];	// 字符不匹配则直接查表看看存不存在更短的共同前后缀
				}
			}
		}
		return next;
	}

おすすめ

転載: blog.csdn.net/weixin_53922163/article/details/132755827