インプレース基数ソート


200

これは長いテキストです。我慢してください。要約すると、問題は、実行可能なインプレース基数ソートアルゴリズムがあるかどうかです。


予備的

「A」、「C」、「G」、および「T」の文字のみを使用する小さな固定長の文字列が大量にあります(そうです、ご想像のとおり、DNAです)。

現時点では、STLのすべての一般的な実装でintrosortを使用std::sortします。これはかなりうまくいきます。しかし、基数ソートは私の問題セットに完全に適合し、実際にははるかにうまく機能するはずだと確信しています。

細部

私はこの仮定を非常に単純な実装でテストしました。比較的小さな入力(10,000程度)の場合、これは真実でした(少なくとも、2倍以上の速さ)。ただし、問題のサイズが大きくなると(N > 5,000,000)、ランタイムは少し低下します。

その理由は明らかです。基数ソートでは、データ全体をコピーする必要があります(実際には、私の単純な実装では2回以上)。これは、メインメモリに最大4 GiBを入れたことを意味します。問題のサイズが実際にはさらに大きくなるため、そうでなかったとしても、これだけのメモリを使用する余裕はありません。

ユースケース

理想的には、このアルゴリズムは、DNAおよびDNA5(追加のワイルドカード文字「N」を許可)、またはIUPAC 曖昧コード(結果として16個の異なる値)を持つDNAの場合でも、2〜100の任意の文字列長で機能する必要があります。しかし、これらすべてのケースをカバーすることはできないので、速度の向上に満足しています。コードは、どのアルゴリズムにディスパッチするかを動的に決定できます。

研究

残念ながら、基数ソートに関するウィキペディアの記事は役に立たない。インプレースバリアントに関するセクションは完全にゴミです。基数ソートNIST-DADSセクションは、存在しないものの隣にあります。アルゴリズム「MSL」について説明しているEfficient Adaptive In-Place Radix Sortingと呼ばれる有望な論文があります。残念ながら、この論文も残念です。

特に、以下のことがございます。

まず、アルゴリズムにはいくつかの間違いがあり、説明がつかないままです。特に、再帰呼び出しの詳細については説明していません(現在のシフト値とマスク値を計算するためにポインターをインクリメントまたは削減すると単純に想定しています)。また、関数dest_groupを使用し、dest_address定義を行いません。これらを効率的に実装する方法がわかりません(つまり、O(1)で、少なくともdest_address簡単ではありません)。

最後に重要なことですが、アルゴリズムは、配列インデックスを入力配列内の要素と交換することにより、インプレースネスを実現します。これは明らかに数値配列でのみ機能します。文字列に使用する必要があります。もちろん、強い型付けをねじ込むだけで、メモリが属していないインデックスの格納をメモリが許容すると想定して先に進むことができます。しかし、これは、文字列を32ビットのメモリ(32ビット整数と仮定)に押し込める場合にのみ機能します。これはわずか16文字です(現時点では、16> log(5,000,000)であるため無視します)。

著者の1人による別の論文では、正確な説明はまったくありませんが、MSLの実行時間がサブリニアであり、これは完全に間違っています。

要約すると:機能するリファレンス実装、または少なくともDNA文字列で機能する機能するインプレース基数ソートの優れた疑似コード/説明を見つける可能性はありますか?


65
それは一つの優れた質問です。
JustinT 2009年

1
小さな固定長の文字列はどのくらい小さいですか?
EvilTeach 2009年

1
@EvilTeach:ユースケースを追加しました。
Konrad Rudolph、

2
@Stephan:これで問題ありません。しかし、コピー/キャッシュミスの場合、遅延が発生します。記憶の場合、私は物理的な限界に達しました。これは単に交渉不可能です。データの一部をディスクに保存するためのこれらのすべての派手な手法は、現在のクイックソートソリューションよりも明らかに低速です。
Konrad Rudolph、

2
(続き)一方、dsimchaのソリューションは、一部の入力ではクイックソートよりも明らかに高速です。移動の数は多く、キャッシュの局所性は小さいかもしれませんが、現実の世界では、それでも十分です。また、実行する必要のあるスワップの数を減らすために、ソリューションを少し調整しました。
Konrad Rudolph、

回答:


61

さて、これがDNAのMSD基数ソートの簡単な実装です。それがDで書かれているのは、それが私が最もよく使用する言語であり、そのため愚かな間違いをする可能性が最も低いためですが、他の言語に簡単に翻訳できます。インプレースですが2 * seq.length、配列を通過する必要があります。

