kmpアルゴリズム原理分析次の配列原理説明コード詳細コメント

解決すべき問題

アルゴリズムを理解する最初に、アルゴリズムの目的を非常に明確に理解し、次に関数またはメソッドを理解する必要があります。次に、その入力、出力、および関数を理解する必要があります。
kmpアルゴリズムは文字列用であり、文字列からターゲット文字列を見つけるために使用されます。

この問題を解決するために、まず、暴力的な解決策について考えてみましょう。ほとんどのアルゴリズムは、暴力的な解決策を改善し、トリックを実行するためです。文字列からターゲット文字列を見つけます。
といった

S; abcabccabcaaabcabcd
T: abcabcd

上記の例は、SからTの位置を見つけ、見つかったSのインデックス位置を返すことです。
私たちの暴力的な解決策は、実際には非常に簡単に考えることができます。
Sの各文字をTの文字と1つずつ比較するたびにトラバースすることです。これは間違いなく見つけることができ、もちろん時間計算量も高くなります。分析は簡単で、時間計算量はO(n * m)、nはSの長さ、mはTの長さです。

次に、kmpアルゴリズムは、力ずくのソリューションに基づいて「わずかな」改善を行い、繰り返しの比較を排除して、全体的な時間の複雑さを軽減します。O(n + m)に減らします。

kmpが速いのはなぜですか

アルゴリズムがどの問題を解決するかを理解し、問題の一般的な暴力的な解決策を知った後、アルゴリズムが暴力的な解決策と比較して多くの繰り返しを削除する(またはAを採用する)ため、アルゴリズムが高速になる理由をさらに考える必要があります単純な方法は暴力的な解決策とは異なりますが、達成が多くの状況を考慮していないことも理解できます)。
では、kmpアルゴリズムはどのような重複を削除しますか?
以下を見てみましょう。

S; abcabccabcaaabcabcd
T: abcabcd

強引な解を使用してSの最初の場所を見てみましょう。以下では、最初の場所がTの最初の場所と同じであることがわかります。次に、Tの7番目の場所とSの7番目の場所を比較して比較します。 、現時点では、暴力的な解決策は何をしますか?
暴力的な解決策は何をしますか?
明らかです:

S; abcabccabcaaabcabcd
T:  abcabcd

Tを1つ右に移動してから、価格を1つずつ比較し続けます。

しかし、明らかに、この比較は間違いなく一貫性がありません。右に1つしか移動できません。間違っています。後で比較できるのは、1つだけです。ああ、最後に、下に移動すると、最初の場所が最終的に一致することがわかりました。

S; abcabccabcaaabcabcd
T:    abcabcd

それから私は2番目の場所を唖然と比較しに行きました、ええと、同じです、はい、3番目は同じです、良いです。4つ目は異なり、それが終わったら、最初からやり直す必要があります。

では、最初の試合が失敗してから、ブルートフォースソリューションは何回移動しましたか?数えて3回動かして3回比較したところ、間違っていることがわかりました。合計6回の操作が行われました。

しかし、それを見ると、目の肥えた人は、初めて一致しなかったときに、Tを3マス右に動かしていることが一目でわかります。Tの最初の3桁とSに対応する3桁です。直接同じ?激しく6回解く必要がありますが、1回だけ動かせませんか?

これは確かに事実であり、1つのステップで実行できること、暴力的な方法は6つのステップを使用します。しかし、問題は、これを1つのステップでどのように行うかということです。

最初の試合が失敗した状況を詳しく見てみましょう。

S; abc"abc"cabcaaabcabcd
T: "abc"abcd

引用符でマークした部分を見てください。それらは等しいですか?はい、Tを後ろに移動して、引用符の緑色の部分を揃えました。それらは等しいので、の緑色の部分の後の文字から直接開始します。引用符。一致(比較)することで、オーバーヘッドを節約します。

S; abc"abc"cabcaaabcabcd
T:    "abc"abcd

