16

数週間前、大量のテキスト内のパターンを検索するための効率的なアルゴリズムの作成について、Stackoverflowに質問を投げかけました。現在、文字列関数indexOfを使用して検索を行っています。1つの提案は、代わりにRabin-Karpを使用することでした。次のようにRabin-Karpの実装をテストするために、次のように小さなテストプログラムを作成しました。

public static void main(String[] args) {
    String test = "Mary had a little lamb whose fleece was white as snow";

    String p = "was";
     long start  = Calendar.getInstance().getTimeInMillis();
     for (int x = 0; x < 200000; x++)
         test.indexOf(p);
     long end = Calendar.getInstance().getTimeInMillis();
     end = end -start;
     System.out.println("Standard Java Time->"+end);

    RabinKarp searcher = new RabinKarp("was");
    start  = Calendar.getInstance().getTimeInMillis();
    for (int x = 0; x < 200000; x++)
    searcher.search(test);
    end = Calendar.getInstance().getTimeInMillis();
    end = end -start;
    System.out.println("Rabin Karp time->"+end);

}

そして、これが私が使用しているRabin-Karpの実装です。

import java.math.BigInteger;
import java.util.Random;

public class RabinKarp {
private String pat; // the pattern // needed only for Las Vegas
private long patHash; // pattern hash value
private int M; // pattern length
private long Q; // a large prime, small enough to avoid long overflow
private int R; // radix
private long RM; // R^(M-1) % Q
static private long dochash = -1L;

public RabinKarp(int R, char[] pattern) {
    throw new RuntimeException("Operation not supported yet");
}

public RabinKarp(String pat) {
    this.pat = pat; // save pattern (needed only for Las Vegas)
    R = 256;
    M = pat.length();
    Q = longRandomPrime();

    // precompute R^(M-1) % Q for use in removing leading digit
    RM = 1;
    for (int i = 1; i <= M - 1; i++)
        RM = (R * RM) % Q;
    patHash = hash(pat, M);
}

// Compute hash for key[0..M-1].
private long hash(String key, int M) {
    long h = 0;
    for (int j = 0; j < M; j++)
        h = (R * h + key.charAt(j)) % Q;
    return h;
}

// Las Vegas version: does pat[] match txt[i..i-M+1] ?
private boolean check(String txt, int i) {
    for (int j = 0; j < M; j++)
        if (pat.charAt(j) != txt.charAt(i + j))
            return false;
    return true;
}

// check for exact match
public int search(String txt) {
    int N = txt.length();
    if (N < M)
        return -1;
    long txtHash;
    if (dochash == -1L) {
        txtHash = hash(txt, M);
        dochash = txtHash;
    } else
        txtHash = dochash;

    // check for match at offset 0
    if ((patHash == txtHash) && check(txt, 0))
        return 0;

    // check for hash match; if hash match, check for exact match
    for (int i = M; i < N; i++) {
        // Remove leading digit, add trailing digit, check for match.
        txtHash = (txtHash + Q - RM * txt.charAt(i - M) % Q) % Q;
        txtHash = (txtHash * R + txt.charAt(i)) % Q;

        // match
        int offset = i - M + 1;
        if ((patHash == txtHash) && check(txt, offset))
            return offset;
    }

    // no match
    return -1; // was N
}

// a random 31-bit prime
private static long longRandomPrime() {
    BigInteger prime = new BigInteger(31, new Random());
    return prime.longValue();
}

// test client

}

Rabin-Karpの実装は、探している文字列の正しいオフセットを返すという点で機能します。しかし、私が驚いたのは、テストプログラムを実行したときに発生したタイミング統計です。はい、どうぞ:

Standard Java Time->39
Rabin Karp time->409

これは本当に驚きでした。Rabin-Karp(少なくともここで実装されている)は、標準のjava indexOf String関数よりも高速ではないだけでなく、桁違いに低速です。何が悪いのかわかりません(もしあれば)。誰かがこれについて考えていますか?

ありがとう、

エリオット

4

7 に答える 7

20