void radixSort(string[] seqs, size_t base = 0) {
    if(seqs.length == 0)
        return;

    size_t TPos = seqs.length, APos = 0;
    size_t i = 0;
    while(i < TPos) {
        if(seqs[i][base] == 'A') {
             swap(seqs[i], seqs[APos++]);
             i++;
        }
        else if(seqs[i][base] == 'T') {
            swap(seqs[i], seqs[--TPos]);
        } else i++;
    }

    i = APos;
    size_t CPos = APos;
    while(i < TPos) {
        if(seqs[i][base] == 'C') {
            swap(seqs[i], seqs[CPos++]);
        }
        i++;
    }
    if(base < seqs[0].length - 1) {
        radixSort(seqs[0..APos], base + 1);
        radixSort(seqs[APos..CPos], base + 1);
        radixSort(seqs[CPos..TPos], base + 1);
        radixSort(seqs[TPos..seqs.length], base + 1);
   }
}

明らかに、これは一般的なものではなく、ある種のDNAに固有のものですが、高速でなければなりません。

編集:

このコードが実際に機能するかどうか知りたくて、自分のバイオインフォマティクスコードが実行されるのを待っている間にテスト/デバッグしました。上記のバージョンは実際にテストされ、動作します。それぞれ5塩基の1000万配列の場合、最適化されたイントロソートより約3倍高速です。


9
2xパスアプローチで対応できる場合、これはradix-Nに拡張されます。パス1 =通過して、N桁のそれぞれの数を数えます。次に、配列をパーティション化している場合、これにより各桁がどこから始まるかがわかります。パス2は、配列内の適切な位置にスワップします。
Jason S

(たとえば、N = 4の場合、90000 A、80000 G、100 C、100000 Tがある場合、APosの代わりに使用される累積合計= [0、90000、170000、170100]に初期化された配列を作成します。 CPosなど、各桁の次の要素を入れ替える場所のカーソルとして。)
Jason S

バイナリ表現とこの文字列表現の関係がどうなるかは
わかりませんが

より長いシーケンスでの速度はどうですか?長さが5の異なる十分な数がない
ステファンエガーモント

4
この基数ソートは、よく知られているインプレイス基数ソートバリアントである米国旗ソートの特別なケースのようです。
エドワードKMETT 2009

21

私はインプレース基数ソートを見たことがありません。基数ソートの性質から、一時配列がメモリに収まる限り、アウトオブプレースソートよりもはるかに高速であるとは思えません。

理由:

ソートは入力配列に対して線形読み取りを行いますが、すべての書き込みはほぼランダムになります。これは、特定のN以上から、書き込みごとのキャッシュミスに要約されます。このキャッシュミスにより、アルゴリズムが遅くなります。配置されていてもいなくても、この効果は変わりません。

私はこれがあなたの質問に直接答えることはないことを知っていますが、ソートがボトルネックである場合は、前処理ステップとして近くのソートアルゴリズムを確認することをお勧めします(ソフトヒープのWikiページで開始できる場合があります)。

これにより、キャッシュの局所性が非常に向上します。そうすれば、教科書の定位置外の基数ソートのパフォーマンスが向上します。書き込みは依然としてほぼランダムですが、少なくともそれらは同じメモリのチャンクに集中するため、キャッシュヒット率が向上します。

それが実際にうまくいくかどうかはわかりません。

ところで:DNA文字列のみを扱っている場合:charを2ビットに圧縮して、データをかなり詰め込むことができます。これにより、単純な表現よりもメモリ要件を4倍に削減できます。アドレッシングはより複雑になりますが、CPUのALUは、とにかくすべてのキャッシュミスの間に多くの時間を費やします。


2
2つの良い点。近くの並べ替えは私にとって新しい概念です。それについて読む必要があります。キャッシュミスは、私の夢を悩ませているもう1つの考慮事項です。;-)私はこれについて見なければなりません。
Konrad Rudolph、

私にとっても(2、3か月)新しいのですが、コンセプトを理解すると、パフォーマンス向上の機会が見えてきます。
ニルスPipenbrinck 2009年

