大きなテキストの文字列検索アルゴリズム
1. 前書き
この記事では、大きなテキストでパターンを検索するためのいくつかのアルゴリズムを紹介します。 提供されているコードと簡単な数学的背景を使用して、各アルゴリズムについて説明します。
提供されているアルゴリズムは、より複雑なアプリケーションで全文検索を行うための最良の方法ではないことに注意してください。 全文検索を適切に行うために、SolrまたはElasticSearchを使用できます。
2. アルゴリズム
最も直感的なアルゴリズムであり、そのタスクに関連する他の高度な問題を発見するのに役立つ、単純なテキスト検索アルゴリズムから始めます。
2.1. ヘルパーメソッド
始める前に、ラビンカープアルゴリズムで使用する素数を計算するための簡単な方法を定義しましょう。
public static long getBiggerPrime(int m) {
BigInteger prime = BigInteger.probablePrime(getNumberOfBits(m) + 1, new Random());
return prime.longValue();
}
private static int getNumberOfBits(int number) {
return Integer.SIZE - Integer.numberOfLeadingZeros(number);
}
2.2. シンプルなテキスト検索
このアルゴリズムの名前は、他の説明よりもよく説明しています。 これが最も自然な解決策です。
public static int simpleTextSearch(char[] pattern, char[] text) {
int patternSize = pattern.length;
int textSize = text.length;
int i = 0;
while ((i + patternSize) <= textSize) {
int j = 0;
while (text[i + j] == pattern[j]) {
j += 1;
if (j >= patternSize)
return i;
}
i += 1;
}
return -1;
}
このアルゴリズムのアイデアは簡単です。テキストを反復処理し、パターンの最初の文字に一致する場合は、パターンのすべての文字がテキストに一致するかどうかを確認します。
mがパターン内の文字数であり、nがテキスト内の文字数である場合、このアルゴリズムの時間計算量はO(m(n-m + 1))です。
最悪のシナリオは、Stringに多くの部分的な発生がある場合に発生します。
Text: baeldunbaeldunbaeldunbaeldun
Pattern: example
2.3. ラビンカープアルゴリズム
前述のように、単純なテキスト検索アルゴリズムは、パターンが長く、パターンの繰り返し要素が多数ある場合、非常に非効率的です。
Rabin Karpアルゴリズムのアイデアは、ハッシュを使用してテキスト内のパターンを見つけることです。 アルゴリズムの開始時に、後でアルゴリズムで使用されるパターンのハッシュを計算する必要があります。 このプロセスは指紋計算と呼ばれ、詳細な説明hereを見つけることができます。
前処理ステップで重要なことは、その時間計算量がO(m)であり、テキストの反復にはO(n)がかかるため、アルゴリズム全体の時間計算量がO(m+n)になることです。
アルゴリズムのコード:
public static int RabinKarpMethod(char[] pattern, char[] text) {
int patternSize = pattern.length;
int textSize = text.length;
long prime = getBiggerPrime(patternSize);
long r = 1;
for (int i = 0; i < patternSize - 1; i++) {
r *= 2;
r = r % prime;
}
long[] t = new long[textSize];
t[0] = 0;
long pfinger = 0;
for (int j = 0; j < patternSize; j++) {
t[0] = (2 * t[0] + text[j]) % prime;
pfinger = (2 * pfinger + pattern[j]) % prime;
}
int i = 0;
boolean passed = false;
int diff = textSize - patternSize;
for (i = 0; i <= diff; i++) {
if (t[i] == pfinger) {
passed = true;
for (int k = 0; k < patternSize; k++) {
if (text[i + k] != pattern[k]) {
passed = false;
break;
}
}
if (passed) {
return i;
}
}
if (i < diff) {
long value = 2 * (t[i] - r * text[i]) + text[i + patternSize];
t[i + 1] = ((value % prime) + prime) % prime;
}
}
return -1;
}
最悪のシナリオでは、このアルゴリズムの時間計算量はO(m(n-m+1))です。 ただし、平均して、このアルゴリズムにはO(n+m)の時間計算量があります。
さらに、このアルゴリズムのモンテカルロバージョンはより高速ですが、間違った一致(誤検知)を引き起こす可能性があります。
2.4 Knuth-Morris-Pratt Algorithm
Simple Text Searchアルゴリズムでは、パターンに一致するテキストの多くの部分がある場合、アルゴリズムがどのように遅くなるかを見ました。
Knuth-Morris-Prattアルゴリズムのアイデアは、パターン候補を検索する場所を提供するシフトテーブルの計算です。 シフトテーブルhereについて詳しく読むことができます。
KMPアルゴリズムのJava実装:
public static int KnuthMorrisPrattSearch(char[] pattern, char[] text) {
int patternSize = pattern.length;
int textSize = text.length;
int i = 0, j = 0;
int[] shift = KnuthMorrisPrattShift(pattern);
while ((i + patternSize) <= textSize) {
while (text[i + j] == pattern[j]) {
j += 1;
if (j >= patternSize)
return i;
}
if (j > 0) {
i += shift[j - 1];
j = Math.max(j - shift[j - 1], 0);
} else {
i++;
j = 0;
}
}
return -1;
}
そして、これがシフトテーブルの計算方法です。
public static int[] KnuthMorrisPrattShift(char[] pattern) {
int patternSize = pattern.length;
int[] shift = new int[patternSize];
shift[0] = 1;
int i = 1, j = 0;
while ((i + j) < patternSize) {
if (pattern[i + j] == pattern[j]) {
shift[i + j] = i;
j++;
} else {
if (j == 0)
shift[i] = i + 1;
if (j > 0) {
i = i + shift[j - 1];
j = Math.max(j - shift[j - 1], 0);
} else {
i = i + 1;
j = 0;
}
}
}
return shift;
}
このアルゴリズムの時間計算量もO(m+n)です。
2.5. 単純なボイヤームーアアルゴリズム
2人の科学者、ボイヤーとムーアは、別のアイデアを思いつきました。 シフト方向を同じに保ちながら、パターンを左から右ではなく右から左にテキストと比較してみませんか。
public static int BoyerMooreHorspoolSimpleSearch(char[] pattern, char[] text) {
int patternSize = pattern.length;
int textSize = text.length;
int i = 0, j = 0;
while ((i + patternSize) <= textSize) {
j = patternSize - 1;
while (text[i + j] == pattern[j]) {
j--;
if (j < 0)
return i;
}
i++;
}
return -1;
}
予想どおり、これはO(m * n)秒の時間で実行されます。 しかし、このアルゴリズムは、発生の実装とアルゴリズムを大幅に高速化する一致ヒューリスティックにつながりました。 より多くのhereを見つけることができます。
2.6. ボイヤー-ムーア-ホースプールアルゴリズム
Boyer-Mooreアルゴリズムのヒューリスティックな実装には多くのバリエーションがあり、最も簡単なものはHorspoolバリエーションです。
このバージョンのアルゴリズムはボイヤー・ムーア・ホースプールと呼ばれ、このバリエーションは負のシフトの問題を解決しました(負のシフト問題については、ボイヤー・ムーアアルゴリズムの説明で読むことができます)。
ボイヤームーアアルゴリズムと同様に、最悪のシナリオの時間計算量はO(m * n)ですが、平均計算量はO(n)です。 スペースの使用量はパターンのサイズに依存しませんが、英語のアルファベットのASCII文字の最大値である256であるアルファベットのサイズにのみ依存します。
public static int BoyerMooreHorspoolSearch(char[] pattern, char[] text) {
int shift[] = new int[256];
for (int k = 0; k < 256; k++) {
shift[k] = pattern.length;
}
for (int k = 0; k < pattern.length - 1; k++){
shift[pattern[k]] = pattern.length - 1 - k;
}
int i = 0, j = 0;
while ((i + pattern.length) <= text.length) {
j = pattern.length - 1;
while (text[i + j] == pattern[j]) {
j -= 1;
if (j < 0)
return i;
}
i = i + shift[text[i + pattern.length - 1]];
}
return -1;
}
4. 結論
この記事では、テキスト検索のためのいくつかのアルゴリズムを紹介しました。 いくつかのアルゴリズムはより強力な数学的背景を必要とするため、各アルゴリズムの下にある主要なアイデアを表現し、簡単な方法で提供することを試みました。
そして、いつものように、ソースコードはover on GitHubで見つけることができます。