上記がkmpが高速である理由ですが、考えてみましょう。引用符の緑色の部分が等しくなると考えることができるのはなぜですか。

初めて一致したときは、次のように一致しました。aはaと同じ、bはbと同じ、cはcと同じ、aはaと同じ、bはbと同じ、c cと同じですか?abc、どこかで見たようですよね。段落の最初と最後には、すでに一致した同じ部分があります。次に、Tも一致させることができるため、Tの最初と最後に同じパーツがあります。次に、Sの前の一致したパーツの最後にTのヘッドを直接引っ張ることができます。

上記が理解しにくい場合は、より明確な図を使用して理解しましょう。
紫色の矢印で試合は失敗します。
ここに画像の説明を挿入
青と黄色の領域は等しい、つまり、正常に一致した部分、頭と尾の領域は等しい(最大の)領域であるため、当然、Sの黄色の領域はSの黄色の領域と直接一致する可能性があります。 T。黄色と黄色が揃うようにTを移動してから、紫色から再開します。
ここに画像の説明を挿入

上記がkmpアルゴリズムの方が速い理由です。アルゴリズムは、ターゲット文字列の特定の特性があるため、毎回最初から検索する必要はなく、保存するために最後に等しい部分を直接整列することを発見しました。時間。オーバーヘッド。

しかし、ここに問題があります。上記は手動検索のロジックです。特定のアルゴリズムで一致した部分の等しい部分のこの発見を実現するにはどうすればよいですか?
次の配列を使用します。以下の次の配列について話しましょう。

次の配列

毎回一致する部分は、位置0から始まる文字列Tの部分文字列であるため、一致がいつ失敗するかはわかりませんが、失敗するたびに、現在一致している部分の先頭と末尾を知る必要があります。 。等しい部品。
とにかく、それは位置0から始まります。次に、各位置の部分文字列を見つけ、次にこれらの部分文字列の最初と最後の等しい部分の長さを見つけます。
上記の例を使用してください。

	T	:	a b c a b c d           (首尾相等的部分最长要小于子串长度)
	子串:                           已匹配部分的首尾相等部分长度
	0-0:	a								0
	0-1:	a b								0
	0-2:	a b c                           0
	0-3:	a b c a                         1
	0-4:	a b c a b                       2
	0-5:	a b c a b c                     3
	0-6:	a b c a b c d                   0

このことから、部分文字列a、ab、およびabcについては、それらの等しい部分を見つけることができないことは明らかです。kmpアルゴリズムでは、適切なステップごとにしか移動できません。
そしてabcabcの場合、それは快適です。一致が失敗した場合、最初と最後の等しい部分の長さが3であることがわかり、3つの正方形を直接右に移動でき、の最初の3文字を一致させる必要はありません。 T、前の文字を使用します。一致しないSの文字は、Tの4番目の文字と一致します。
ここで、0〜6の部分文字列は表示できないため意味がありません。表示されている場合は、一致が成功したことを意味します。

したがって、次の配列には、現在の一致状況での最初と最後の共通部分の長さが実際に格納されます。一致が失敗した場合、次の配列のnext [i]値を照会することにより、next [i]ユニットを直接右に移動できます。 。
たとえば、abcabcと一致しなかった場合、3ユニットを直接右に移動します。abcabと一致しない場合は、2ユニットを直接右に移動します。

もちろん、上記の次の配列は最後の次の配列ではありません(実際には、プレフィックステーブルです)。実際、少し処理する必要があります。0-6は実際には意味がないため、0-6を削除し、配列全体を1グリッド戻し、ヘッドとテールを-1に設定して、最後の次の配列を形成します(実際にはプログラミングの便宜のため)。
最終的な次の配列です[-1,0,0,0,1,2,3]