基数が非常に大きくない限り、書き込みはほぼランダムではありません。たとえば、一度に1つの文字を並べ替えると(基数4の並べ替え)、すべての書き込みは4つの線形に増加するバケットの1つになります。これは、キャッシュとプリフェッチの両方に適しています。もちろん、より大きな基数を使用したい場合もあり、あるポインタでは、キャッシュとプリフェッチの親しみやすさと基数サイズの間のトレードオフが発生します。ソフトウェアのプリフェッチを使用するか、「実際の」バケットに定期的にフラッシュするバケットのスクラッチ領域を使用して、損益分岐点をより大きな基数に向けてプッシュできます。
BeeOnRope 2017

8

シーケンスをビット単位でエンコードすることにより、メモリ要件を確実に減らすことができます。順列を見ているので、長さ2の場合、「ACGT」は16状態、つまり4ビットです。長さ3の場合、64ビットの状態であり、6ビットでエンコードできます。したがって、シーケンスの各文字は2ビット、16文字は約32ビットのように見えます。

有効な「単語」の数を減らす方法がある場合は、さらに圧縮できる可能性があります。

したがって、長さ3のシーケンスの場合、64個のバケットを作成できます。サイズはuint32またはuint64の可能性があります。それらをゼロに初期化します。3つの文字シーケンスの非常に大きなリストを反復処理し、上記のようにエンコードします。これを添え字として使用し、そのバケットを増分します。
すべてのシーケンスが処理されるまで、これを繰り返します。

次に、リストを再生成します。

64のバケットを順番に繰り返し、そのバケットで見つかったカウントについて、そのバケットで表されるシーケンスのインスタンスをその数だけ生成します。
すべてのバケットが反復されると、ソートされた配列ができます。

シーケンス4は2ビットを追加するため、256バケットになります。5のシーケンスは2ビットを追加するため、1024バケットになります。

ある時点で、バケットの数が制限に近づきます。ファイルからシーケンスを読み取る場合、それらをメモリに保持する代わりに、バケット用により多くのメモリを使用できます。

バケットはワーキングセット内に収まる可能性が高いため、これはその場でソートを行うよりも高速だと思います。

これはテクニックを示すハックです

#include <iostream>
#include <iomanip>

#include <math.h>

using namespace std;

const int width = 3;
const int bucketCount = exp(width * log(4)) + 1;
      int *bucket = NULL;

const char charMap[4] = {'A', 'C', 'G', 'T'};

void setup
(
    void
)
{
    bucket = new int[bucketCount];
    memset(bucket, '\0', bucketCount * sizeof(bucket[0]));
}

void teardown
(
    void
)
{
    delete[] bucket;
}

void show
(
    int encoded
)
{
    int z;
    int y;
    int j;
    for (z = width - 1; z >= 0; z--)
    {
        int n = 1;
        for (y = 0; y < z; y++)
            n *= 4;

        j = encoded % n;
        encoded -= j;
        encoded /= n;
        cout << charMap[encoded];
        encoded = j;
    }

    cout << endl;
}

int main(void)
{
    // Sort this sequence
    const char *testSequence = "CAGCCCAAAGGGTTTAGACTTGGTGCGCAGCAGTTAAGATTGTTT";

    size_t testSequenceLength = strlen(testSequence);

    setup();


    // load the sequences into the buckets
    size_t z;
    for (z = 0; z < testSequenceLength; z += width)
    {
        int encoding = 0;

        size_t y;
        for (y = 0; y < width; y++)
        {
            encoding *= 4;

            switch (*(testSequence + z + y))
            {
                case 'A' : encoding += 0; break;
                case 'C' : encoding += 1; break;
                case 'G' : encoding += 2; break;
                case 'T' : encoding += 3; break;
                default  : abort();
            };
        }

        bucket[encoding]++;
    }

    /* show the sorted sequences */ 
    for (z = 0; z < bucketCount; z++)
    {
        while (bucket[z] > 0)
        {
            show(z);
            bucket[z]--;
        }
    }

    teardown();

    return 0;
}

ハッシュできるときを比較するのはなぜですか?
2009年

1
くそーまっすぐ。一般に、パフォーマンスはDNA処理の問題です。
EvilTeach 2009年

6

データセットが非常に大きい場合は、ディスクベースのバッファアプローチが最適だと思います。

sort(List<string> elements, int prefix)
    if (elements.Count < THRESHOLD)
         return InMemoryRadixSort(elements, prefix)
    else
         return DiskBackedRadixSort(elements, prefix)

