単一の文字に適した検索アルゴリズムはありますか?


23

KMPやBoyer-Mooreなどの基本的な文字列照合アルゴリズムはいくつか知っていますが、それらはすべて検索前にパターンを分析しますが、1つの文字がある場合、分析することはあまりありません。テキストのすべての文字を比較する単純な検索よりも優れたアルゴリズムはありますか?


13
SIMD命令を投げることはできますが、O(n)を超えることはありません。
CodesInChaos

7
同じ文字列での単一の検索または複数の検索の場合?
クリストフ

KMPは、私が「基本的な」文字列マッチングアルゴリズムと呼ぶものではないことは間違いありません。基本的なものが必要な場合は、Zアルゴリズムを試してください。
Mehrdad

検索アルゴリズムが見ていない文字位置があったとします。そうすると、その位置に針の文字が含まれる文字列と、その位置に異なる文字が含まれる文字列を区別できなくなります。
user253751

回答:


29

最悪のケースはO(N)であることが理解されており、いくつかの非常に優れたマイクロ最適化があります。

単純なメソッドは、各文字に対して文字比較とテキストの終わりの比較を実行します。

使用してセンチネルを(テキストの最後にターゲット文字のすなわちコピー)は文字あたり1との比較の数を減らすことができます。

少しいじるレベルであります:

#define haszero(v)      ( ((v) - 0x01010101UL) & ~(v) & 0x80808080UL )
#define hasvalue(x, n)  ( haszero((x) ^ (~0UL / 255 * (n))) )

ワード内のバイト(x)に特定の値(n)があるかどうかを知るため。

部分式v - 0x01010101ULは、対応するバイトvが0またはより大きい場合、任意のバイトに設定された上位ビットに評価されます0x80

部分式~v & 0x80808080ULはバイトの高ビットセットに評価されますが、バイトのv高ビットセットはありません(したがって、バイトは未満でした0x80)。

これらの2つの部分式(haszero)をAND v演算0x80すると、最初の部分式よりも大きい値によって設定された上位ビットが2番目の部分によってマスクされるため、結果はバイトがゼロの上位ビットセットになります(4月27日、 1987年、アラン・ミクロフト)。

これxで、興味のあるバイト値で埋められた単語()でテストする値()をXORできますn。値をそれ自体とXORすると、バイトがゼロになり、それ以外の場合はゼロ以外になるため、結果をに渡すことができhaszeroます。

これは、一般的なstrchr実装でよく使用されます。

(Stephen M Bennetは、2009年12月13日にこれを提案しました。詳細については、有名なBit Twiddling Hacksを参照してください)。


PS

このコードは1111、次のの任意の組み合わせに対して壊れています0

ハックはブルートフォーステストに合格します(辛抱強く待ってください)。

#include <iostream>
#include <limits>

bool haszero(std::uint32_t v)
{
  return (v - std::uint32_t(0x01010101)) & ~v & std::uint32_t(0x80808080);
}

bool hasvalue(std::uint32_t x, unsigned char n)
{
  return haszero(x ^ (~std::uint32_t(0) / 255 * n));
}

bool hasvalue_slow(std::uint32_t x, unsigned char n)
{
  for (unsigned i(0); i < 32; i += 8)
    if (((x >> i) & 0xFF) == n)
      return true;

  return false;
}

int main()
{
  const std::uint64_t stop(std::numeric_limits<std::uint32_t>::max());

  for (unsigned c(0); c < 256; ++c)
  {
    std::cout << "Testing " << c << std::endl;

    for (std::uint64_t w(0); w != stop; ++w)
    {
      if (w && w % 100000000 == 0)
        std::cout << w * 100 / stop << "%\r" << std::flush;

      const bool h(hasvalue(w, c));
      const bool hs(hasvalue_slow(w, c));

      if (h != hs)
        std::cerr << "hasvalue(" << w << ',' << c << ") is " << h << '\n';
    }
  }

  return 0;
}

仮定を1文字= 1バイトとする答えに対する多くの賛成票。これは現在ではもはや標準ではありません

発言ありがとうございます。

答えは、マルチバイト/可変幅エンコーディングに関するエッセイ以外のものであることが意図されていました:-)

