第 4 章: 文字列 (KMP アルゴリズムと KMP 最適化)

コンセプト:

0 個以上の文字で構成される有限シーケンスは文字列と呼ばれ、
任意の数の連続する文字で構成される部分シーケンスは文字列の部分文字列と呼ばれ、
部分文字列でいっぱいの文字列はメイン文字列と呼ばれます。

データ構造の定義
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
    
    
	char ch[10];
	int length;
} SString;

void main()
{
    
    
	SString s;
	s.length = 5;
	char a[5] = {
    
    'H','e','l','l','o'};
	// 复制数组 
	memcpy(s.ch , a,strlen(a));
	
	for(int i = 0; i<s.length; i++){
    
    
		printf("%c\n",s.ch[i]);
	}
	
}

ここに画像の説明を挿入

文字列の使用は非常に簡単で、部分文字列のマッチング アルゴリズムのみに焦点を当てています。

(1) 単純なパターンマッチング

考え方は非常に簡単で、メイン文字列の最初の文字とサブ文字列から始めて、添え字の位置を 1 つずつ比較し、一致しない場合は次の位置の比較を続けます。
より一般的に言えば、単語ごとに比較するだけです。
1: 部分文字列を外側のループとして、メイン文字列を内側のループとして取り、サブ文字列要素を取り、メイン文字列の要素のスキャンを開始します。一致した場合、サブストリングは下に移動します サブスクリプトポインタとメインストリングのサブスクリプトポインタは同時に 1 移動します、つまり、次のものを比較します; 一致しない場合、メインストリングのサブスクリプトポインタは 1 移動します、部分文字列は文字列の先頭に戻り、再照合の準備が整います。
ここに画像の説明を挿入

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
    
    
	char ch[10];
	int length;
} SString;

void main()
{
    
    
	SString s;
	s.length = 5;
	char a[5] = {
    
    'H','e','l','l','o'};
	memcpy(s.ch , a, strlen(a));
	char b[2] = {
    
    'l','o'};
	for(int i = 0; i<s.length; i++){
    
    
		int j = i;
		int k = 0;
		while(s.ch[j] == b[k]){
    
    
				printf("匹配成功: %c\n",s.ch[j]);
				k++;
				j++;
				if(k>1){
    
    
					printf("全部匹配成功\n");
					return;
				}
		}
		printf("字符串%c匹配失败\n",s.ch[j]);
	}
}
このうち、ループの入れ子は 2 層あり、1 つは主文字列のループ、もう 1 つは部分文字列のループであるため、最悪の時間計算量は O(M N ) になります。つまり、最後の要素の不一致が一致すると仮定した場合です。毎回、つまり、外側の層が実行されるほぼ毎回、内側の層は N 回実行されるため、外側の層の M 回は、合計実行の M N 回になります。

上記の例から、部分文字列が非常に長い場合、たとえば 6000 文字があり、メイン文字列が数百万文字ある場合、最初の文字列が毎回 i から開始して正常に一致すると、部分文字列と同時にメイン文字列のポインタは後方に移動し、5998 ビットの不一致がある場合でも、サブ文字列のポインタは 0 に戻り、メイン文字列 i+1 との比較を続けます。非常に非効率的

(2) KMPアルゴリズム

上の例によれば、非効率の根本原因はポインタのバックトラッキングであるため、部分文字列と主文字列の最初の 0 ~ N 要素が同じである場合、部分文字列の一部が等しい場合を想像します。 : 部分文字列は aabaac で、aa は等しいプレフィックスとサフィックスです。メイン文字列が次の aac と比較され、等しくないことが判明した場合は、メイン文字列の前にある aa の部分が実際にサブ文字列と一致することが証明されます。実際、メイン文字列のポインタを動かさないようにすることができ、サブ文字列のポインタをプレフィックスの aa の位置までバックトラックし、aab が等しいかどうかを比較します。 aa の等しい部分を比較する必要があります。3 番目からは 1 番目の位置にある要素の比較が始まります(この部分はわかりにくいですが、次の例を読むと理解できます)。

しかし問題は、部分文字列ポインタがどこに戻ったかをどのようにして知ることができるかということです。部分文字列の最大の接頭辞に対応する添え字はどこですか? これには Next 配列が必要です

