16

指定された文字列キーの整数を返す必要がある、次のシグネチャを持つ検索関数を考えてみましょう。

int GetValue(string key) { ... }

さらに、キーと値のマッピング (番号 N) は、関数のソース コードが記述されているときに事前にわかっていることを考慮してください。たとえば、次のようになります。

// N=3
{ "foo", 1 },
{ "bar", 42 },
{ "bazz", 314159 }

したがって、上記の入力に対する関数の有効な (ただし完全ではない!) 実装は次のようになります。

int GetValue(string key)
{
    switch (key)
    {
         case "foo": return 1;
         case "bar": return 42;
         case "bazz": return 314159;
    }

    // Doesn't matter what we do here, control will never come to this point
    throw new Exception();
}

また、特定のキーごとに関数が実行時に呼び出される正確な回数 (C>=1) も事前にわかっています。例えば:

C["foo"] = 1;
C["bar"] = 1;
C["bazz"] = 2;

ただし、そのような呼び出しの順序は不明です。たとえば、上記は実行時に次の一連の呼び出しを記述することができます。

GetValue("foo");
GetValue("bazz");
GetValue("bar");
GetValue("bazz");

呼び出し回数が一致する場合、またはその他のシーケンス。

制限 M もあり、最も便利な単位で指定され、 で使用できるルックアップ テーブルやその他のヘルパー構造体のメモリ上限を定義しますGetValue(構造体は事前に初期化されます。その初期化は複雑さに対してカウントされません)。関数の)。たとえば、M=100 文字、または M=256 sizeof(オブジェクト参照) です。

GetValue問題は、可能な限り高速になるように の本体を記述する方法です。つまり、すべてのGetValue呼び出しの合計時間 (上記のすべての合計数を知っていることに注意してください) は、与えられた N、C に対して最小です。そしてM?

アルゴリズムは、M の妥当な最小値を必要とする場合があります (例: M >= ) char.MaxValue。また、M を何らかの合理的な境界に揃えることも必要になる場合があります。たとえば、2 のべき乗のみである場合などです。また、M が特定の種類の N の関数でなければならないことも必要になる場合があります (たとえば、有効な M=N、または M=2N、...; または有効な M=N、または M=N^2、 ...;など)。

アルゴリズムは、適切な言語またはその他の形式で表現できます。生成されたコードのランタイム パフォーマンスの制約については、生成されたコードGetValueが C#、VB、または Java であると仮定します (実際には、文字列が文字の不変配列として扱われる限り、つまり O(1) の長さと O (1) 索引付け、事前に計算されたその他のデータなし)。また、これを少し単純化するために、すべてのキーに対して C=1 であると仮定する回答は有効と見なされますが、より一般的なケースをカバーする回答が優先されます。

可能なアプローチについてのいくつかの熟考

上記に対する明白な最初の答えは、完全なハッシュを使用することですが、完全なハッシュを見つけるための一般的なアプローチは不完全なようです。たとえば、上記のサンプル データに対して Pearson ハッシュを使用して最小限の完全ハッシュのテーブルを簡単に生成できますが、その場合、 を呼び出すたびに入力キーをGetValueハッシュする必要があり、Pearson ハッシュは必然的に入力文字列全体をスキャンします。しかし、すべてのサンプル キーは実際には 3 番目の文字が異なるため、文字列全体ではなく、3 番目の文字のみをハッシュの入力として使用できます。さらに、M が少なくともchar.MaxValueである必要がある場合、3 番目の文字自体が完全なハッシュになります。

別のキーのセットでは、これはもはや当てはまらないかもしれませんが、正確な答えを得る前に考慮される文字の量を減らすことはまだ可能かもしれません. さらに、最小限の完全なハッシュが文字列全体を検査する必要がある場合は、ハッシュを非最小限にすることで、ルックアップをサブセットに減らすか、そうでなければ高速化することができます (たとえば、より複雑でないハッシュ関数?)。 (つまり、M > N) - 速度のためにスペースを効果的に犠牲にします。