とにかく、上記のアイデア/トリックはMBE(特に自己同期エンコーディング)にある程度適応できると思われます:

  • Johanのコメントに記載されているように、ハックは「簡単に」拡張して2バイトなどに機能させることができます(もちろん、それをあまりにも大きくすることはできません)。
  • マルチバイト文字列内の文字を見つける典型的な関数:
    • strchr/ への呼び出しを含むstrstr(例:GNUlib coreutils mbschr
    • それらがうまく調整されることを期待しています。
  • センチネル技術は少し先見の明をもって使用できます。

1
これはSIMD操作の貧乏人向けバージョンです。
ルスラン

@Ruslan絶対に!これは、効果的なビット調整ハッキングの場合によくあります。
マンリオ

2
いい答えだ。読みやすさの観点から、なぜ0x01010101ULある行と~0UL / 255次の行に書くのか理解できません。そうでなければ、なぜ2つの異なる方法でそれを書くのでしょうか?
hvd

3
これは一度に4バイトをチェックするので便利ですが、#definesはに展開されるため、複数(8?)の命令が必要です( (((x) ^ (0x01010101UL * (n)))) - 0x01010101UL) & ~((x) ^ (0x01010101UL * (n)))) & 0x80808080UL )。シングルバイトの比較は高速ではないでしょうか?
ジェドシャーフ

1
@DocBrownを使用すると、コードを簡単にダブルバイト(ハーフワード)またはニブルなどに使用できます。(私が言及した警告を考慮に入れて)。
ヨハン-モニカの復活

20

特定のテキスト内の単一の文字の出現をすべて検索するテキスト検索アルゴリズムは、テキストの各文字を少なくとも1回読み取る必要があります。また、これは1回限りの検索には十分なので、より良いアルゴリズムはありません(この場合、「線形」またはO(N)と呼ばれる実行時の順序で考えると、Nは文字数検索する)。

ただし、実際の実装では、実行時の順序を全体的に変更せず、実際の実行時間を短縮する多くのマイクロ最適化が確実に可能です。そして、目標が単一のキャラクターのすべての出現を見つけることではなく、最初の出現のみを見つけることである場合、もちろん最初の出現で停止することができます。それでも、その場合でも、最悪の場合は、探している文字がテキストの最後の文字であるため、この目標の最悪の場合の実行時順序はまだO(N)です。


8

「haystack」が複数回検索される場合、ヒストグラムベースのアプローチは非常に高速になります。ヒストグラムが作成された後、答えを見つけるために必要なのはポインター検索だけです。

検索したパターンが存在するかどうかだけを知る必要がある場合は、簡単なカウンターが役立ちます。干し草の山で各文字が見つかった位置、または最初に出現した位置を含めるように拡張できます。

string haystack = "agtuhvrth";
array<int, 256> histogram{0};
for(character: haystack)
     ++histogram[character];

if(histogram['a'])
    // a belongs to haystack

1

この非常に同じ文字列内の文字を複数回検索する必要がある場合、可能なアプローチは、文字列をより小さな部分に、おそらく再帰的に分割し、これらの各部分にブルームフィルターを使用することです。

ブルームフィルターを使用すると、文字列のフィルターで「表示」される部分に文字が含まれていないかどうかを確認できるため、文字の検索中に一部の部分をスキップできます。

例:次の文字列では、4つの部分(各11文字の長さ)に分割し、各部分にブルームフィルタ(おそらく4バイトの大きさ)をその部分の文字で埋めることができます。

The quick brown fox jumps over the lazy dog 
          |          |          |          |

たとえば、キャラクターの検索を高速化できます。aブルームフィルターに適切なハッシュ関数を使用すると、高い確率で、1番目、2番目、3番目のいずれの部分も検索する必要がないことがわかります。したがって、33文字をチェックする必要がなくなり、代わりに16バイトのみをチェックする必要があります(4ブルームフィルターの場合)。これはO(n)、定数(分数)係数を使用するだけです(これを有効にするには、検索文字のハッシュ関数を計算するオーバーヘッドを最小限に抑えるために、より大きな部分を選択する必要があります)。

再帰的なツリーのようなアプローチを使用すると、次のようになりますO(log n)

The quick brown fox jumps over the lazy dog 
   |   |   |   |   |   |   |   |---|-X-|   |  (1 Byte)
       |       |       |       |---X---|----  (2 Byte)
               |               |-----X------  (3 Byte)
-------------------------------|-----X------  (4 Byte)
---------------------X---------------------|  (5 Byte)

この構成では、チェックする必要があります(再び、幸運になり、フィルターの1つから誤検知がなかったと仮定します)。

5 + 2*4 + 3 + 2*2 + 2*1 bytes

最後の部分に到達するまで(を見つけるまで3文字をチェックする必要があるa)。

良い(上記のように)細分化スキームを使用すると、かなり良い結果が得られます。(注:偽陽性の可能性を低くするために、例に示すように、ツリーのルートにあるブルームフィルターは葉に近いよりも大きくする必要があります)


親愛なるダウンボーター、私の答えが役に立たないと思う理由を説明してください。
ダニエルジュール

1

文字列が複数回検索される場合(通常の「検索」問題)、解決策はO(1)になります。解決策は、インデックスを作成することです。

例:

Map。Keyは文字で、Valueは文字列内のその文字のインデックスのリストです。

これにより、単一のマップルックアップで答えが得られます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.