千の電話番号を保存する最も効率的な方法


93

これはグーグルのインタビューの質問です:

保存する電話番号は約10桁で、約1000あります。あなたは、それぞれの最初の5桁が数千の数字で同じであると仮定することができます。次の操作を実行する必要があります。指定された番号が存在するかどうかを検索します。b。すべての番号を印刷

これを行う最も効率的なスペース節約方法は何ですか?

私はハッシュテーブルとハフマンコーディングに答えましたが、面接担当者は私が正しい方向に進んでいないと述べました。ここで私を助けてください。

接尾辞トライを使用できますか?

理想的には、1000の数値を格納するには数値あたり4バイトがかかるため、1000の数値を格納するには全体で4000バイトがかかります。量的には、ストレージを4000バイト未満に減らしたいと思っています。これは、面接官が私に説明したものです。


28
私は通常のデータベースを使用してそれらをテキストとして、さらには数千/数百万として保存でき、ルックアップ操作は依然として非常に高速であると答えます。今後、国際番号をサポートするためにシステム全体をやり直さなければならない場合や、「0」で始まる電話番号が表示されるようになった場合、または政府が電話番号の形式などを変更します。
Thomas Bonini

1
@AndreasBonini:GoogleやFacebookのような会社で面接していない限り、私はおそらくその答えを出すでしょう。たとえばpostgresも試してみましたが、これらがgoogleが必要とするデータスループットを削減するかどうかはわかりません。
LiKao、2011年

1
@LiKao:OPが「約1000の数値」を具体的に述べていることを覚えておいてください
Thomas Bonini

@AndreasBonini:確かに、テストでもあった可能性があります。インタビュイーはそのような制約を正しく解釈し、これに従って最適なソリューションを選択することを知っています。
LiKao、2011年

4
この質問の「効率的」は本当に定義する必要があります-どの方法で効率的ですか?スペース、時間、両方?
matt b

回答:


36

これがaixの答えの改善です。データ構造に3つの「層」を使用することを検討してください。最初の層は最初の5桁(17ビット)の定数です。ここからは、各電話番号の残りの5桁のみが残ります。これらの残りの5桁を17ビットの2進整数と見なし、1つの方法を使用してそれらのビットのkを保存し、別の方法では17- k = mを使用して、必要なスペースを最小限に抑えるために最後にkを決定します。

最初に電話番号を並べ替えます(すべて5桁の10進数に削減されます)。次に、最初のmビットで構成される2進数がすべて0である電話番号の数を数えます。最初のmビットが最大で0 ... 01の電話番号の数、最初のmの電話番号の数を数えます。ビットは最大で0 ... 10など、最初のmビットが1 ... 11 である電話番号のカウントまで-この最後のカウントは1000(10進数)です。このようなカウントは2 ^ mあり、各カウントは最大で1000です。最後のカウントを省略すると(とにかく1000であることがわかっているため)、これらのすべての数を(2 ^ m -1)の連続したブロックに格納できます。* 10ビット。(1024未満の数値を格納するには10ビットで十分です。)

すべての(縮小された)電話番号の最後のkビットは、連続してメモリに格納されます。したがって、kが7の場合、このメモリブロックの最初の7ビット(ビット0〜6)は最初の(縮小された)電話番号の最後の7ビットに対応し、ビット7〜13は最後の7ビットに対応します。 2番目の(短縮された)電話番号など。これには、合計17 +(2 ^(17- k)-1)* 10 + 1000 * kの 1000 * kビットが必要です。これは、k = 10で最小11287に達するため、すべての電話番号をceil( 11287/8)= 1411バイト。

たとえば、1111111(binary)で始まる数字がないことを確認することで、追加のスペースを節約できます。これは、これで始まる最小の数字が130048であり、10進数が5桁しかないためです。これにより、メモリの最初のブロックからいくつかのエントリを削ることができます。2^ m -1カウントの代わりに、ceil(99999/2 ^ k)だけが必要です。つまり、式は

17 + ceil(99999/2 ^ k)* 10 + 1000 * k

k = 9とk = 10、またはceil(10997/8)= 1375バイトの両方で驚くほど十分に最小値の10997を達成します。