また、従来のハッシュは最初からあまり良い考えではない可能性もありますGetValue。一連の条件として本体を構造化する方が簡単であり、最初に「最も可変性のある」文字 (全体的に変化する文字) をチェックするように配置されます。ほとんどのキー)、正しい答えを決定するために、必要に応じてさらにネストされたチェックを行います。ここでの「分散」は、各キーが検索される回数の影響を受ける可能性があることに注意してください (C)。さらに、ブランチの最良の構造がどのようなものであるべきかは、常に容易に明らかであるとは限りません。たとえば、「最も変化しやすい」文字では、100 個のキーのうち 10 個のキーしか区別できず、残りの 90 個のキーについては 1 回の追加チェックが必要になる場合があります。それらを区別する必要はありません。「最も変化しやすい」キャラクターから始めないでください。目標は、チェックの完全な順序を決定することです。

4

8 に答える 8

4

ボイヤー検索を使用することもできますが、トライの方がはるかに効率的な方法だと思います。Trie を変更して、キーのヒット カウントを 0 にするときに単語を折りたたむことができます。これにより、検索の回数を減らすことができます。得られる最大の利点は、インデックスの配列ルックアップを実行していることです。これは、比較よりもはるかに高速です。

于 2011-07-16T02:49:54.927 に答える
4

事前計算に関しては、メモリの制限について話しましたが、時間の制限もありますか?

私はトライを検討しますが、必ずしも最初の文字から始めるとは限りません。代わりに、検索スペースを最も削減するインデックスを見つけて、それを最初に検討してください。したがって、サンプル ケース ("foo"、"bar"、"bazz") では、3 番目の文字を使用すると、それがどの文字列であるかがすぐにわかります。(常に入力単語の 1 つが与えられることがわかっている場合は、一意の潜在的な一致が見つかったらすぐに戻ることができます。)

ここで、一意の文字列に到達する単一のインデックスがないと仮定すると、その後に参照する文字を決定する必要があります。理論的には、トライを事前に計算して、次に確認する最適な文字を分岐ごとに計算します (たとえば、「3 番目の文字が 'a' の場合、次に 2 番目の文字を確認する必要があります。それが 'o' の場合は、次に最初の文字を確認する必要があります) が、それにはより多くの時間とスペースが必要になる可能性があります。時間 - 1 文字下に移動したため、各ブランチには、最終的な文字列を一意に識別するインデックスが選択される可能性がありますが、毎回異なるインデックスになります。このアプローチで必要なスペースの量は、文字列がどの程度似ているかによって異なり、事前に予測するのは難しい場合があります。可能なすべてのトライ ノードに対してこれを動的に実行できると便利ですが、構築スペースが不足していることが判明した場合は、「このノードの下のすべて」に対して単一の順序を決定します。(したがって、そのノードの下の各ノードに「次の文字インデックス」を保存することはなく、単一のシーケンスのみです。)これが明確でない場合はお知らせください。詳しく説明します...

トライをどのように表現するかは、入力文字の範囲によって異なります。それらがすべて 'a'-'z' の範囲内にある場合、単純な配列はナビゲートが信じられないほど高速であり、使用可能なオプションのほとんどに可能性があるトライ ノードではかなり効率的です。後で、可能性のある分岐が 2 つまたは 3 つしかない場合、メモリが無駄になります。サブブランチの数に応じて最適なタイプのノードを構築できるように、ポリモーフィックな Trie ノード クラスをお勧めします。

これはいずれもカリングを実行しません。迅速なカリングによってどれだけの結果が得られるかは明らかではありません。1つのトライ ノードからの分岐の数が (使い果たされた分岐の削除により) 1 に減少した場合に、その分岐を完全に削除できる場合に役立ちます時間が経つにつれて、これは大きな違いを生む可能性があり、計算するのはそれほど難しいことではありません. 基本的に、トライを作成すると、各分岐が何回行われるかを予測できます。また、トライをナビゲートすると、ナビゲートするときに分岐ごとにそのカウントから 1 を引くことができます。

これが私がこれまで思いついたすべてであり、完全な実装ではありませんが、役に立てば幸いです...

于 2011-07-29T20:05:56.283 に答える
0

ハッシュルーチンのターゲットとなる文字の最小サブセットを決定するための実行可能なアプローチは次のとおりです。

k
は、すべてのキーワードにわたる個別の文字の量です
。cは、キーワードの最大長です
。n
は、例のキーワードの数です(スペース付きの短いキーワードが埋め込まれています)。

"foo "
"bar "
"bazz"

