【インタビュー】文字列アルゴリズム

一、具体

要件:ホワイトボードコードの記述は通常必要ありません。

1.暴力的な解決策(O(MN)O(MN)O M N

// Java
public static int forceSearch(String txt, String pat) {
    
    
    int M = txt.length();
    int N = pat.length();

    for (int i = 0; i <= M - N; i++) {
    
    
        int j;
        for (j = 0; j < N; j++) {
    
    
            if (txt.charAt(i + j) != pat.charAt(j))
                break;
        }
        if (j == N) {
    
    
            return i;
        }
        // 更加聪明? 
        // 1. 预先判断 hash(txt.substring(i, M)) == hash(pat)
        // 2. KMP 
    }
    return -1;
}

2.Rabin-Karpアルゴリズム

// Java
public final static int D = 256;
public final static int Q = 9997;

static int RabinKarpSerach(String txt, String pat) {
    
    
    int M = pat.length();
    int N = txt.length();
    int i, j;
    int patHash = 0, txtHash = 0;

    for (i = 0; i < M; i++) {
    
    
        patHash = (D * patHash + pat.charAt(i)) % Q;
        txtHash = (D * txtHash + txt.charAt(i)) % Q;
    }
    int highestPow = 1;  // pow(256, M-1)
    for (i = 0; i < M - 1; i++) 
        highestPow = (highestPow * D) % Q;

    for (i = 0; i <= N - M; i++) {
    
     // 枚举起点
        if (patHash == txtHash) {
    
    
            for (j = 0; j < M; j++) {
    
    
                if (txt.charAt(i + j) != pat.charAt(j))
                    break;
            }
            if (j == M)
                return i;
        }
        if (i < N - M) {
    
    
            txtHash = (D * (txtHash - txt.charAt(i) * highestPow) + txt.charAt(i + M)) % Q;
            if (txtHash < 0)
                txtHash += Q;
        }
    }

    return -1;
}

3 KMP

詳しくは、1&2&3の動画&記事をご覧ください。

3.1、コアコンセプト

  • 接頭辞:最後の文字を除いて、文字列のすべての頭の組み合わせ。

  • サフィックス:最初の文字を除いて、文字列のすべてのテールの組み合わせ。

    例:文字列S = "ABCDAB"、プレフィックスは[A、AB、ABC、ABCD、ABCDA]、サフィックスは[BCDAB、CDAB、DAB、AB、B]、要素の合計は "AB"、長さは2です。

3.2、KMP処理プロセス

1:プレフィックステーブル

文字列の場合、すべての文字列のプレフィックスとサフィックスの最も長い共通要素の長さを見つけます。
プレフィックステーブル

計算された共通の長さを1位置戻します。最初の位置は、-1です。
プロセス1
2.文字列マッチング

ステップ1:i = 0、j = 0から始めて、それらが1つずつ等しいかどうかを比較します。
プロセス1.1
ステップ2:不等式が発生した場合は、最初にプレフィックステーブルの下の対応する番号を確認し、次にポインタをパターン文字列の対応する添え字に移動します。同等のエフェクトパターン文字列は、全体として後方に移動します。
処理する
ステップ3:移動後の効果を以下に示します。
結果

コード:

// C V2.0
#include<stdio.h>
#include<stdlib.h>
#include<string.h>

void prefix_table(char pattern[], int prefix[], int n) {
    
    
	prefix[0] = 0;
	int len = 0;
	int i = 1;
	while (i<n) {
    
    
		if ( pattern[i]==pattern[len] ) {
    
      // ABA --> ABAB, 最长公共前缀长度由1 --> 2
			len++;
			prefix[i] = len;
			i++;
		}
		else {
    
    
			if (len > 0) {
    
      // 防止越界,
				// ABABCABA --> ABABCABAA。关键步骤,补充理解见下。
				len = prefix[len-1];
			}
			else {
    
      // pattern[i]!=pattern[len] && len==0, 防止死循环 A --> AB, 0 --> -1, 此时i = 0
				prefix[i] = len;
				i++;
			}
		}
	}
}

void move_prefix_table(int prefix[], int n) {
    
    
	int i;
	for (i=n-1; i>0; i--) {
    
    
		prefix[i] = prefix[i-1];
	}
	prefix[0] = -1;
}

void kmp_search(char text[], char pattern[]) {
    
    
	int n = strlen(pattern);
	int m = strlen(text);
	int* prefix = malloc(sizeof(int) * n);
	prefix_table(pattern, prefix, n);
	move_prefix_table(prefix, n);
	
	// text[i]    , len(text)    = m 
	// pattern[i] , len(pattern) = n 
	
	int i = 0;
	int j = 0;
	while (i < m) {
    
    
		if (j==n-1 && text[i] == pattern[j]) {
    
    
			printf("Found pattern at %d\n", i-j);
			j = prefix[j];
		}
		if (text[i]==pattern[j]) {
    
    
			i++; j++;
		}
		else {
    
    
			j = prefix[j];  // 关键过程可见上图
			if (j == -1) {
    
    
				i++; j++;
			}
		}
	}
}
/**/
int main() {
    
    
	/**/
	char pattern[] = "ABABCABAA";
	char text[]    = "ABABABCABAABABABAB";
	kmp_search(text, pattern);

	/*
	char pattern[] = "ABABCABAA";
	int prefix[9];
	int n = 9;
	prefix_table(pattern, prefix, n);
	move_prefix_table(prefix, n);
	int i;
	for (i=0; i<n; i++) {
		printf("%d\n", prefix[i]);
	}
    */
	
	return 0;
}

時間の複雑さ:O(m + n)、最悪の場合はO(mn)に縮退する可能性があります({aaaaaaaab、ab}など)。
空間の複雑さ:O(1)

  • 補足理解:次の配列をすばやく構築する方法

キーポイント:パターン文字列はそれ自体と一致します。
next [i]の定義: P [0] 〜P [i]文字列のこのセクションの接頭辞と接尾辞の最大共通文字列の長さ、つまり、k-接尾辞の最大kはk-接尾辞に等しい。

理解するための重要なステップ: len = prefix [len-1]、なぜですか?(分析は参考文献7からのものです)

目標:インデックス= 12になり、次の配列の最後の桁のk値を見つけます。つまり、next [last] =? 次を見つける方法[i + 1]

  • 既知の情報:
  1. これで、文字列Aは文字列Bと同じになり、next [last-1] = String_A.length = String_B.length = 5になります。
  2. 前半では、next [now-1] = {a、b} .length = 2、2は、サブストリングAのプレフィックスとサフィックスの最大の共通文字ストリングの長さです。
  • 分析:
  1. P [now] == P [x]の場合、前の桁に基づいて+1を直接加算すると、5 + 1 = 6になります。
  2. しかし、P [now]!= P [x]、次にnext [last] = next [now] = 0?明らかにそうではありません。
  • 次を見つける[最後] =?P [0] 〜P [x-1]の共通の接頭辞と接尾辞を知る必要があります。

文字列A =文字列B、P [now]!= P [x]、つまり、プレフィックスとサフィックスの最大の共通文字がすでにわかっている-文字列AまたはBが満たされていない。共通文字列を短くする必要があり、プレフィックスとサフィックスをさらに短くする必要があります。

分析を続けると、次の反復のプレフィックス(短縮)は文字列Aに含まれ、サフィックスは文字列Bに含まれる必要があります。文字列A ==文字列Bなので、文字列Aのプレフィックスとサフィックスの最大共通長、つまりnext [now-1]を見つけることになります。

ここでジャンプします。特定の値:now = next [now-1]、次にp [now] == p [x]を比較しますか?等しい場合、next [last] = now + 1;それ以外の場合、now = next [now-1]、ループを続行します。
結果

2、参照

1. KMP文字列照合アルゴリズム1
2 KMP文字列照合アルゴリズム2
3 KMPアルゴリズム
のための文字列照合4、KMPのための文字列照合、BoyerMoore日曜日アルゴリズム
5、文字列照合ブルートフォースコード例
6、ラビン-カープコードサンプル
7はどのようにより良いKMPアルゴリズムを理解し、習得するには?

おすすめ

転載: blog.csdn.net/HeavenDan/article/details/109165811