次の配列は、実際には、X 番目の位置で不一致が発生した場合に、部分文字列のポインタがどこまで遡るべきかを記述します。
次の配列を見つけるにはどうすればよいですか?

例 1: 部分文字列 S='aaab' を想定し、次の配列を検索します

まず、1ビット目で不一致があった場合、後戻りは一切できないので、1ビット目は0に固定されます。2ビット目
で不一致があれば、1ビット目は一致していることが証明されるので、 2 番目のビットは 1 に固定されます。
3 番目の桁で不一致が発生した場合、これは、メイン文字列 aa の最初の 2 桁が部分文字列と一致するが、3 番目の桁が一致しないことが証明されるため、プレフィックスに依存します。
3桁目は?
同時に、部分文字列を 1 ビット戻し、一致する最大の接頭辞を見つけて取得します。2
ここに画像の説明を挿入
番目の a と ? を使用します。比較してみてください。つまり、a に等しいでしょうか。部分文字列の添字は 2 番目の要素に戻る必要があります。
したがって、配列の3番目の位置が一致せず、2に戻る必要があります。

4 番目の位置に不一致がある場合は、
ここに画像の説明を挿入
a と部分文字列の 3 番目の位置を使用しますか? 比較してみてください。これは
、部分文字列ポインタが 3 番目の位置に戻るということです
か? 4 番目の位置に不一致がない場合、ペアリングは成功です~
したがって、次の配列の値は次のようになります: 0123
ここに画像の説明を挿入

概要: 次の配列値を見つける手順は次のとおりです。

1: 最初の 2 桁は 0 1 に固定されます。
2: 3 番目の位置から開始して、部分文字列を後方に移動して、左行の上下に一致する最大の接頭辞を見つけます。つまり、左行の 2 つの文字列が完全に一致する必要があります。文字列位置は、次の配列内で比較される次の値の位置、つまりバックトラック位置です。
3: 部分文字列が完全に中央の線を越えて移動し、2 つの文字列の一致する位置が見つからない場合、次の配列の値は 1 に設定されます。つまり、既知のどの部分も一致せず、最初の配列の値は 1 に設定されます。最初からしかマッチングできません。

例 2: ababaaababaa の次の配列を検索します。

ここに画像の説明を挿入
次の配列の最初の 2 桁が -1 0 に固定されるという別の言い方もあります。
実際、上で計算された次の配列はまとめて -1 になります。

上記の計算例の後、部分文字列が一致しない場合、次の配列の位置に対応する値 next[i] は、実際には next[i] の位置まで遡る部分文字列のポインタであると漠然と感じることができます。 i 番目の位置メイン文字列ポインタは変更されず、サブ文字列の next[i] 要素がメイン文字列と一致しない要素と比較され、それらが等しいかどうかが確認されます。
再度メイン文字列と比較するにはどうすればよいでしょうか? 例を挙げて理解するとよいでしょう。

例 3 文字列 S = 'aabaabaabaac'、P = 'aabaac' と仮定します。

(1) P の次の配列を見つける

ここに画像の説明を挿入

(2) S がメイン文字列、P がパターン文字列の場合、KMP アルゴリズムのマッチング処理を記述します。

ここに画像の説明を挿入

メイン文字列 S = 'aabaabaabaac'
パターン文字列 P = 'aabaac'
(1) S[j] = = P[i]; i j++ の場合、最初の比較のロジックは同じです
(2) 比較aabaa all match 、6 番目の位置まで、不一致が発生します。今回は i=6、i=6 の次の値を見つけ、それが 3 であることがわかり、部分文字列は i=3 の位置に戻ります。この時点ではメイン文字列ポインタ j=6 バックトラックする必要はありません。つまり、P[3] = b を使用してメイン文字列の 6 番目のビットと比較し、b == S[6], (
3 ) 一致を続けます, ij ++; P[6]=c, S[9] = a になるまで、再び不一致になります このとき、再び i は 6 になります 次の i=6 の値を求めて、 3 の場合、部分文字列は i=3 の位置に戻ります (4) P[3] = =
S[9] = = b、その後 ij ++ は下に進みます
(5) 今度は、不一致はなくなります。 P[6] = = S[12] = = c. この時点で、部分文字列の終わりは一致の成功をマークします。
これがマッチングの全プロセスです