k = 7(f、o、b、a、r、z、)、c = 4、n = 3

これを使用して、検索の下限を計算できます。キーワードを一意に識別するには、少なくともlog_k(n)文字が必要です。log_k(n)> = cの場合、キーワード全体を使用する必要があり、続行する理由はありません。

次に、一度に1つの列を削除し、n個の個別の値が残っているかどうかを確認します。検索を最適化するためのヒューリスティックとして、各列の個別の文字を使用します。

2 2 3 2
f o o .
b a r .
b a z z

明確な文字が最も少ない列を最初に削除します。<= log_k(n)列が残っている場合は、停止できます。オプションで、ビットをランダム化して2番目に低い個別の列を削除するか、削除された列の結果がn個未満の個別の単語である場合に回復を試みることができます。このアルゴリズムは、回復しようとする量に応じて、おおよそO(n!)です。最適なソリューションを見つけることは保証されていませんが、それは良いトレードオフです。

文字のサブセットを取得したら、完全なハッシュを生成するための通常のルーチンに進みます。結果は、最適な完全なハッシュになるはずです。

于 2011-07-16T02:51:27.077 に答える
0

必要なのは、ルックアップテーブルのルックアップテーブルです。メモリコストが問題にならない場合は、すべてを実行できます。

const int POSSIBLE_CHARCODES = 256; //256 for ascii //65536 for unicode 16bit
struct LutMap {
    int value;
    LutMap[POSSIBLE_CHARCODES] next;
}
int GetValue(string key) {
    LutMap root = Global.AlreadyCreatedLutMap;
    for(int x=0; x<key.length; x++) {
        int c = key.charCodeAt(x);
        if(root.next[c] == null) {
            return root.value;
        }
        root = root.next[c];
    }
}
于 2011-07-28T18:02:29.373 に答える
0

正しいハッシュ関数を見つけることがすべてだと思います。キーと値の関係が事前にわかっている限り、分析を行って、要件を満たすハッシュ関数を見つけようとすることができます。あなたが提供した例を取り上げて、入力文字列をバイナリ整数として扱います。

foo  = 0x666F6F (hex value)
bar  = 0x626172
bazz = 0x62617A7A

それらすべてに存在する最後の列は、それぞれ異なります。さらに分析します。

foo  = 0xF = 1111
bar  = 0x2 = 0010
bazz = 0xA = 1010

右に 2 回ビット シフトし、オーバーフローを破棄すると、それぞれに異なる値が得られます。

foo  = 0011
bar  = 0000
bazz = 0010

再び右に 2 回ビット シフトし、新しいバッファにオーバーフローを追加します: foo = 0010 bar = 0000 bazz = 0001

これらを使用して、静的な 3 エントリのルックアップ テーブルをクエリできます。この非常に個人的なハッシュ関数は、ニブル (2)、ビット シフト (2)、ビット シフトと追加 (4)、およびクエリ (1) を取得するために 9 つの非常に基本的な操作が必要になると思います。これらの操作の多くは、巧妙なアセンブリの使用によってさらに圧縮されます。これは、実行時の情報を考慮するよりも高速である可能性があります。

于 2011-07-30T00:15:09.567 に答える
0

TCBを見たことがありますか。おそらく、そこで使用されているアルゴリズムを使用して値を取得できます。あなたが解決しようとしている問題によく似ています。経験から言うと、tcb は私が使用した中で最も高速なキー ストア ルックアップの 1 つです。格納されているキーの数に関係なく、一定のルックアップ時間です。

于 2011-07-30T07:18:21.753 に答える
0

Knuth–Morris–Pratt アルゴリズムの使用を検討してください。

以下のように、指定されたマップを大きな文字列に前処理します

String string = "{foo:1}{bar:42}{bazz:314159}";
int length = string.length();

KMP の前処理時間に応じて、stringがかかりO(length)ます。任意の単語/キーで検索するには、単語/キーの長さがO(w)複雑になります。w

KMPアルゴリズムに 2 つの変更を加える必要があります。

  • キーは、結合された順序で表示される必要がありますstring
  • true/false を返す代わりに、数値を解析して返す必要があります

良いヒントになれば幸いです。

于 2011-07-31T20:18:20.040 に答える
0