特定の電話番号がセットに含まれているかどうかを知りたい場合は、最初の2桁の2進数が、保存した5桁と一致するかどうかを最初に確認します。次に、残りの5桁を上位のm = 7ビット(つまり、mビットの数値M)と下位のk = 10ビット(数値K)に分割します。現在、数見つける[M-1]第れる減少電話番号のm個の数字が最大であるM 1、および数- [M]最初れる減少電話番号のm個の数字が最大であるMを、どちらもビットの最初のブロックから。次に、a[M-1]番目との[M]は番目のシーケンスのk我々が見つけるかどうかを確認するためにメモリの第2ブロック内のビットKを、最悪の場合、そのようなシーケンスは1000あるため、バイナリ検索を使用すると、O(log 1000)操作で終了できます。

私はアクセスどこすべて1000年の数字を印刷するための擬似コードは、以下のK「番目のkビットとしてメモリの最初のブロックのエントリ[K]とM」番目のMとしてメモリの第2ブロックのビットエントリB [M] (これらは両方とも、書き出すのが面倒な数ビットの操作を必要とします)。最初の5桁は数字cです。

i := 0;
for K from 0 to ceil(99999 / 2^k) do
  while i < a[K] do
    print(c * 10^5 + K * 2^k + b[i]);
    i := i + 1;
  end do;
end do;

たぶん、K = ceil(99999/2 ^ k)の境界のケースに問題があるかもしれませんが、それは簡単に修正できます。

最後に、エントロピーの観点から、すべての10 ^ 5未満の10 ^ 3正整数のサブセットをceil(log [2](binomial(10 ^ 5、10 ^ 3))より少ない数で格納することはできません。 )=8073。最初の5桁に必要な17を含めても、10997-8090 = 2907ビットのギャップがあります。比較的効率的に番号にアクセスできるより良い解決策があるかどうかを確認することは興味深い課題です。


4
ここで説明しているデータ構造は、実際には非常に効率的なバージョンのtrieであり、インデックス作成に必要な量だけを使用し、2つのレベルのみを使用します。実際には、これがより多くのレベルでトライを打つことができるかどうかを確認するのは良いことですが、これは数字の分布に大きく依存すると思います(実際のライブ電話番号は完全にランダムではなく、ほとんどです)。
LiKao、2011年

こんにちは、エリック、他の選択肢を見てみたいとおっしゃっていたので、私の解決策をチェックしてください。それは8,580ビットでそれを解決します。これは理論上の最小値からわずか490ビットです。個々の数値を検索するのは少し非効率的ですが、ストレージは非常にコンパクトです。
Briguy37 2011年

1
正気なインタビュアーは、「複雑なカスタムデータベース」ではなく「トライ」という答えを好むと思います。133tのハッキングスキルを見せたい場合は、「必要に応じて、この特殊なケースに特定のツリーアルゴリズムを作成することも可能です」と追加できます。
KarlP、2011年

こんにちは、5桁で17ビットを格納する方法を教えてください。
Tushar Banne

@tushar 5桁は、00000から99999までの数値をエンコードします。その数を2進数で表します。2 ^ 17 = 131072なので、17ビットで十分ですが、16ビットでは不十分です。
Erik P.

43

以下では、数値を(文字列ではなく)整数変数として扱います。

  1. 番号を並べ替えます。
  2. 各数値を最初の5桁と最後の5桁に分割します。
  3. 最初の5桁はすべての数字で同じなので、一度だけ保存します。これには、17ビットのストレージが必要です。
  4. 各番号の最後の5桁を個別に保存します。これには、数値ごとに17ビットが必要です。

要約すると、最初の17ビットは共通のプレフィックスであり、後続の17ビットのグループは、昇順で格納されている各数値の最後の5桁です。

合計すると、1000の番号は2128バイト、つまり10桁の電話番号あたり17.017ビットです。

検索はO(log n)(バイナリ検索)であり、完全な列挙はO(n)です。


ええと、スペースの複雑さはどこにありますか?
aioobe

トライを構築する場合のO(n k)と比較して、ソートに(O(log(n)* n k)(kは長さ)の場合)構築に時間がかかりすぎます。また、共通の長いプレフィックスが個別に保存されるため、スペースが最適とはほど遠い。検索時間も最適ではありません。このような文字列データの場合、検索を支配する数値の長さを忘れがちです。つまり、バイナリ検索はO(log(n)* k)ですが、トライはO(k)のみを必要とします。kが一定の場合、これらの式を減らすことができますが、これは文字列を格納するデータ構造について推論するときに一般的な問題を示すためです。
LiKao、2011年

@LiKao:文字列について何か言ったのは誰ですか?私は整数変数のみを扱っているためk、無関係です。
NPE、2011年