KMP マッチング プロセスを理解し、シミュレーションできるようになったら、王書から抜粋された過去数年間の実際の質問をいくつか見て、印象を深めることができます。実際、プロセスを理解していれば、次のことは非常に簡単です。質問をする

ここに画像の説明を挿入
8 番目と 9 番目の質問の Next 配列はすべて計算されます: 0 1 1 2 2 3 8
番目の質問、最初の不一致は s[5] != t[5] です。これは、KMP 機能によると、a != c です。メイン文字列 S ポインタ i はバックトラックせず、サブストリング ポインタは次の配列に従って 3 番目の 3 位置にバックトラックしますが、ここでの添え字は 0 から始まるため、t[ 2 ] になるはずなので、 i = 5; j =
2

9問目は比較回数を計算する問題で、1回目の比較はa= =ab= =ba= =aa= =ab= =bすべて一致、6回目の比較はa!=cです。部分文字列は 3 に戻ります。つまり、
部分文字列のプレフィックス ab はもう比較する必要がありません
。その後、比較を続けます: a= =aa= =ab= =bc= =c と一致を完了します
。合計: 6+4 = 10 個の比較

(3) KMPの最適化


KMP の最適化は実際には Next データの最適化です。最適化後は NextVal 配列と呼ばれ、 Next よりもパフォーマンスが向上します。より一般的
言えば、部分文字列が i = 6; P[6] = a に一致し、一致が失敗した場合、次の配列に従って、i = 3 の位置に戻りますが、P[3] = a ; その後、私は a のマッチングに失敗しました。そして、失敗したことがわかっているマッチングを行うために、戻って a を押し続けるように言われました。これは時間の無駄です。不一致がある場合は、単純に i = 3 を与えてください。i に戻る必要があります。
この原則によれば、i のバックトラックに対応する値が常に同じであれば、常にバックトラックします。値が同じでなくなるまで、この時点で比較することは理にかなっています。
引き続き例を見てください。

例 1: ababaaababaa の次の val 配列を検索します。

この配列は以前に質問したもので、その計算結果を直接取得できます:
ここに画像の説明を挿入
充填ステップ:
前から後ろに充填:
1 不一致、ジャンプなし、または 0
2 不一致、1 に戻る、a != b、値 Not
等しい、3 つの不一致は上書きしない、1 に戻る、この時点では p[3] == p[1] = = a ; したがって、3 つの不一致が発生した場合は、次の値 1 を直接取得するため、次の val = 0
4不一致が発生した場合、2 に戻ります、この時点では p[4] = = p[2] = = b; したがって、4 に不一致がある場合は、次の値 2 を直接取得するため、次の val = 1 5 には不一致があります。ジャンプ
バック 3. このとき、 p[5] = = p[3] = = a ; したがって、5 に不一致がある場合は、次の値 3 を直接取り、次の val = 0 6 に不一致があるので、ジャンプバックします。から 4 まで、このとき p
[ 6] != p[4] 文字が同じではないので比較する必要がありスキップできないため、6 に不一致がある場合、次の val の値は同じになりますnext の値として、 next val = 4 となります
このロジックによれば、段階的に計算するのは非常に簡単ですが、最初のラウンドは少し不安定になるはずで、最終的な出力は次のようになります。
ここに画像の説明を挿入

(4) 時間計算量

上記の例からわかるように、KMP の 2 つの主な操作は次のとおりです。
(1) Next 配列または NextVAL を周期的に生成します。部分文字列のサイズが M の場合、必要なのは O(M) のみです
(2)配列 Map の「ナビゲーション」では、部分文字列と主文字列が一致し、主文字列をバックトラックする必要がありません。主文字列のデータ サイズが N の場合、必要なのは O(N) だけであり、合計は: O(M+N
)

以上が弟による文字列マッチングアルゴリズムの概要と理解ですが、何か間違っている点があれば修正して一緒に議論してください〜

おすすめ

転載: blog.csdn.net/whiteBearClimb/article/details/127817796