次の配列とは何かについて話し合った後、解決すべきもう1つの非常に重要な問題があります。それは、次の配列をどのように生成するかです。もちろん、部分文字列を手動で分析するのは簡単ですが、コンピューターは良くありません。どうすれば私たちのように数えることができますか?それが暴力的である場合次の配列が解決方法によって生成される場合、それは非常に遅くなりますが、最終的なkmpアルゴリズムが遅くなり、暴力的な解決策ほど良くはありません。次の配列を生成するための迅速な方法が必要です。以下の次の配列を生成する方法について話しましょう。

次の配列を生成する方法

ここでの考え方は次のとおり
です。現在のエンドツーエンドの等しい部分の長さは、前のエンドツーエンドの等しい部分の長さに依存します。

たとえば、前回同じ長さが2、abcabである場合。(文字は位置0から始まります)
次に、私はabcabcです(最後の文字cが追加されます)。部分文字列の2番目の文字を最新の文字cと比較するだけで済みます。その後、一見すると、実際に等しいです。 、そしてこの時点で、最初と最後の等しい長さ=最後の等しい長さ+1。
そして、私の最後の等しい長さが0だったのはいつですか?、次に、今回は0番目の文字が最新の文字と等しいかどうかを直接比較します。等しい場合は現在の長さ= 0 + 1、そうでない場合はまだ0です。

このように、それは非常に単純ですが、事実はそれほど単純ではありません。
前回の長さがiで、今回は位置iの文字が最新の文字と等しくない場合を考えてみます。この時に何をしますか?直接の長さは0ですか?いいえ、例を見てみましょう。

                                                 首尾相等部分长度长度
上一次:  (a b c d a b c)(a b c d a b c)                  7  
这一次:  (a b c d a b c)(a b c d a b c) d                ?

この例を見ると、今回の最初と最後の等しい部分の長さはどれくらいですか?7未満でなければならないことはわかっていますが、いくらですか?それは0ですか、絶対にありません。手動で見つけることができます:

                                                 首尾相等部分长度长度
上一次:  (a b c d a b c)(a b c d a b c)                  7  
这一次:  (a b c d)a b c  a b c d (a b c d)

だからそれは間違いなく0ではありませんが、これは私たちの人間の目が見つけるものです、それを見つけるためのアルゴリズムを構築する方法は?

ここにいくつかの事実があります:

  1. 新しい等しいパーツの長さは、以前の長さよりも小さくする必要があります。
  2. 新しい平等は前のものの中になければなりません。

これら二つの事実について、私たちはこの説明をします。1の場合、新しい等しい部分の長さが前の部分よりも長い場合、それは明らかに間違っています。そして2の場合、新しい等しい部分が前の等しい部分の内側にない場合、頭から見てみましょう。新しい等しい部分は前の等しい部分の内側にありません。つまり、新しい等しい部分の長さ>前の等しい部分を意味します。長さ、そしてこれは明らかに間違っている事実1と矛盾します。

これら2つの事実に基づいて検索しました。
新しい等しい部分を見つけるには、前の等しい部分の内部を調べるだけです。
ここに画像の説明を挿入
したがって、後半のみを見て、上図の青と黄色で示されているように、後半で等しい部分を探します。
次に、ここでの長さが3であることがわかり、3番目の位置のd文字の後半を新しい文字dと比較するため、最初と最後の等しい部分の長さは4になります。

ここで正面を考えてみませんか?これは、前半と後半がまったく同じであるためです。このような図を使用すると、より直感的に理解できます。
ここに画像の説明を挿入
それらが等しいので、黄色の部分は端から端まで黄色の部分に対応し、等しい部分の子が等しいので、黄色の部分と青い部分も等しいので、最後の黄色の部分は等しくなります。

もちろん、準等しい部分が見つかった場合、ここで等しい部分の長さが4でない可能性もあります。たとえば、最後のdをeに変更した場合、明らかに4ではありません。
しかし、それは問題ではありません。私たちはこのように繰り返し続けます。

この時点で、次の配列がどのように構築されるかのロジックは明確です。以下のコードを見てみましょう。コードを歩いても基本的に問題はありません。

Javaコードの実装と分析