DiskBackedRadixSort(elements, prefix)
    DiskBackedBuffer<string>[] buckets
    foreach (element in elements)
        buckets[element.MSB(prefix)].Add(element);

    List<string> ret
    foreach (bucket in buckets)
        ret.Add(sort(bucket, prefix + 1))

    return ret

たとえば、文字列が次のような場合は、さらに多くのバケットにグループ化してみます。

GATTACA

最初のMSB呼び出しはGATTのバケット(合計256バケット)を返すため、ディスクベースのバッファーのブランチを少なくすることができます。これによりパフォーマンスが向上する場合と向上しない場合があるため、実験してみてください。


一部のアプリケーションでは、メモリマップファイルを使用しています。ただし、一般に、明示的なディスクバッキングを必要としない十分なRAMがマシンから提供されるという前提の下で作業します(もちろん、スワッピングは行われます)。ただし、自動ディスクバックアップアレイのメカニズムはすでに開発中です
Konrad Rudolph

6

すぐに外に出て、ヒープ/ ヒープソート実装に切り替えることをお勧めします。この提案にはいくつかの前提条件があります。

  1. データの読み取りを制御します
  2. ソートを開始するとすぐに、ソートされたデータを使用して意味のあることを行うことができます。

ヒープ/ヒープソートの優れた点は、データを読み取りながらヒープを構築でき、ヒープを構築した瞬間から結果を取得できることです。

一歩戻りましょう。データを非同期で読み取ることができるほど幸運である場合(つまり、ある種の読み取り要求をポストし、データの準備ができたときに通知を受けることができます)、次に、待機している間にヒープのチャンクを構築できます。次に来るデータのチャンク-ディスクからでも。多くの場合、このアプローチでは、並べ替えの半分のコストのほとんどを、データの取得に費やされた時間の背後に埋めることができます。

データを読み取ったら、最初の要素はすでに利用可能です。データの送信先に応じて、これは素晴らしい場合があります。別の非同期リーダー、または並列の「イベント」モデル、またはUIに送信する場合は、チャンクとチャンクを送信することができます。