私は以前にこの質問に答えましたが、エリオットは私が単に間違っていると指摘しました. コミュニティにお詫び申し上げます。

String.indexOf コードについて魔法のようなことは何もありません。ネイティブに最適化されたり、そのようなものではありません。String ソース コードから indexOf メソッドをコピーすると、同じようにすばやく実行できます。

ここにあるのは、O() の効率と実際の効率の差です。長さ N の String と長さ M のパターンの Rabin-Karp、Rabin-Karp は O(N+M) で、最悪の場合は O(NM) です。調べてみると、String.indexOf() にも O(N+M) のベストケースと O(NM) のワーストケースがあります。

テキストにパターンの開始部分との部分一致が多数含まれている場合、Rabin-Karp はベスト ケースのパフォーマンスに近い状態を保ちますが、String.indexOf はそうではありません。たとえば、上記のコードを (今回は適切に :-)) 100 万個の '0' の後に単一の '1' をテストし、1000 個の '0' の後に単一の '1' を検索しました。これにより、String.indexOf が最悪の場合のパフォーマンスに追い込まれました。この非常に縮退したテストでは、Rabin-Karp アルゴリズムは indexOf よりも約 15 倍高速でした。

自然言語テキストの場合、Rabin-Karp は最良のケースに近く、indexOf はわずかに低下するだけです。したがって、決定要因は、各ステップで実行される操作の複雑さです。

最も内側のループで、indexOf は一致する最初の文字をスキャンします。各反復では、次のことを行う必要があります。

  • ループカウンターをインクリメントする
  • 2 つの論理テストを実行する
  • 1 つの配列アクセスを行う

Rabin-Karp では、各反復で次のことを行う必要があります。

  • ループカウンターをインクリメントする
  • 2 つの論理テストを実行する
  • 2 つの配列アクセスを行う (実際には 2 つのメソッド呼び出し)
  • 上記の9つの数値演算が必要なハッシュを更新します

したがって、反復ごとに Rabin-Karp はますます遅れをとっていきます。ハッシュ アルゴリズムを単純化して XOR 文字だけにしようとしましたが、まだ余分な配列アクセスと 2 つの数値演算があったため、それでも遅くなりました。

さらに、一致が見つかった場合、Rabin-Karp はハッシュの一致のみを知っているため、すべての文字をテストする必要がありますが、indexOf は最初の文字の一致を既に知っているため、実行するテストが 1 つ少なくなります。

Rabin-Karp が剽窃の検出に使用されていることをウィキペディアで読んだので、私は聖書のルース書を取り、すべての句読点を削除し、すべてを小文字にして、10000 文字弱を残しました。次に、テキストの最後近くにある「andthewomenherneighboursgaveitaname」を検索しました。String.indexOf は、XOR ハッシュのみを使用した場合でも、さらに高速でした。ただし、String のプライベートな内部文字配列にアクセスできるという String.indexOfs の利点を取り除き、文字配列をコピーするように強制すると、最終的に Rabin-Karp は本当に高速になりました。

ただし、ルース書には 213 個の「and」と 28 個の「andthe」があるため、意図的にそのテキストを選択しました。代わりに、最後の文字「ursgaveitaname」だけを検索した場合、テキストには「urs」が 3 つしかないため、indexOf は最良のケースに近くなり、再び競争に勝ちます。

より公正なテストとして、テキストの後半からランダムに 20 文字の文字列を選択し、時間を計りました。Rabin-Karp は、String クラスの外部で実行される indexOf アルゴリズムよりも約 20% 遅く、実際の indexOf アルゴリズムよりも 70% 遅くなりました。したがって、おそらく適切であると思われるユースケースであっても、最良の選択ではありませんでした。

では、ラビン・カープは何の役に立つのでしょうか? 検索するテキストの長さや性質に関係なく、比較するすべての文字で遅くなります。どのハッシュ関数を選択しても、追加の配列アクセスと少なくとも 2 つの数値演算を行う必要があります。より複雑なハッシュ関数を使用すると、誤った一致が少なくなりますが、より多くの数値演算子が必要になります。Rabin-Karp が追いつくことは絶対にありません。

