web-dev-qa-db-ja.com

最速の部分文字列検索アルゴリズムとは何ですか?

OK、だから私は問題/要件をより明確に述べるつもりだばかのように聞こえません:

  • 針(パターン)とhaystack(検索するテキスト)は、どちらもCスタイルのヌル終了文字列です。長さ情報は提供されません。必要に応じて、計算する必要があります。
  • 関数は、最初の一致へのポインターを返すか、一致が見つからない場合はNULLを返す必要があります。
  • 失敗のケースは許可されません。つまり、非一定(または大きな定数)のストレージ要件を持つアルゴリズムでは、割り当ての失敗に対してフォールバックケースが必要になります(そして、フォールバックケアのパフォーマンスは、最悪の場合のパフォーマンスに寄与します)。
  • 実装はCで行われますが、コードなしのアルゴリズム(またはそのようなリンク)の適切な説明も問題ありません。

...また、「最速」という意味も:

  • 確定的O(n)ここで、n =干し草の長さ。 (しかし、より堅牢なアルゴリズムと組み合わせて決定論的なO(nm)結果を与える場合、通常はO(n)(たとえばローリングハッシュ)であるアルゴリズムのアイデアを使用することが可能かもしれません)。
  • ナイーブなブルートフォースアルゴリズムよりもパフォーマンスが悪い(測定可能; if (!needle[1])などのクロックは大丈夫です)、特に最も一般的なケースである可能性が非常に高いニードルでは特にそうです。 (無条件の重い前処理のオーバーヘッドは悪いです。また、有望な針を犠牲にして病理学的針の線形係数を改善しようとしています。)
  • 任意の針と干し草の山が与えられた場合、他の広く実装されているアルゴリズムと同等またはそれ以上のパフォーマンス(検索時間が50%以上長くなる)。
  • これらの条件は別として、「最速」オープンエンドの定義は残しておきます。適切な答えは、「最速」を提案しているアプローチを検討する理由を説明する必要があります。

私の現在の実装は、glibcのTwo-Way実装よりも約10%から8倍(入力に応じて)遅くなります。

更新:私の現在の最適なアルゴリズムは次のとおりです:

  • 長さ1の針の場合、strchrを使用します。
  • 長さ2〜4の針の場合、機械語を使用して、次のように2〜4バイトを一度に比較します。ビットシフトを使用して16ビットまたは32ビットの整数で針をプリロードし、 。 haystackのすべてのバイトが1回だけ読み取られ、0(文字列の終わり)と1回の16ビットまたは32ビットの比較に対するチェックが発生します。
  • 4を超える長さの針の場合、ウィンドウの最後のバイトにのみ適用される不良なシフトテーブル(ボイヤームーアなど)を備えた双方向アルゴリズムを使用します。多くの中程度の長さの針の正味損失となる1kbテーブルを初期化するオーバーヘッドを避けるために、シフトテーブルのどのエントリが初期化されるかを示すビット配列(32バイト)を保持します。設定されていないビットは、針に表示されないバイト値に対応します。この場合、針の長さいっぱいのシフトが可能です。

私の心に残っている大きな質問は次のとおりです。

  • 不良シフトテーブルをより有効に使用する方法はありますか? Boyer-Mooreは後方(右から左)にスキャンすることで最大限に活用しますが、Two-Wayでは左から右へのスキャンが必要です。
  • 一般的なケース(メモリ不足または二次のパフォーマンス条件なし)で見つかった2つの実行可能な候補アルゴリズムは、 双方向順序付きアルファベットの文字列マッチング のみです。 。しかし、異なるアルゴリズムが最適であると簡単に検出できるケースはありますか?確かに、スペースアルゴリズムのO(m)mは針の長さ)の多くは、m<100などに使用できます。また、線形時間のみを必要とする可能性のある針の簡単なテストがあれば、最悪の場合は2次のアルゴリズムを使用することも可能です。