つまり、データの読み取り方法を制御できず、同期的に読み取られ、完全に書き出されるまでソートされたデータを使用できない場合は、これをすべて無視してください。:(

ウィキペディアの記事をご覧ください。


1
良い提案。しかし、私はすでにこれを試しました、そして私の特定のケースでは、ヒープを維持するオーバーヘッドは、ベクトルにデータを蓄積してすべてのデータが到着した後でソートするよりも大きいです。
コンラッドルドルフ


4

パフォーマンスに関しては、より一般的な文字列比較ソートアルゴリズムを確認することをお勧めします。

現在、すべての文字列のすべての要素に触れる必要がありますが、もっと上手にできます!

特に、バーストソートはこの場合に非常に適しています。おまけとして、burstsortは試行に基づいているため、DNA / RNAで使用される小さなアルファベットサイズに対してはばかばかしく機能します。これは、3進検索ノード、ハッシュ、またはその他のトライノード圧縮スキームをに構築する必要がないためです。トライの実装。試行は、サフィックス配列のような最終目標にも役立つ場合があります。

Burstsortのまともな汎用実装は、ソースフォージのhttp://sourceforge.net/projects/burstsort/で利用できますが、インプレースではありません。

比較のために、C-burstsortの実装はhttp://www.cs.mu.oz.au/~rsinha/papers/SinhaRingZobel-2006.pdfのベンチマークでカバーされており、いくつかの典型的なワークロードでは、クイックソートおよび基数ソートよりも4〜5倍高速です。


私は間違いなくバーストソートを確認する必要があります。現時点では、トライをインプレースで構築する方法はわかりません。一般に、サフィックスアレイは、実用的なアプリケーションでの優れたパフォーマンス特性のために、バイオインフォマティクスでサフィックスツリー(つまり、試行)を置き換えただけです。
Konrad Rudolph、

4

あなたは大規模なゲノム配列処理を見てみたいと思うでしょうDrs。による。笠原と森下。

ストリングは、4つの塩基の文字A、C、Gから成る、そしてTは、特別のために整数に符号化することができるはるかに速く処理します。基数ソートは、本で説明されている多くのアルゴリズムの1つです。あなたはこの質問に受け入れられた答えを適合させ、大きなパフォーマンスの改善を見ることができるはずです。


この本で紹介されている基数ソートは、適切な位置にないため、この目的には使用できません。文字列の圧縮については、私は(もちろん)既にこれを行っています。私の(多かれ少なかれ)最終的な解決策(以下に投稿)では、ライブラリは通常の文字列のように扱うことができるため、これを示していませんRADIX
Konrad Rudolph

3

トライを使ってみてください。データの並べ替えは、データセットを繰り返し処理して挿入するだけです。構造は自然にソートされ、Bツリーと同様に考えることができます(比較を行う代わりに、常にポインターの間接参照を使用します)。

キャッシュ動作はすべての内部ノードを優先するため、おそらくそれを改善することはできません。ただし、トライの分岐係数をいじることもできます(すべてのノードが単一のキャッシュラインに収まることを確認し、ヒープに似たトライノードをレベル順のトラバーサルを表す連続した配列として割り当てます)。試行もデジタル構造(長さkの要素のO(k)挿入/検索/削除)であるため、基数ソートに対して競争力のあるパフォーマンスが必要です。


トライは、私の素朴な実装と同じ問題を抱えています。単純に多すぎるO(n)の追加メモリが必要です。
Konrad Rudolph、

3

私は考えburstsort文字列のパックビット表現を。Burstsortは、基数ソートよりも局所性がはるかに優れていると主張されており、従来の試行の代わりにバースト試行を使用して、余分なスペースの使用を抑えています。元の用紙には測定値があります。


2

Radix-Sortはキャッシュを意識せず、大規模なセットの最速のソートアルゴリズムではありません。あなたは見ることができます:

また、並べ替え配列に格納する前に、圧縮を使用してDNAの各文字を2ビットにエンコードすることもできます。


請求書:このqsort関数がstd::sortC ++によって提供される関数よりも優れている点を説明していただけませんか?特に、後者は最新のライブラリに高度に洗練されたイントロソートを実装し、比較演算をインライン化します。ほとんどの場合、O(n)で実行されるという主張は購入しません。これには、一般的なケースでは利用できないある程度のイントロスペクションが必要になるためです(少なくともオーバーヘッドが多くない限り)。
Konrad Rudolph、

私はc ++を使用していませんが、私のテストでは、インラインQSORTはstdlibのqsortよりも3倍高速です。ti7qsortは、整数の最高速のソートです(インラインQSORTより高速です)。小さな固定サイズのデータ​​を並べ替えるのにも使用できます。データを使用してテストを実行する必要があります。
法案

1

dsimchaのMSB基数ソートは見た目は良いですが、Nilsはキャッシュの局所性が大きな問題のサイズであなたを殺しているものであるという観察により、問題の中心に近づきます。

私は非常にシンプルなアプローチを提案します:

  1. 最大サイズを経験的に推定する m基数ソートが効率的なをます。
  2. m要素のブロックを一度に読み取り、基数でソートし、入力がなくなるまで(十分なメモリがある場合はメモリバッファーに、それ以外の場合はファイルに)書き出します。
  3. 結果のソートされたブロックをマージソートします。

Mergesortは、私が知っている最もキャッシュフレンドリなソートアルゴリズムです。「配列AまたはBから次の項目を読み取り、次に出力バッファーに項目を書き込みます。」テープドライブで効率的に実行されます。アイテムの2n並べ替えにはスペースが必要ですnが、キャッシュの局所性が大幅に改善され、重要ではなくなります。インプレースではない基数ソートを使用している場合は、とにかく余分なスペースが必要でした。

最後に、mergesortは再帰なしで実装できることに注意してください。実際、これを行うと、実際の線形メモリアクセスパターンが明確になります。


1

問題は解決したようですが、念のため、実行可能なインプレース基数ソートの1つのバージョンが「アメリカのフラグソート」であるようです。ここで説明されています:Engineering Radix Sort。一般的な考え方は、各文字に2つのパスを実行することです。最初に各文字の数を数えるため、入力配列をビンに分割できます。次に、各要素を正しいビンに入れ替えて、もう一度調べます。次に、各ビンを次の文字位置で再帰的にソートします。


実際、私が使用するソリューションは、フラグソートアルゴリズムと非常に密接に関連しています。関連する区別があるかどうかはわかりません。
コンラッドルドルフ

2
American Flag Sortのことは聞いたことがありませんが、見たところ それは私がコード化したものstd::sortです。問題(アルゴリズムではなく、テストスイート自体)
Mooing Duck

@KonradRudolph:フラグソートと他の基数ソートの大きな違いは、カウントパスです。すべての基数ソートは非常に密接に関連していると思いますが、私はあなたのフラグソートを考慮しません。
Mooing Duck、2015

@MooingDuck:そこのサンプルからインスピレーションを得ただけです-独自の独立した実装に行き詰まり、あなたの実装が軌道に戻るのを助けてくれました。ありがとう!可能な最適化の1つ-ここにまだ価値があるかどうかを確認するのに十分な距離がありません:スワップする位置にある要素がすでに必要な場所にある場合は、それをスキップして、その要素に進むことができます。そうではありません。もちろん、これを検出するには追加のロジックが必要であり、追加のストレージも必要になる可能性がありますが、スワップは比較に比べてコストがかかるため、実行する価値があります。
内部サーバーエラー

1

まず、問題のコーディングについて考えます。文字列を削除し、バイナリ表現で置き換えます。最初のバイトを使用して、長さ+エンコードを示します。または、4バイト境界で固定長表現を使用します。その後、基数ソートがはるかに簡単になります。基数ソートの場合、最も重要なことは、内部ループのホットスポットで例外処理を行わないことです。

OK、4項問題についてもう少し考えました。あなたはジュディツリーのような解決策を求めていますこのための。次のソリューションは可変長文字列を処理できます。固定長の場合は、長さのビットを削除するだけで、実際に簡単になります。

16ポインタのブロックを割り当てます。ブロックは常に整列されるため、ポインターの最下位ビットを再利用できます。特別なストレージアロケータが必要な場合があります(大きなストレージを小さなブロックに分割します)。ブロックにはさまざまな種類があります。

  • 可変長文字列の7つの長さのビットを使用したエンコード。それらがいっぱいになったら、次のものに置き換えます。
  • Positionは次の2つの文字をエンコードします。次のブロックで終了する次のブロックへの16のポインターがあります。
  • 文字列の最後の3文字のビットマップエンコーディング。

ブロックの種類ごとに、LSBに異なる情報を格納する必要があります。可変長の文字列があるため、文字列の終わりも格納する必要があり、最後の種類のブロックは最も長い文字列に対してのみ使用できます。7つの長さのビットは、構造を深く理解するにつれて、少ないものに置き換える必要があります。

これにより、ソートされた文字列のかなり高速でメモリ効率の高いストレージが提供されます。トライのように振る舞う。これを機能させるには、十分な単体テストを作成してください。すべてのブロック遷移のカバレッジが必要です。2番目の種類のブロックのみから始めたいとします。

さらにパフォーマンスを向上させるには、さまざまなブロックタイプとより大きなサイズのブロックを追加することをお勧めします。ブロックが常に同じサイズで十分な大きさである場合、ポインターに使用できるビットをさらに少なくすることができます。ブロックサイズが16ポインタの場合、32ビットのアドレス空間に既に1バイトの空きがあります。興味深いブロックタイプについては、Judyツリーのドキュメントをご覧ください。基本的に、スペース(およびランタイム)のトレードオフのためにコードとエンジニアリング時間を追加します

最初の4文字は、256ワイドの直接基数から始めるとよいでしょう。それは適切な空間/時間のトレードオフを提供します。この実装では、単純なトライよりもはるかに少ないメモリオーバーヘッドが発生します。約3分の1です(測定していません)。O(n log n)クイックソートと比較すると気付いたように、定数が十分に小さければ、O(n)は問題ありません。

ダブルスの処理に興味がありますか?短いシーケンスでは、そうなるでしょう。カウントを処理するようにブロックを適応させるのは難しいですが、スペース効率が非常に高くなる可能性があります。


ビットパックされた表現を使用すると、基数ソートがどのように簡単になるかわかりません。ちなみに、私が使用するフレームワークは、実際にはビットパック表現を使用する可能性を提供しますが、これは、インターフェイスのユーザーとして私には完全に透過的です。
Konrad Rudolph、

ストップウォッチを見たときではありません:)
Stephan Eggermont

私は間違いなくジュディの木を見ます。バニラの試みは、実際にはテーブルに多くのものをもたらしませんが、それらは基本的に通常のMSD基数ソートのように動作し、要素へのパスは少なくなりますが、追加のストレージを必要とします。
Konrad Rudolph、
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.