上で示したように、頻繁に繰り返されるテキスト ブロックが前に付いた一致を見つける必要がある場合、indexOf は遅くなる可能性がありますが、それを行っていることがわかっている場合は、indexOf を使用してテキストを検索した方がよいように見えます。プレフィックスなしで、プレフィックスが存在するかどうかを確認します。

今日の調査によると、Rabin Karp の追加の複雑さがいつ報われるかはわかりません。

于 2012-03-17T00:24:55.650 に答える
6

これがjava.lang.Stringのソースです。indexOfは1770行目です。

私の疑惑は、このような短い入力文字列で使用しているため、java.lang.StringのindexOfの単純な実装よりも、Rabin-Karpアルゴリズムの余分なオーバーヘッドがあり、アルゴリズムの実際のパフォーマンスが表示されていないことです。パフォーマンスを比較するために、はるかに長い入力文字列で試してみることをお勧めします。

于 2012-03-16T16:51:24.310 に答える
5

私の理解では、Rabin Karp は、複数の単語/フレーズをテキスト ブロックで検索する場合に最適です。

虐待的な言葉にフラグを立てるための悪い言葉の検索について考えてみてください。

派生語を含む 2000 語のリストがある場合、検索しようとしている単語ごとに 1 つずつ、indexOf を 2000 回呼び出す必要があります。

RabinKarp は、検索を逆に行うことでこれを支援します。2000 語それぞれの 4 文字のハッシュを作成し、それを高速ルックアップで辞書に入れます。

ここで、検索テキストの 4 文字ごとに、ハッシュして辞書と照合します。

ご覧のとおり、検索は逆になっています。代わりに、2000 語を検索して一致する可能性を探しています。次に、辞書から文字列を取得し、equals を実行して確認します。

また、文字列の一致ではなく辞書を検索しているため、この方法では検索が高速になります。

ここで、すべての indexOf 検索を実行する最悪のシナリオを想像してみてください。チェックする最後の単語が一致する...

RabinKarp のウィキペディアの記事では、あなたが説明する状況では劣っているとさえ述べています。;-) http://en.wikipedia.org/wiki/Rabin-Karp_algorithm

于 2012-05-17T08:55:02.730 に答える
1

しかし、これは当然のことです。
まず第一に、あなたのテスト入力は取るに足らないものです。

indexOfwas小さなバッファ(Stringの内部配列`)を検索するインデックスを返しますがchar、Rabin-Karpはデータを機能させるために前処理を行う必要があり、これには余分な時間がかかります。

違いを確認するには、式を見つけるために非常に大きなテキストでテストする必要があります。

また、より高度な文字列検索アルゴリズムを使用する場合、「高価な」セットアップ/前処理を行って、非常に高速な検索を提供できることにも注意してください。
あなたの場合、あなたはただwas文の中を検索します。いずれにせよ、あなたは常に入力を考慮に入れるべきです

于 2012-03-16T16:51:20.620 に答える
1

詳細を調べずに、2つの理由が思い浮かびます。

于 2012-03-16T16:55:21.540 に答える
0

この種の検索では、Knuth-Morris-Pratt の方がパフォーマンスが良い場合があります。特に、部分文字列が単に文字を繰り返さない場合、KMP は indexOf() よりも優れているはずです。最悪の場合(すべて同じ文字の文字列)、同じになります。

于 2014-12-09T08:23:27.160 に答える
0

単純に長い静的文字列を試すだけでなく、ランダムな長い文字列を生成し、検索対象を毎回ランダムな場所に挿入してみてください。ランダム化しないと、固定の結果が表示されindexOfます。

EDITED: ランダムは間違った概念です。ほとんどのテキストは本当にランダムではありません。ただし、同じ文字列を複数回テストするだけでなく、効果的にするには、さまざまな長い文字列が必要になります。さらに大きなテキストソースなどから「ランダムな」大きな文字列を抽出する方法があると確信しています。

于 2012-03-16T16:56:22.780 に答える