ボーナスポイント:

  • Needleとhaystackの両方が整形式のUTF-8であると想定して、パフォーマンスを改善できますか? (さまざまなバイト長の文字では、整形式であるため、針と干し草の間に文字列のアライメント要件が課せられ、不一致のヘッドバイトに遭遇した場合に自動2-4バイトシフトが可能になります。最大の接尾辞計算、適切な接尾辞シフトなどは、すでにさまざまなアルゴリズムを提供していますか?)

注:私はそこにあるアルゴリズムのほとんどをよく知っていますが、実際にどれだけうまく機能しているかはわかりません。ここに参考文献がありますので、人々は私にコメント/回答としてアルゴリズムの参照を与え続けません: http://www-igm.univ-mlv.fr/~lecroq/string/index.html

157
R..

可能性の高い針と干し草のテストライブラリを構築します。総当たりなど、いくつかの検索アルゴリズムのテストのプロファイルを作成します。データに最も適したものを選択してください。

Boyer-Moore は、適切な接尾辞テーブルとともに、悪い文字テーブルを使用します。

Boyer-Moore-Horspool は不良文字テーブルを使用します。

Knuth-Morris-Pratt は部分一致テーブルを使用します。

Rabin-Karp は実行中のハッシュを使用します。

それらはすべてオーバーヘッドを引き換えにさまざまな程度の比較を減らすため、実際のパフォーマンスは針と干し草の両方の平均の長さに依存します。初期オーバーヘッドが多いほど、入力が長いほど良くなります。非常に短い針では、ブルートフォースが勝つ可能性があります。

編集:

塩基対、英語のフレーズ、または単一の単語を見つけるには、別のアルゴリズムが最適かもしれません。すべての入力に最適なアルゴリズムが1つあれば、それは公表されていたはずです。

次の小さなテーブルについて考えてください。疑問符ごとに、最適な検索アルゴリズムが異なる場合があります。

                 short needle     long needle
short haystack         ?               ?
long haystack          ?               ?

これは実際にはグラフであり、各軸に短い入力から長い入力の範囲があります。このようなグラフに各アルゴリズムをプロットした場合、それぞれの署名は異なります。一部のアルゴリズムは、パターンの繰り返しが多く、遺伝子の検索などの使用に影響する可能性があります。全体的なパフォーマンスに影響する他のいくつかの要因は、同じパターンを複数回検索することと、異なるパターンを同時に検索することです。

サンプルセットが必要な場合は、googleやwikipediaなどのサイトをスクレイプし、すべての結果ページからhtmlを削除すると思います。検索サイトの場合は、Wordを入力し、提案された検索フレーズのいずれかを使用します。必要に応じて、いくつかの異なる言語を選択します。 Webページを使用すると、すべてのテキストは短〜中程度になるため、十分なページをマージして長いテキストを取得します。また、パブリックドメインの書籍、法的記録、その他の大きなテキストを見つけることもできます。または、辞書から単語を選択してランダムなコンテンツを生成します。ただし、プロファイリングのポイントは、検索するコンテンツのタイプをテストすることなので、可能であれば実世界のサンプルを使用します。

私は短くて長いあいまいなままにした。針については、短い8文字未満、中程度の64文字未満、1k未満の長さを考えています。干し草の山については、短い2 ^ 10以下、中程度の2 ^ 20以下、長い2 ^ 30文字と考えています。

37
drawnonward

2011年に公開された、 "単純なリアルタイム定数スペース文字列マッチング" ダニー・ブレスラウアー、ロベルト・グロッシ、フィリッポ・ミニョーシ。

更新:

2014年に、著者はこの改善を公開しました: 最適なパックド文字列照合に向けて

25
Mehrdad

http://www-igm.univ-mlv.fr/~lecroq/string/index.html リンクは、最もよく知られ、研究されている文字列マッチングの優れたソースと要約です。アルゴリズム。

ほとんどの検索問題の解決策には、前処理のオーバーヘッド、時間とスペースの要件に関するトレードオフが含まれます。すべての場合に最適なアルゴリズムや実用的なアルゴリズムはありません。