1
わかりました、私はその時の答えを読み違えました。それでも、共通部品は一緒に保管されないため、スペース効率についてのポイントが残っています。5桁の数字のうち1000の場合、共通のプレフィックスがかなり存在するため、これらを減らすと非常に役立ちます。また、数値の場合、文字列のO(log(n))とO(k)があり、さらに高速です。
LiKao、2011年

1
@Geek:17ビットの1001グループは17017ビットまたは2128バイトです(一部変更あり)。
NPE

22

http://en.wikipedia.org/wiki/Acyclic_deterministic_finite_automaton

私はかつて彼らがデータ構造について尋ねたインタビューを持っていました。「配列」を忘れました。


1
+1は間違いなく進むべき道です。私は学生のときに、ライブラリツリーや字句検索ツリーなどの名前でこれを学びました(その古い名前を覚えている人がいれば教えてください)。
Valmond

6
これは4000バイトの要件を満たしていません。ポインターストレージのみの場合、最悪のシナリオでは、次のレベルへの1〜4番目の葉には1つのポインター、5番目には10個のポインター、6番目には100個、7番目、8番目、および9番目のレベルには1000個のポインターが必要です。 、これによりポインターの合計は3114になります。これにより、ポインターが指すのに必要な少なくとも3114の異なるメモリロケーションが得られます。つまり、各ポインターに少なくとも12ビットが必要になります。12 * 3114 = 37368ビット= 4671バイト> 4000バイトであり、これは、各リーフの値をどのように表すかについてもわかりません
Briguy37 2011年

16

おそらく、Trieの圧縮バージョン(@Mishaによって提案されたDAWG)の使用を検討するでしょう。

これは、すべてに共通のプレフィックスがあるという事実を自動的に利用します。

検索は一定時間で実行され、印刷は線形時間で実行されます。


問題は、データを保存する最もスペース効率の良い方法についてです。この方法で1000電話番号に必要なスペースの見積もりを教えていただけませんか。ありがとう。
NPE、2011年

トライのスペースは最大でO(n * k)です。nは文字列の数、kは各文字列の長さです。数字を表すのに8ビット文字は必要ないことを考慮して、16進数のインデックスを4つ、16進数のインデックスを1つ、残りのビットに1つを保存することをお勧めします。この方法では、数値あたり最大17ビットが必要です。このコーディングを使用すると、すべてのレベルですべてのレベルでクラッシュが発生するため、実際にはこれを下回ることができます。1000個の数値を格納することを期待して、最初のレベルでの衝突のためにすでに合計250ビットを保存できます。サンプルデータで正しいコーディングをテストするのが最善です。
LiKao、2011年

@LiKao、そうです。たとえば、1000の数値は100を超える最後の2桁を超えることはできないので、トライは最後のレベルで大幅に折りたたむことができます。
aioobe

@aioobe:子がないため、最終レベルで葉がつぶれる可能性があります。ただし、2番目から最後のレベルの葉には2 ^ 10 = 1024の状態が必要です(各最後の桁がオンまたはオフになる可能性があります)。この場合、数値が1000しかないため、これを削減することはできません。これは、必要なリーフが5 + 10 + 100 + 1000 + 1000 + 10 = 2125になり、それぞれに必要な12バイトが変更されない一方で、最悪の場合のポインターの数は3114(Mishaの回答に関する私のコメントを参照)に留まることを意味しますポインタ。したがって、これは依然として、ポインタのみを考慮した場合、トライソリューションを4671バイトに配置します。
Briguy37 2011年

@ Briguy37、「最後の各桁がオンまたはオフになる可能性がある」という引数が得られるかどうかはわかりません。すべての数字は10桁ですよね?
aioobe 2011年

15

私は以前にこの問題について聞いたことがありますが(最初の5桁が同じであるという前提はありません)、それを行う最も簡単な方法はRice Codingでした:

1)順序は関係ないので、並べ替えて、連続する値の差だけを保存できます。私たちの場合、平均差は100.000 / 1000 = 100になります

2)ライスコード(ベース128または64)またはGolombコード(ベース100)を使用して違いをエンコードします。

編集: 128をベースにしたライスコーディングの推定(最良の結果が得られるためではなく、計算が容易になるため):

最初の値をそのまま保存します(32ビット)。
残りの999の値は差異です(これらの値は小さく、平均で100であると予想されます)。

単項値value / 128(可変ビット数+ターミネータとして1ビット)(7ビット)の
2進値value % 128