テーブルのバイナリ検索は本当にひどいですか? 可能性のある文字列のリストを取得して「最小化」し、並べ替えて、最後にそれらのブロックに対してバイナリ検索を実行します。

最小化とは、それらを必要最小限に減らすことを意味し、一種のカスタム ステミングです。

たとえば、「alfred」、「bob」、「bill」、「joe」という文字列がある場合、「a」、「bi」、「bo」、「j」に分解します。

次に、それらを連続したメモリ ブロックに配置します。次に例を示します。

char *table = "a\0bi\0bo\0j\0"; // last 0 is really redundant..but
char *keys[4];
keys[0] = table;
keys[1] = table + 2;
keys[2] = table + 5;
keys[3] = table + 8;

理想的には、次のように単純に実行すると、コンパイラがこれらすべてを実行します。

keys[0] = "a";
keys[1] = "bi";
keys[2] = "bo";
keys[3] = "j";

しかし、それが本当かどうかはわかりません。

これで、そのテーブルを bsearch できるようになり、キーはできるだけ短くなります。キーの端をたたくと、一致します。そうでない場合は、標準の bsearch アルゴリズムに従います。

目標は、すべてのデータを近づけて、コードを細かく保ち、すべてが CPU キャッシュに収まるようにすることです。プログラムからキーを直接処理できます。前処理や追加は必要ありません。

合理的に分散されたかなり多数のキーの場合、これは非常に高速になると思います。それは、関係する文字列の数に大きく依存します。数値が小さい場合、ハッシュ値などを計算するオーバーヘッドは、このような検索よりも多くなります。より大きな値の場合、それだけの価値があります。それらの数がすべてアルゴリズムなどに依存します。

ただし、メモリが重要である場合、これはおそらく最小のソリューションです。

これには、単純さの利点もあります。

補遺:

「文字列」以外の入力に関する仕様はありません。また、使用する文字列の数、長さ、共通性、使用頻度についても議論されていません。これらはおそらくすべて「ソース」から導き出すことができますが、アルゴリズム設計者が計画することはできません。次のようなものを作成するアルゴリズムを求めています。

inline int GetValue(char *key) {
    return 1234;
}

常に 1 つのキーのみを使用する小規模なプログラムから、何百万もの文字列に対して完全なハッシュ アルゴリズムを作成するプログラムまで。それはかなり難しい注文です。

「可能な限りすべてのパフォーマンスを絞り込む」後の設計は、「ありとあらゆる文字列」よりも入力について詳しく知る必要があります。その問題空間は、あらゆる条件で可能な限り高速にしたい場合、単純に大きすぎます。

非常に長い同一のプレフィックスを持つ文字列を処理するアルゴリズムは、完全にランダムな文字列で機能するアルゴリズムとはまったく異なる場合があります。アルゴリズムは、「キーが「a」で始まる場合、次の 100 文字はすべて a であるため、スキップする」と言うことができます。

しかし、これらの文字列が人間によって供給され、同じ文字の長い文字列を使用していて、そのデータを維持しようとして気が狂っていない場合、アルゴリズムのパフォーマンスが悪いと不満を言うと、「あなたはばかげたことをする、それをしないでください。」しかし、これらの文字列のソースもわかりません。

したがって、アルゴリズムを対象とする問題空間を選択する必要があります。さまざまな制約に対処し、さまざまな状況でより適切に機能するため、表向きは同じことを行うあらゆる種類のアルゴリズムがあります。

ハッシュは高価であり、ハッシュマップのレイアウトは高価です。関連するデータが十分でない場合は、ハッシュよりも優れた手法があります。大量のメモリ バジェットがある場合は、ノードごとに N 個の状態に基づいて巨大なステート マシンを作成できます (N は文字セットのサイズです -- これは指定しません -- BAUDOT? 7 ビット ASCII? UTF-32?) . 状態によって消費されるメモリの量が CPU キャッシュを破壊したり、他のものを圧迫したりしない限り、これは非常に高速に実行されます。

これらすべてのコードを生成することもできますが、コード サイズの制限に達する可能性があります (たとえば、Java には 64K のメソッド バイト コード制限があります)。

ただし、これらの制約は指定しません。そのため、ニーズに合った最もパフォーマンスの高いソリューションを入手するのはちょっと難しいです.

于 2011-07-16T05:13:22.923 に答える