文字列検索用の特定のアルゴリズムを設計することを目的とする場合、私が言わなければならないことの残りを無視します。一般化された文字列検索サービスルーチンを開発する場合は、次を試してください。

すでに参照したアルゴリズムの特定の長所と短所を確認するために時間をかけます。関心のある文字列検索の範囲と範囲をカバーするアルゴリズムのセットを見つける目的でレビューを実行します。次に、指定された入力に最適なアルゴリズムをターゲットとする分類関数に基づいてフロントエンド検索セレクターを構築します。これにより、最も効率的なアルゴリズムを使用してジョブを実行できます。これは、アルゴリズムが特定の検索に非常に適しているが、劣化が少ない場合に特に効果的です。たとえば、ブルートフォースはおそらく長さ1の針に最適ですが、針の長さが長くなるとすぐに低下します。その結果、 sustik-moore algoritim は(小さいアルファベットに対して)より効率的になり、その後、長い針とアルファベットが大きいほど、KMPまたはBoyer-Mooreアルゴリズムの方が適している可能性があります。これらは可能な戦略を説明するための単なる例です。

複数のアルゴリズムによるアプローチは新しいアイデアではありません。いくつかの市販のソート/検索パッケージに採用されていると思います(たとえば、メインフレームで一般的に使用されるSYNCSORTは、いくつかのソートアルゴリズムを実装し、特定の入力に「最適な」ものを選択するためにヒューリスティックを使用します)

各検索アルゴリズムには、たとえば paper が示すように、パフォーマンスに大きな違いをもたらすいくつかのバリエーションがあります。

サービスをベンチマークして、追加の検索戦略が必要な領域を分類するか、セレクター機能をより効果的に調整します。このアプローチは迅速でも簡単でもありませんが、うまくやれば非常に良い結果が得られます。

23
NealB

この議論で技術レポートが引用されているのを見て驚いた。私は、上記のSustik-Mooreという名前のアルゴリズムの著者の1人です。 (私たちは論文でその用語を使用しませんでした。)

ここで強調したいのは、私にとってアルゴリズムの最も興味深い特徴は、各文字が多くても1回しか検査されないことを証明することが非常に簡単であることです。 Boyer-Mooreの以前のバージョンでは、各文字が最大で3回、その後最大で2回検査され、それらの証明がより複雑であることが証明されました(論文の引用を参照)。したがって、この変種を提示/研究する際の教訓的な価値もあります。

また、この論文では、理論的な保証を緩和しながら、効率化に向けたさらなるバリエーションについても説明します。それは短い論文であり、この資料は私の意見では平均的な高校卒業生にとって理解可能であるべきです。

私たちの主な目標は、このバージョンをさらに改善できる他の人に注意を喚起することでした。文字列検索には非常に多くのバリエーションがあり、私たちだけでは、このアイデアがメリットをもたらす可能性のあるすべてを考えることはできません。 (固定テキストと変更パターン、固定パターンの異なるテキスト、前処理可能/不可能、並列実行、大きなテキストの一致するサブセットの検索、エラーの許可、ほぼ一致など)

18
Matyas

最速の部分文字列検索アルゴリズムは、コンテキストに依存します。

  1. アルファベットサイズ(例:DNAと英語)
  2. 針の長さ

2010年の論文 「完全な文字列照合問題:包括的な実験的評価」 は、51のアルゴリズム(さまざまなアルファベットサイズと針の長さ)のランタイムを持つテーブルを提供するため、コンテキストに最適なアルゴリズムを選択できます。

これらのアルゴリズムにはすべて、Cの実装とテストスイートがあります。

http://www.dmi.unict.it/~faro/smart/algorithms.php

15
JDiMatteo

私はそれが古い質問であることを知っていますが、ほとんどの悪いシフト表は単一の文字です。データセットに意味がある場合(特に単語が書かれている場合など)、使用可能なスペースがある場合は、1文字ではなくn-gramで構成される悪いシフトテーブルを使用することで劇的な高速化を実現できます。

4
Timothy Jones