VBL可変ビット数の下限を(なんとか呼んでみましょう)推定する必要があります。
下限:幸運であり、ベース(この場合は128)よりも大きな差がないことを考慮してください。これは、0の追加ビットを与えることを意味します。
上限:baseよりも小さいすべての差分は数値のバイナリ部分でエンコードされるため、単項でエンコードする必要がある最大数は100000/128 = 781.25です(ほとんどの差分がゼロであるとは想定されていないため、さらに少なくなります)。 )。

したがって、結果は32 + 999 *(1 + 7)+ variable(0..782)ビット= 1003 + variable(0..98)バイトになります。


エンコード方法と最終的なサイズ計算について詳しく教えてください。1101バイトまたは8808ビットは、理論上の制限である8091ビットに非常に近いようです。そのため、実際にこのようなことを実現できることは非常に驚きです。
LiKao、2011年

32 + 999 * (1 + 7 + variable(0..782))ビットではないでしょうか?各999の数値には、の表現が必要ですvalue / 128
カークブロードハースト

1
@カーク:いいえ、それらすべてが5桁の範囲内にある場合。これは、これらすべての差の合計(最初の値とN番目の値の間ではなく、連続する値の間の差をエンコードすることを忘れないでください)が100000を下回ると予想されるためです(最悪のシナリオでも)
ruslik

最初の値を表すには、32ビットではなく34ビットが必要です(9,999,999,999> 2 ^ 32 = 4,294,967,296)。また、数値は一意であるため、最大128は00000から99001になります。これにより、ベース128の782ではなく774 1が追加されます。したがって、ベース128の1,000の数値の格納範囲は8026-8800ビットまたは1004-1100バイトです。64ビットベースは、879〜1072バイトの範囲で、より優れたストレージを提供します。
Briguy37

1
@raisercostin:これはカークが尋ねたものです。あなたの例では、最初の2つの値の間の20kの差を一度エンコードすると、最大範囲の80kのみが将来発生する可能性があります。これは、最大782(100kに対応)から最大20k / 128 = 156の単項ビットを使用します
ruslik

7

これは、Bentley's Programming Pearlsのよく知られた問題です。

解決策:すべての番号で同じであるため、番号から最初の5桁を取り除きます。次に、ビットごとの演算を使用して、残りの9999可能な値を表します。数値を表すために必要なのは2 ^ 17ビットだけです。各ビットは数値を表します。ビットが設定されている場合、番号は電話帳に記載されています。

すべての数値を印刷するには、ビットが設定されているすべての数値をプレフィックスと連結して印刷します。特定の数値を検索するには、必要なビット演算を実行して、数値のビット表現をチェックします。

O(1)で数値を検索でき、ビットの再現によりスペース効率が最大になります。

HTHクリス。


3
これは、数値の密度の高いセットに対しては適切なアプローチです。残念ながら、このセットは非常にまばらです。可能な100,000のうち、1,000の数しかありません。したがって、このアプローチでは、平均して数値あたり100ビットが必要になります。〜17ビットしか必要としない代替案については、私の回答を参照してください。
NPE、2011年

1
すべての数値を印刷するのにかかる時間は、1,000ではなく100,000に比例しませんか?
aioobe

2つのアイデアを組み合わせると、基本的にすぐにトライが得られます。100,000エントリのビットベクトルを使用することは、全体的な割り当てであり、多くのスペースを必要とします。ただし、O(log(n))の検索は、多くの場合、遅くなります(ここでのクエリの数によって異なります)。したがって、インデックス付けにビットセットの階層を使用すると、O(1)ルックアップを取得しながら、番号ごとに最大17ビットを格納できます。これがトライの仕組みです。また、トライのO(n)での印刷の時間は、ソートされたケースから継承されます。
LiKao、2011年

これは「これを行う最も効率的なスペース節約方法」ではありません。
Jake Berger、

5

1,000個の数値の1073バイトの固定ストレージ:

この格納方法の基本的な形式は、最初の5桁、各グループのカウント、および各グループの各番号のオフセットを格納することです。

接頭辞:
5桁の接頭辞は最初の17ビットを占めます

グループ化:
次に、数値の適切なサイズのグループ化を理解する必要があります。グループごとに約1つの数を試してみましょう。格納する数値が約1000あることがわかっているため、99,999を約1000の部分に分割します。グループサイズを100に選択すると、ビットが無駄になるため、128ビットのグループサイズを試してみましょう。7ビットで表すことができます。これにより、782のグループで作業できます。