私はここのコードについて非常に詳細なコメントをしました

public class kmp {
    
    
    /**
     * 构建prefix table,也就是求目标字符串子串的首尾相等部分
     * @param pattern
     * @return
     */
    public int[] setPrefix(char[] pattern){
    
    
        int len = pattern.length;
        int[] prefix = new int[len];

        for(int i=1; i<len; i++){
    
    
            int k = prefix[i-1];//获取前一个子串的最长首尾相等部分长度
                                //同时k刚好是相等子串首部的后一个,需要判断的当前一个

            while(pattern[i]!=pattern[k]&& k!=0){
    
    //如果不等于的话就一直找,找的逻辑是相等部分的首部相等部分,如果不是,继续寻找,这个要想一下是为什么                
                k = prefix[k-1];
                //来想想为什么是k = prefix[k-1]
                //其实蛮好理解的,如果不等于,那么出去i点,前面相等的部分一定在当前的相等部分内部,也就是说在相等部分的内部还存在子相等
                //这个子相等才是当前点i需要的子相等
                //那么就去寻找首部相等部分里的子相等,因为首部相等部分里的 子首部相等 与 子尾部 相等,那么同理,尾部相等部分 中的子首部也对称与它的子尾部 相等,所以首部相等部分里的子首部 与 尾部相等部分 里的子尾部相等
                //从而就找到了一个更小的相等部分
                //那么再来想一个问题,有没有比这个子首部更大的子首部呢?肯定没有,如果有的话,最大想等的又要修改了,所以,这已经是最大的了。
            }
            if(pattern[i]==pattern[k]){
    
    //如果找到了,则直接在基础上加1即可
                prefix[i] = k+1;
            }
            else{
    
    //如果找不到,则直接命名为0
                prefix[i] = 0;
            }
        }
        return prefix;
    }

    /**
     * 对prefix table 进行一个后移,然后初值赋值为-1,从而就获得了真正的next数组
     * @param prefix
     * @return
     */
    public int[] movePrefix(int[] prefix){
    
    
        for(int i = prefix.length-1; i>0; i--){
    
    
            prefix[i] = prefix[i-1];
        }
        prefix[0] = -1;
        return prefix;
    }

    /**
     * kmp算法
     * @param pattern
     * @param text
     */
    public void kmpSearch(char[] pattern,char[] text){
    
    
        //获取netx数组
        int[] prefix = setPrefix(pattern);
        prefix = movePrefix(prefix);
        //进行kmp查询
        //text[i]     len(text)     = M
        //pattern[j]  len(pattern)  = N
        int i = 0, j = 0, M = text.length, N = pattern.length;

        while(i<M){
    
    
            if(j>=N){
    
    //为了排除j>=N导致数组越界的问题
                j = 0;
            }
            if(j == N-1 && text[i] == pattern[j]){
    
    
                System.out.println("found pattern at :"+String.valueOf(i-j));
                //当找到第一个后,还得继续进行匹配
                j = prefix[j];
                if(j==-1){
    
    //排除AA中找A的问题
                    j++;
                }
            }
            if(text[i] == pattern[j]){
    
    
                i++;
                j++;
            }
            else {
    
    
                j = prefix[j];
                if(j == -1){
    
    //当移动到-1时
                    i++;
                    j++;
                }
            }
        }
    }

    public static void main(String[] args) {
    
    
        kmp demo = new kmp();
        char[] pattern = {
    
    'A','B','A','B','C','A','B','A','A'};
//        char[] pattern = {'A'};
        char[] text = {
    
    'A','B','A','B','A','B','C','A','B','A','A','B','A','C','A','B','A','B','C','A','B','A','A'};
//        char[] text = {'A','A'};

        demo.kmpSearch(pattern,text);

    }
}

参照

https://blog.csdn.net/yearn520/article/details/6729426
https://www.bilibili.com/video/BV1Px411z7Yo
感謝

おすすめ

転載: blog.csdn.net/qq_34687559/article/details/109586789