本当に良い質問です。ほんの小さなビットを追加してください...

  1. 誰かがDNA配列マッチングについて話していました。しかし、DNAシーケンスの場合、通常行うことは、haystackのデータ構造(接尾辞配列、接尾辞ツリー、FMインデックスなど)を構築し、それに多くの針を一致させることです。これは別の質問です。

  2. 誰かがさまざまなアルゴリズムのベンチマークを行いたい場合は、本当に素晴らしいでしょう。圧縮と接尾辞配列の構築に関する非常に優れたベンチマークがありますが、文字列のマッチングに関するベンチマークを見たことはありません。潜在的な干し草の候補は、 SACAベンチマーク からのものです。

  3. 数日前に 推奨ページ からBoyer-Moore実装をテストしていました(編集:memmem()のような関数呼び出しが必要ですが、標準関数ではないため、実装することにしましたそれ)。私のベンチマークプログラムはランダムな干し草を使用しています。そのページでのボイヤー・ムーアの実装は、glibcのmemmem()およびMacのstrnstr()よりも何倍も速いようです。興味がある場合、実装は here であり、ベンチマークコードは here です。これは確かに現実的なベンチマークではありませんが、開始点です。

4
user172818

最近、利用可能なさまざまなアルゴリズムのパフォーマンスを測定するための素敵なツールを発見しました。 http://www.dmi.unict.it/~faro/smart/index.php

役に立つかもしれません。また、部分文字列検索アルゴリズムをすばやく呼び出す必要がある場合は、Knuth-Morris-Prattを使用します。

3
Sandeep Giri

高速な「単一の一致する文字の検索」(ala strchr)アルゴリズム。

重要な注意事項:

  • これらの関数は、「(先頭|末尾)ゼロの数/カウント」gccコンパイラ組み込み関数__builtin_ctzを使用します。これらの関数は、この操作を実行する命令(x86、ppc、armなど)があるマシンでのみ高速になる可能性があります。

  • これらの関数は、ターゲットアーキテクチャが32ビットおよび64ビットの非整列ロードを実行できることを前提としています。ターゲットアーキテクチャがこれをサポートしていない場合は、読み取りを適切に調整するための起動ロジックを追加する必要があります。

  • これらの機能はプロセッサに依存しません。ターゲットCPUにベクトル命令が含まれている場合は、(はるかに)改善できる可能性があります。たとえば、以下の strlen 関数はSSE3を使用し、XOR 0以外のバイトを探すためにスキャンされたバイトに簡単に変更できます。 Mac OS X 10.6(x86_64)を実行している2.66GHz Core 2ラップトップで実行されたベンチマーク:

    • strchrの843.433 MB/s
    • findFirstByte64の2656.742 MB/s
    • strlenの13094.479 MB/s

... 32ビットバージョン:

#ifdef __BIG_ENDIAN__
#define findFirstZeroByte32(x) ({ uint32_t _x = (x); _x = ~(((_x & 0x7F7F7F7Fu) + 0x7F7F7F7Fu) | _x | 0x7F7F7F7Fu); (_x == 0u)   ? 0 : (__builtin_clz(_x) >> 3) + 1; })
#else
#define findFirstZeroByte32(x) ({ uint32_t _x = (x); _x = ~(((_x & 0x7F7F7F7Fu) + 0x7F7F7F7Fu) | _x | 0x7F7F7F7Fu);                    (__builtin_ctz(_x) + 1) >> 3; })
#endif

unsigned char *findFirstByte32(unsigned char *ptr, unsigned char byte) {
  uint32_t *ptr32 = (uint32_t *)ptr, firstByte32 = 0u, byteMask32 = (byte) | (byte << 8);
  byteMask32 |= byteMask32 << 16;
  while((firstByte32 = findFirstZeroByte32((*ptr32) ^ byteMask32)) == 0) { ptr32++; }
  return(ptr + ((((unsigned char *)ptr32) - ptr) + firstByte32 - 1));
}

...および64ビットバージョン:

#ifdef __BIG_ENDIAN__
#define findFirstZeroByte64(x) ({ uint64_t _x = (x); _x = ~(((_x & 0x7F7F7F7F7f7f7f7full) + 0x7F7F7F7F7f7f7f7full) | _x | 0x7F7F7F7F7f7f7f7full); (_x == 0ull) ? 0 : (__builtin_clzll(_x) >> 3) + 1; })
#else
#define findFirstZeroByte64(x) ({ uint64_t _x = (x); _x = ~(((_x & 0x7F7F7F7F7f7f7f7full) + 0x7F7F7F7F7f7f7f7full) | _x | 0x7F7F7F7F7f7f7f7full);                    (__builtin_ctzll(_x) + 1) >> 3; })
#endif

unsigned char *findFirstByte64(unsigned char *ptr, unsigned char byte) {
  uint64_t *ptr64 = (uint64_t *)ptr, firstByte64 = 0u, byteMask64 = (byte) | (byte << 8);
  byteMask64 |= byteMask64 << 16;
  byteMask64 |= byteMask64 << 32;
  while((firstByte64 = findFirstZeroByte64((*ptr64) ^ byteMask64)) == 0) { ptr64++; }
  return(ptr + ((((unsigned char *)ptr64) - ptr) + firstByte64 - 1));
}

2011/06/04の編集OPは、このソリューションには「乗り越えられないバグ」があるとコメントで指摘しています。

求められたバイトまたはヌルターミネータを超えて読み取ることができ、マップされていないページまたは読み取り許可なしでページにアクセスできます。文字列関数では、整列しない限り、大量の読み取りを使用できません。

これは技術的には正しいのですが、コメントでOPによって提案された メソッド を含む、1バイトより大きいチャンクで動作するほぼすべてのアルゴリズムに適用されます。

典型的なstrchr実装は単純ではありませんが、指定したものよりもかなり効率的です。最も広く使用されているアルゴリズムについては、この最後を参照してください。 http://graphics.stanford.edu/~seander/bithacks.html#ZeroInWord

また、alignment自体とはまったく関係ありません。確かに、これは使用中の大部分の一般的なアーキテクチャで説明されている動作を引き起こす可能性がありますが、これはマイクロアーキテクチャの実装の詳細と関係があります-アライメントされていない読み取りが4K境界にまたがる場合(再び、通常)、その読み取りはプログラムを引き起こします次の4Kページ境界がマッピングされていない場合、終了フォルト。

しかし、これは答えで与えられたアルゴリズムの「バグ」ではありません。その動作は、strchrstrlenなどの関数が検索のサイズを制限するlength引数を受け入れないためです。 char bytes[1] = {0x55};を検索します。これは、説明の目的で、たまたま4K VMページ境界の最後に配置され、次のページはstrchr(bytes, 0xAA)strchrは一度に1バイトずつの実装です)はまったく同じようにクラッシュします。 strchr関連のいとこstrlenについても同じです。

length引数がないと、高速アルゴリズムからバイト単位のアルゴリズムに切り替えるタイミングを判断する方法がありません。 「バグ」の可能性が高いのは、「割り当てのサイズを超える」ことで、これは技術的にはさまざまなC言語標準に従ってundefined behaviorになり、valgrindなどのエラーとしてフラグが立てられます。

要約すると、これはコードが応答し、OPが指摘するコードに応答するが、バイトチャンクよりも大きい速度で動作するものはすべて、length引数がない場合は「バギー」である可能性が高い「最後の読み取り」のコーナーケースを制御します。

この回答のコードは、ターゲットCPUに高速のctzのような命令がある場合、自然なCPU Wordサイズチャンクの最初のバイトをすばやく見つけることができるカーネルです。正しく整列された自然境界、または何らかの形のlength boundでのみ動作することを確認するなどのことを追加するのは簡単です。これにより、高速カーネルからバイト単位の遅いチェックに切り替えることができます。

OPはコメントにも次のように述べています。