カウント:
次に、782個のグループのそれぞれについて、各グループのエントリの数を保存する必要があります。各グループの7ビットカウントはを生成し7*782=5,474 bitsます。これは、グループの選択方法により、表される平均数が約1であるため、非常に非効率的です。

したがって、代わりに、グループ内の各数値の先頭に1が続き、その後に0が続く可変サイズのカウントがあります。したがって、xグループ内に数値がある場合、カウントを表すためにがx 1's続き0ます。たとえば、グループに5つの数値がある場合、カウントはで表され111110ます。この方法では、1,000個の数値がある場合、1000の1と782の0になり、合計で1000 + 782 = 1,782ビットになります

オフセット:
最後に、各数値の形式は、各グループの7ビットのオフセットになります。たとえば、00000と00001が0-127グループの唯一の数値である場合、そのグループのビットはになります110 0000000 0000001。1,000の数を想定すると、オフセットには7,000ビットが存在します。

したがって、1,000の数を想定した最終的なカウントは次のようになります。

17 (prefix) + 1,782 (counts) + 7,000 (offsets) = 8,799 bits = 1100 bytes

次に、128ビットに切り上げることによるグループサイズの選択がグループサイズの最良の選択であったかどうかを確認します。x各グループを表すビット数を選択すると、サイズの式は次のようになります。

Size in bits = 17 (prefix) + 1,000 + 99,999/2^x + x * 1,000

整数値に対して、この式を最小化することx与えるx=6= 8580ビットで得た、1,073バイト。したがって、理想的なストレージは次のとおりです。

  • グループサイズ:2 ^ 6 = 64
  • グループ数:1,562
  • 総ストレージ:

    1017 (prefix plus 1's) + 1563 (0's in count) + 6*1000 (offsets) = 8,580 bits = 1,073 bytes


1

これを純粋に理論的な問題とみなし、実装をそのままにしておくと、最も効率的な方法は、巨大なインデックステーブルの最後の10000桁のすべての可能なセットにインデックスを付けることです。あなたが正確に1000の数を持っていると仮定すると、現在のセットを一意に識別するために8000ビットより少し多くが必要になります。同じ状態で識別される2つのセットがあるため、大きな圧縮は不可能です。

これの問題は、プログラムの2 ^ 8000セットのそれぞれをlutとして表現する必要があり、Googleでさえこれをリモートで実行できないことです。

ルックアップはO(1)で、すべての数値O(n)を出力します。挿入はO(2 ^ 8000)であり、理論的にはO(1)ですが、実際には使用できません。

面接では、私がこの答えを出すのは、確信が持てる場合は、会社が箱から出してすぐに考えることができる人を探していることだけです。そうでなければ、これはあなたが現実世界の懸念のない理論家のように見えるかもしれません。

編集:わかりました、ここに1つの「実装」があります。

実装を構築する手順:

  1. サイズが100 000 *(1000は100 000を選択)ビットの定数配列を取ります。はい、この配列には宇宙の原子よりも数桁大きいスペースが必要であることを知っています。
  2. この大きな配列をそれぞれ100 000のチャンクに分割します。
  3. 各チャンクには、最後の5桁の特定の組み合わせのビット配列が格納されます。

これはプログラムではなく、プログラムで使用できる巨大なLUTを構築する一種のメタプログラムです。プログラムの定数は通常、スペース効率を計算するときにカウントされないため、最終的な計算を行うときは、この配列を気にしません。

このLUTの使用方法は次のとおりです。

  1. 誰かがあなたに1000の数字を与えたとき、あなたは最初の5桁を別々に保存します。
  2. 配列のどのチャンクがこのセットに一致するかを調べます。
  3. セットの番号を単一の8074ビット番号に格納します(これをcと呼びます)。

つまり、ストレージには8091ビットしか必要ありません。これは、ここで最適なエンコーディングであることが証明されています。ただし、正しいチャンクを見つけるにはO(100 000 *(100 000は1000を選択))が必要ですが、これは数学のルールによればO(1)ですが、実際には常に宇宙の時間よりも長くかかります。

