KMPやBoyer-Mooreなどの基本的な文字列照合アルゴリズムはいくつか知っていますが、それらはすべて検索前にパターンを分析しますが、1つの文字がある場合、分析することはあまりありません。テキストのすべての文字を比較する単純な検索よりも優れたアルゴリズムはありますか?
KMPやBoyer-Mooreなどの基本的な文字列照合アルゴリズムはいくつか知っていますが、それらはすべて検索前にパターンを分析しますが、1つの文字がある場合、分析することはあまりありません。テキストのすべての文字を比較する単純な検索よりも優れたアルゴリズムはありますか?
回答:
最悪のケースは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(特に自己同期エンコーディング)にある程度適応できると思われます:
strchr
/ への呼び出しを含むstrstr
(例:GNUlib coreutils mbschr)0x01010101UL
ある行と~0UL / 255
次の行に書くのか理解できません。そうでなければ、なぜ2つの異なる方法でそれを書くのでしょうか?
#define
sはに展開されるため、複数(8?)の命令が必要です( (((x) ^ (0x01010101UL * (n)))) - 0x01010101UL) & ~((x) ^ (0x01010101UL * (n)))) & 0x80808080UL )
。シングルバイトの比較は高速ではないでしょうか?
特定のテキスト内の単一の文字の出現をすべて検索するテキスト検索アルゴリズムは、テキストの各文字を少なくとも1回読み取る必要があります。また、これは1回限りの検索には十分なので、より良いアルゴリズムはありません(この場合、「線形」またはO(N)と呼ばれる実行時の順序で考えると、Nは文字数検索する)。
ただし、実際の実装では、実行時の順序を全体的に変更せず、実際の実行時間を短縮する多くのマイクロ最適化が確実に可能です。そして、目標が単一のキャラクターのすべての出現を見つけることではなく、最初の出現のみを見つけることである場合、もちろん最初の出現で停止することができます。それでも、その場合でも、最悪の場合は、探している文字がテキストの最後の文字であるため、この目標の最悪の場合の実行時順序はまだO(N)です。
「haystack」が複数回検索される場合、ヒストグラムベースのアプローチは非常に高速になります。ヒストグラムが作成された後、答えを見つけるために必要なのはポインター検索だけです。
検索したパターンが存在するかどうかだけを知る必要がある場合は、簡単なカウンターが役立ちます。干し草の山で各文字が見つかった位置、または最初に出現した位置を含めるように拡張できます。
string haystack = "agtuhvrth";
array<int, 256> histogram{0};
for(character: haystack)
++histogram[character];
if(histogram['a'])
// a belongs to haystack
この非常に同じ文字列内の文字を複数回検索する必要がある場合、可能なアプローチは、文字列をより小さな部分に、おそらく再帰的に分割し、これらの各部分にブルームフィルターを使用することです。
ブルームフィルターを使用すると、文字列のフィルターで「表示」される部分に文字が含まれていないかどうかを確認できるため、文字の検索中に一部の部分をスキップできます。
例:次の文字列では、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
)。
良い(上記のように)細分化スキームを使用すると、かなり良い結果が得られます。(注:偽陽性の可能性を低くするために、例に示すように、ツリーのルートにあるブルームフィルターは葉に近いよりも大きくする必要があります)