Ctzの最適化に関しては、O(1) tail操作に対してのみ違いがあります。小さな文字列(例:strchr("abc", 'a');でパフォーマンスを向上させることができますが、メジャーサイズの文字列では確かにそうではありません。

この記述が真実かどうかは、問題のマイクロアーキテクチャに大きく依存します。正規の4ステージRISCパイプラインモデルを使用すると、ほぼ間違いなく真実です。しかし、コアの速度がメモリストリーミングの速度を完全に低下させる可能性がある現代のアウトオブオーダーのスーパースカラーCPUに当てはまるかどうかを判断するのは非常に困難です。この場合、「ストリームできるバイト数」に対して「リタイアできる命令の数」に大きなギャップがあることがもっともらしいだけでなく、かなり一般的であるため、「ストリーミングできるバイトごとに廃止できる命令の数」。これが十分に大きい場合は、ctz + shift命令を「無料で」実行できます。

3
johne

Pythonの検索実装 は、コア全体で使用されています。コメントは、圧縮されたボイヤー・ムーア・デルタ1表を使用することを示しています。

私は自分自身で文字列検索でかなり広範な実験を行ってきましたが、それは複数の検索文字列のためのものでした。 Horspool および Bitap のアセンブリ実装は、多くの場合、パターン数が少ない場合は Aho-Corasick などのアルゴリズムに対して独自に保持できます。

3
Matt Joiner

たとえば、4つの異なるアルゴリズムを実装できます。 (経験的に決定される)M分ごとに、現在の実際のデータで4つすべてを実行します。 N回の実行(未定)の統計を累積します。その後、次のM分間勝者のみを使用します。

Winsの統計を記録して、勝つことのないアルゴリズムを新しいものに置き換えることができます。最高のルーチンに最適化の努力を集中します。ハードウェア、データベース、またはデータソースに変更を加えた後は、統計に特に注意してください。可能であれば、その情報を統計ログに含めるので、ログの日付/タイムスタンプから把握する必要はありません。

3
Guy Gordon

質問で言及した双方向アルゴリズム(信じられないことに!)は最近、マルチバイトワードで一度に効率的に動作するように改善されました: 最適なパックドストリングマッチング

私は論文全体を読んでいませんが、彼らは時間のためにSSEであるいくつかの新しい特別なCPU命令(例えばO(1) 4.2に含まれる)に依存しているようです複雑さを主張しますが、それらが利用できない場合、彼らはあまり聞こえないw-bitワードのためにO(log log w)時間でそれらをシミュレートできます。

3
j_random_hacker

Stdlib strstrを使用します。

char *foundit = strstr(haystack, needle);

それは非常に高速で、タイプするのに約5秒しかかかりませんでした。

2
Conrad Meyer

また、パフォーマンスに大きな影響を与える可能性があるため、いくつかのタイプの文字列を含む多様なベンチマークを作成することもできます。アルゴは、自然言語の検索に基づいて差異を実行します(そして、ここでも、異なる形態のために、きめの細かい区別があるかもしれません)、DNA文字列またはランダムな文字列など。

アルファベットのサイズは、針のサイズと同様に、多くのアルゴリズムで役割を果たします。たとえば、Horspoolは英語のテキストには適していますが、アルファベットのサイズが異なるためDNAには悪影響があります。良い接尾辞を導入すると、これが大幅に緩和されます。

絶対に最高かどうかはわかりませんが、 Boyer-Moore については十分な経験があります。

0

これは質問に直接答えませんが、テキストが非常に大きい場合は、テキストを重複するセクションに分割して(パターンの長さで重複)、スレッドを使用してセクションを同時に検索します。最速のアルゴリズムに関して、ボイヤー・ムーア・ホースプールは、ボイヤー・ムーアのバリアントの中で最速ではないとしても最速の1つだと思います。このトピックでは、ボイヤー・ムーアのバリアントをいくつか投稿しました(名前はわかりません) アルゴリズムはBMH(ボイヤー–ムーア–ホースプール)検索よりも高速です

0
Roy Alilin