ルックアップは簡単ですが:

  1. 最初の5桁のストリップ(残りの番号はn 'と呼ばれます)。
  2. それらが一致するかどうかをテストする
  3. i = c * 100000 + n 'を計算します
  4. LUTのiのビットが1に設定されているかどうかを確認します

すべての数値を出力することも簡単です(そして、現在のチャンクのすべてのビットを常にチェックする必要があるため、実際にはO(100000)= O(1)が必要です。

制限を露骨に無視しているため、これを「実装」とは呼びません(宇宙のサイズとこの宇宙が生きている時間、またはこの地球が存在する時間)。ただし、理論的にはこれが最適なソリューションです。小さな問題の場合、これは実際に実行でき、場合によっては実行されます。たとえば、並べ替えネットワークはこのコーディング方法の例であり、再帰的な並べ替えアルゴリズムの最後のステップとして使用して、大幅に高速化できます。


1
これを行う最も効率的なスペース節約方法は何ですか?
スベン

1
ランタイムスペースの計算を行う場合、システムの考えられる状態を1つの数値のみで列挙するため、これが最も効率的なスペース節約方法であることが簡単に証明できます。この問題には、これより小さいエンコーディングはありません。この答えの秘訣は、計算を行うときにプログラムのサイズがほとんど考慮されないことです(これを考慮に入れた答えを見つけてみてください。そうすれば、私の意味がわかります)。したがって、サイズに制限がある問題については、常にすべての状態を列挙して、それを処理する最もスペース節約の方法を得ることができます。
LiKao、2011年

1

これは、それぞれ100,000未満の非負の整数を1000個格納するのと同じです。これを行うには、算術エンコーディングなどを使用できます。

最終的に、番号はソートされたリストに格納されます。リスト内の隣接する数値間の予想される差は100,000 / 1000 = 100であり、7ビットで表すことができることに注意します。7ビット以上が必要な場合も多くあります。これらのあまり一般的でないケースを表す簡単な方法は、最初のビットが設定されていない限り、1バイトが7ビット整数を表すutf-8スキームを採用することです。その最初のビットが設定されます。その場合、次のバイトが読み取られ、21ビット整数を表します。

したがって、連続する整数間の差の少なくとも半分は1バイトで表すことができ、残りのほとんどすべてが2バイトを必要とします。16,384より大きい差で区切られたいくつかの数値は3バイトを必要としますが、そのうち61を超えることはできません。その場合、平均的なストレージは、数値あたり約12ビット、またはそれより少し少なく、最大で1500バイトになります。

このアプローチの欠点は、数値の存在のチェックがO(n)になることです。ただし、時間の複雑さの要件は指定されていません。

書いた後、私はruslikがすでに上記の差分方法を提案していることに気づきました、唯一の違いはエンコーディングスキームです。鉱山はおそらく単純ですが効率が落ちます。


1

数値を基数36に変更したくない理由を迅速に尋ねるだけです。スペースはそれほど節約されないかもしれませんが、10桁以下の範囲で表示されるため、検索時間を確実に節約できます。または、グループごとにファイルに分割します。したがって、ファイルに(111)-222.txtという名前を付け、そのグループに適合する番号のみを格納し、番号順に参照可能にすることで、ファイルが存在するかどうかを常に確認することができます。より大きな検索を実行する前に。または、正確には、ファイルが存在するかどうかを確認するために、ファイルのバイナリ検索を実行します。そして、ファイルの内容に関する別のボンナリー検索


0

シンプルにしてみませんか?構造体の配列を使用します。

最初の5桁を定数として保存できるので、ここではそれらを忘れます。

65535は16ビットの数値に格納できる最大の数値であり、最大数は99999です。これは、最大131071の17番目のビット数に適合します。

32ビットのデータ型を使用するのは無駄です。これは、余分な16ビットのうち1ビットしか必要ないためです。したがって、ブール(または文字)と16ビットの数値を持つ構造体を定義できます。

C / C ++を想定

typedef struct _number {

    uint16_t number;
    bool overflow;
}Number;

この構造体は3バイトしか必要とせず、1000の配列が必要なので、合計で3000バイトです。総スペースを25%削減しました!

数値を保存する限り、単純なビットごとの計算を行うことができます

overflow = (number5digits & 0x10000) >> 4;
number = number5digits & 0x1111;

そしてその逆

//Something like this should work
number5digits = number | (overflow << 4);

それらすべてを出力するには、配列に対して単純なループを使用できます。特定の数値の取得は配列なので、もちろん一定の時間で行われます。

for(int i=0;i<1000;i++) cout << const5digits << number5digits << endl;

数値を検索するには、ソートされた配列が必要です。したがって、数値が保存されたら、配列をソートします(個人的にマージソートを選択します、O(nlogn))。次に検索するには、マージソートアプローチを使用します。配列を分割し、番号の間にある1つを確認します。次に、その配列に対してのみ関数を呼び出します。一致が見つかるまで再帰的にこれを行い、インデックスを返します。そうでなければ、それは存在せず、エラーコードを出力します。この検索は非常に速く、最悪の場合はO(nlogn)よりも優れており、マージソートよりも短い時間で実行されます(両側でなく、毎回分割の片側だけを再帰します:))。 O(nlogn)です。


0

私のソリューション:ベストケース7.025ビット/数、ワーストケース14.193ビット/数、おおよその平均8.551ビット/数。ストリームエンコード、ランダムアクセスなし。

ruslikの回答を読む前でも、各数値の差は小さく、比較的一貫しているはずなので、すぐにエンコードすることを考えましたが、ソリューションは最悪のシナリオにも対応できなければなりません。1000個の数字のみを含む100000個の数字のスペースがあります。完全に均一な電話帳では、各番号は前の番号より100大きくなります。

55555-12 3 45
55555-12 4 45
55555-12 5 45

その場合、既知の定数であるため、数値間の差異をエンコードするためにストレージをゼロにする必要があります。残念ながら、数値は100の理想的なステップとは異なる場合があります。2つの隣接する数値が103異なる場合、2の隣接する数値が3をエンコードし、2つの隣接する数値が92異なる場合、Iの理想的な増分との差をエンコードします。 -8をエンコードします。100の理想的な増分からのデルタを「分散」と呼びます。

差異の範囲は-99(つまり、2つの連続した番号)から99000(電話帳全体は00000…00999の番号とさらに遠い番号99999で構成されます)の範囲で、99100の可能な値の範囲です。

私は私が(のような大きな違いが発生した場合、最も一般的な違いをエンコードし、ストレージを拡張するために、最小限のストレージを割り当てることを目指したいいるProtobufさんvarint)。私は7ビットのチャンクを使用し、6つをストレージに、最後に追加のフラグビットを使用して、この分散が現在のチャンクの後に最大3つのチャンク(最大で3 * 6 = 18ビットのストレージ。これは262144可能な値であり、可能な分散の数(99100)よりも多い。フラグが立てられた後に続く各チャンクには、より重要度の高いビットがあるため、最初のチャンクには常にビット0-図5に示すように、オプションの第2のチャンクはビット6〜11を有し、オプションの第3のチャンクはビット12〜17を有する。

単一のチャンクは、64個の値に対応できる6ビットのストレージを提供します。その単一のチャンクに収まるように64個の最小分散(つまり、-32から+31の分散)をマッピングしたいので、ProtoBuf ZigZagエンコーディングを使用します。分散は-99から+98までです(必要がないため-99を超える負の分散の場合)、この時点で98オフセットされた通常のエンコーディングに切り替えます。  

分散| エンコードされた値
----------- + ----------------
    0 | 0
   -1 | 1
    1 | 2
   -2 | 3
    2 | 4
   -3 | 5
    3 | 6
   ... | ...
  -31 | 61
   31 | 62
  -32 | 63
----------- | --------------- 6ビット
   32 | 64
  -33 | 65
   33 | 66
   ... | ...
  -98 | 195
   98 | 196
  -99 | 197
----------- | ---------------ジグザグの終わり
   100 | 198
   101 | 199
   ... | ...
  3996 | 4094
  3997 | 4095
----------- | --------------- 12ビット
  3998 | 4096
  3999 | 4097
   ... | ...
 262045 | 262143
----------- | --------------- 18ビット

追加のチャンクを示すフラグを含む、分散がビットとしてエンコードされる方法のいくつかの例:

分散| エンコードされたビット
----------- + ----------------
     0 | 000000 0
     5 | 001010 0
    -8 | 001111 0
   -32 | 111111 0
    32 | 000000 1 000001 0
   -99 | 000101 1 000011 0
   177 | 010011 1 000100 0
 14444 | 001110 1 100011 1 000011 0

したがって、サンプルの電話帳の最初の3つの番号は、次のようにビットのストリームとしてエンコードされます。

BIN 000101001011001000100110010000011001 000110 1 010110 1 00001 0
PH#55555-12345 55555-12448 55555-12491
POS 1 2 3

ベストケースのシナリオでは、電話帳はある程度均一に分散され、32を超える分散を持つ電話番号は2つないため、番号ごとに7ビットに加えて開始番号に32ビットを使用し、合計で32 + 7 * 999になります。 = 7025ビット。800の電話番号の差異が1つのチャンクに収まる
混合シナリオ(800 * 7 = 5600)、180の番号はそれぞれ2つのチャンクに収まり(180 * 2 * 7 = 2520)、19の番号はそれぞれ3つのチャンクに収まる(20 * 3 * 7 = 399)、および最初の32ビット、合計で8551ビット
最悪のシナリオ:25個の数値が3つのチャンク(25 * 3 * 7 = 525ビット)に収まり、残りの974個の数値が2つのチャンク(974 * 2 * 7 = 13636ビット)に収まるほか、グランドの最初の数値に32ビットの合計14193ビット

   エンコードされた数値の量|
 1チャンク| 2チャンク| 3チャンク| 総ビット
--------- + ---------- + ---------- + ------------
   999 | 0 | 0 | 7025
   800 | 180 | 19 | 8551
    0 | 974 | 25 | 14193

必要なスペースをさらに削減するために実行できる4つの追加の最適化を確認できます。

  1. 3番目のチャンクは7ビット全体を必要とせず、フラグビットなしで5ビットにすることもできます。
  2. 各チャンクの最適なサイズを計算するために、最初に数値を渡すことができます。たぶん特定の電話帳の場合、最初のチャンクに5 + 1ビット、2番目のチャンクに7 + 1、3番目のチャンクに5 + 1を割り当てるのが最適です。これにより、サイズがさらに6 * 999 + 32 = 6026ビットに減少し、さらに3ビットの2つのセットにチャンク1と2のサイズが格納されます(チャンク3のサイズは必要な16ビットの残りです)。 6032ビットの!
  3. 同じ初期パスで、デフォルトの100よりも良い予測増分を計算できます。おそらく55555-50000から始まる電話帳があり、そのため、番号範囲が半分であるため、予測増分は50になるはずです。あるいは、非線形があるかもしれません。分布(おそらく標準偏差)とその他の最適な予測増分を使用できます。これにより、一般的な差異が減少し、さらに小さな最初のチャンクを使用できるようになる可能性があります。
  4. 最初のパスでさらに分析を行って、電話帳をパーティション化できます。各パーティションには、独自の予測される増分とチャンクサイズの最適化があります。これにより、電話帳の特定の非常に均一な部分の最初のチャンクサイズを小さくし(消費されるビット数を減らす)、不均一な部分のチャンクサイズを大きくする(継続フラグで無駄になるビット数を減らす)ことができます。

0

本当の問題は、5桁の電話番号を保存することです。

コツは、0から99,999までの数値の範囲を格納するために17ビットが必要になることです。しかし、従来の8バイトのワード境界に17​​ビットを格納するのは面倒です。そのため、32ビット整数を使用しないことで4k未満で実行できるかどうかを尋ねられます。

質問:すべての数値の組み合わせは可能ですか?

電話システムの性質上、可能な組み合わせは65,000未満になる場合があります。市外局番やエクスチェンジプレフィックスではなく、電話番号の後半の5つの位置について話しているので、そうだと思います

質問:このリストは静的ですか、それとも更新をサポートする必要がありますか?

staticの場合は、データベースにデータを入力するときに、桁数が50,000未満で桁数が50,000以上の場合に数えます。適切な長さの2つの配列を割り当てますuint16。1つは50,000未満の整数用で、もう1つは上位セット用です。上位の配列に整数を格納する場合は50,000を減算し、その配列から整数を読み取る場合は50,000を加算します。これで、1,000の整数を2,000の8バイトワードに格納しました。

電話帳を作成するには、2つの入力トラバーサルが必要ですが、検索は、単一の配列を使用する場合よりも、平均で半分の時間で発生します。ルックアップ時間が非常に重要である場合は、より小さな範囲でより多くの配列を使用できますが、これらのサイズでは、パフォーマンスの限界はメモリから配列を引き出すことになると思います。日々。

動的な場合は、1000程度の配列1つ割り当てuint16、並べ替えられた順序で数値を追加します。最初のバイトを50,001に設定し、2番目のバイトをNULLまたは65,000などの適切なnull値に設定します。番号を保存するときは、並べ替えて保存してください。数値が50,001未満の場合は、50,001マーカーのに格納します。数値が50,001以上の場合、50,001マーカーのに格納しますが、格納された値から50,000を減算します。

配列は次のようになります。

00001 = 00001
12345 = 12345
50001 = reserved
00001 = 50001
12345 = 62345
65000 = end-of-list

したがって、電話帳で数値を検索すると、配列をトラバースし、50,001の値に到達すると、配列の値に50,000を追加し始めます。

これは挿入を非常に高価にしますが、ルックアップは簡単であり、ストレージに2k以上を費やすことはありません。

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