web-dev-qa-db-ja.com

リンクリストをソートするための最速のアルゴリズムは何ですか?

O(n log n)がリンクリストでできる最高のものであるかどうか興味があります。

87
Dirk

実行時間ではO(N log N)以上のことはできないと期待するのは合理的です。

ただし、興味深いのは、ソートできるかどうかを調べることです in-placestably 、その最悪の場合の動作など。

PuTTY名声のSimon Tathamが、 マージソートでリンクリストをソートする の方法を説明しています。彼は次のコメントで締めくくります:

自尊心のあるソートアルゴリズムと同様に、これには実行時間O(N log N)があります。これはMergesortであるため、最悪の場合の実行時間はまだO(N log N)です。病理学的なケースはありません。

補助ストレージの要件は小さく、一定です(つまり、ソートルーチン内のいくつかの変数)。配列からのリンクリストの本質的に異なる動作のおかげで、このMergesort実装は、アルゴリズムに通常関連付けられるO(N)補助ストレージコストを回避します。

Cには、単一リンクリストと二重リンクリストの両方で機能する実装例もあります。

@JørgenFoghが以下で言及しているように、big-O表記法は、アイテムの数が少ないなど、メモリの局所性のために1つのアルゴリズムのパフォーマンスを向上させる一定の要因を隠す場合があります。

90
csl

いくつかの要因に応じて、リストを配列にコピーしてから Quicksort を使用する方が実際には速いかもしれません。

これが高速になる理由は、配列の方がリンクリストよりもキャッシュパフォーマンスがはるかに優れているためです。リスト内のノードがメモリ内に分散している場合、場所全体にキャッシュミスが発生している可能性があります。繰り返しますが、配列が大きい場合、キャッシュミスが発生します。

Mergesortはより良い並列化を行うので、それがあなたが望むのであれば、より良い選択かもしれません。また、リンクリストで直接実行すると、はるかに高速になります。

両方のアルゴリズムはO(n * log n)で実行されるため、情報に基づいた決定を行うには、実行するマシンで両方のプロファイルを作成する必要があります。

---編集

私は仮説をテストすることにし、intのリンクリストを並べ替えるのにかかる時間を(clock()を使用して)測定するCプログラムを作成しました。各ノードにmalloc()が割り当てられているリンクリストと、ノードが配列内に直線的に配置されているリンクリストを使用してみたので、キャッシュのパフォーマンスが向上します。これらを組み込みのqsortと比較しました。これには、フラグメント化されたリストから配列にすべてをコピーし、結果を再度コピーすることが含まれていました。各アルゴリズムを同じ10個のデータセットで実行し、結果を平均しました。

結果は次のとおりです。

N = 1000:

マージソート付きの断片化リスト:0.000000秒

Qsortを使用した配列:0.000000秒

マージソート付きのパックドリスト:0.000000秒

N = 100000:

マージソート付きの断片化リスト:0.039000秒

Qsortを使用した配列:0.025000秒

マージソート付きのパックドリスト:0.009000秒

N = 1000000:

マージソート付きの断片化リスト:1.162000秒

Qsortを使用した配列:0.420000秒

マージソート付きのパックドリスト:0.112000秒

N = 100000000:

マージソート付きの断片化リスト:364.797000秒

Qsortを使用した配列:61.166000秒

マージソート付きのパックドリスト:16.525000秒

結論:

少なくとも私のマシンでは、実際に完全にパックされたリンクリストを持っていることはめったにないので、キャッシュのパフォーマンスを改善するために、アレイにコピーする価値があります。私のマシンには2.8GHzのPhenom IIがありますが、0.6GHzのRAMしかないため、キャッシュは非常に重要です。

68
Jørgen Fogh

比較ソート(つまり、要素の比較に基づくソート)は、n log nよりも高速になることはありません。基礎となるデータ構造が何であるかは関係ありません。 Wikipedia を参照してください。

リストに同じ要素が多数あることを利用する他の種類のソート(カウントソートなど)、またはリスト内の要素の予想される分布は高速ですが、特にうまく機能するものは考えられませんリンクリスト上。

7
Artelius

何度も述べたように、一般データの比較に基づくソートの下限はO(n log n)になります。これらの引数を簡単に要約すると、n!リストをソートするさまざまな方法。 nを持つあらゆる種類の比較ツリー(これはO(n ^ n)にあります)可能な最終ソートには、少なくともその高さとしてlog(n!)が必要です。これにより、O(log(n ^ n))の下限、つまりO(nログn)。

そのため、リンクリストの一般的なデータの場合、2つのオブジェクトを比較できるデータで機能する最適な並べ替えは、O(n log n)になります。ただし、作業対象の領域がより限定されている場合は、かかる時間を改善できます(少なくともnに比例)。たとえば、ある値以下の整数で作業している場合、 Counting Sort または Radix Sort を使用できます。これらは、ソートする特定のオブジェクトを使用して削減するためです。 nに比例する複雑さ。しかし、これらはあなたが考慮しないかもしれない複雑さに他のいくつかを追加することに注意してください(例えば、ソートソートと基数ソートの両方は、ソートする数値のサイズに基づいた要因を追加します、O(n + k )ここで、kは、たとえばCounting Sortの最大数のサイズです)。

また、完璧なハッシュ(または少なくともすべての値を異なる方法でマップするハッシュ)を持つオブジェクトがある場合、そのハッシュ関数でカウントまたは基数ソートを使用してみてください。

5
DivineWolfwood

これはこのトピックに関する素敵な小さな論文です。彼の経験的な結論は、Treesortが最適であり、QuicksortとMergesortがそれに続くということです。堆積物の並べ替え、バブルの並べ替え、選択の並べ替えのパフォーマンスが非常に悪い。

Ching-Kuang Sheneによるリンクリストソートアルゴリズムの比較研究

http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.31.9981

5
Neal Richter

Radix sort は、リンクリストに特に適しています。数字の可能な各値に対応するヘッドポインターのテーブルを簡単に作成できるためです。

3
Mark Ransom

マージソートはO(1)アクセスを必要とせず、O(n ln n)です。一般データをソートするための既知のアルゴリズムは、O(n ln n)より優れています。

基数ソート(データのサイズ制限)やヒストグラムソート(離散データをカウント)などの特殊なデータアルゴリズムは、O(1)で異なる構造を使用している限り、成長関数の低いリンクリストをソートできます。一時ストレージとしてアクセスします。

特別なデータのもう1つのクラスは、k個の要素の順序が乱れているほぼソート済みリストの比較ソートです。これはO(kn)操作でソートできます。

リストを配列にコピーして戻すことはO(N)であるため、スペースが問題にならない場合は任意のソートアルゴリズムを使用できます。

たとえば、uint_8を含むリンクリストがある場合、このコードはヒストグラムソートを使用してO(N)時間でソートします。

#include <stdio.h>
#include <stdint.h>
#include <malloc.h>

typedef struct _list list_t;
struct _list {
    uint8_t value;
    list_t  *next;
};


list_t* sort_list ( list_t* list )
{
    list_t* heads[257] = {0};
    list_t* tails[257] = {0};

    // O(N) loop
    for ( list_t* it = list; it != 0; it = it -> next ) {
        list_t* next = it -> next;

        if ( heads[ it -> value ] == 0 ) {
            heads[ it -> value ] = it;
        } else {
            tails[ it -> value ] -> next = it;
        }

        tails[ it -> value ] = it;
    }

    list_t* result = 0;

    // constant time loop
    for ( size_t i = 255; i-- > 0; ) {
        if ( tails[i] ) {
            tails[i] -> next = result;
            result = heads[i];
        }
    }

    return result;
}

list_t* make_list ( char* string )
{
    list_t head;

    for ( list_t* it = &head; *string; it = it -> next, ++string ) {
        it -> next = malloc ( sizeof ( list_t ) );
        it -> next -> value = ( uint8_t ) * string;
        it -> next -> next = 0;
    }

    return head.next;
}

void free_list ( list_t* list )
{
    for ( list_t* it = list; it != 0; ) {
        list_t* next = it -> next;
        free ( it );
        it = next;
    }
}

void print_list ( list_t* list )
{
    printf ( "[ " );

    if ( list ) {
        printf ( "%c", list -> value );

        for ( list_t* it = list -> next; it != 0; it = it -> next )
            printf ( ", %c", it -> value );
    }

    printf ( " ]\n" );
}


int main ( int nargs, char** args )
{
    list_t* list = make_list ( nargs > 1 ? args[1] : "wibble" );


    print_list ( list );

    list_t* sorted = sort_list ( list );


    print_list ( sorted );

    free_list ( list );
}
2
Pete Kirkham

質問に対する直接的な答えではありませんが、 スキップリスト を使用すると、すでにソートされており、O(log N)の検索時間があります。

1
Mitch Wheat

私が知っているように、最良のソートアルゴリズムは、コンテナが何であれO(n * log n)です-Wordの広い意味でのソート(mergesort/quicksortなどのスタイル)は低くならないことが証明されています。リンクリストを使用しても、実行時間は短縮されません。

O(n)で実行される唯一のアルゴリズムは、実際に並べ替えるのではなく、値のカウントに依存する「ハック」アルゴリズムです。

1
laura

ここに実装があります リストを1回だけ走査して実行を収集し、mergesortと同じ方法でマージをスケジュールします。

複雑さはO(n log m)です。ここで、nは項目の数、mは実行の数です。最良のケースはO(n)(データが既にソートされている場合)で、最悪のケースは予想どおりO(n log n)です。

O(log m)一時メモリが必要です。ソートはリスト上でインプレースで行われます。

(以下で更新。コメント者の方は、ここでそれを説明するのが良い点です)

アルゴリズムの要点は次のとおりです。

    while list not empty
        accumulate a run from the start of the list
        merge the run with a stack of merges that simulate mergesort's recursion
    merge all remaining items on the stack

ランを蓄積するのに多くの説明は必要ありませんが、昇順のランと降順のラン(逆転)の両方を蓄積する機会をとることは良いことです。ここでは、実行の先頭よりも小さいアイテムを先頭に追加し、実行の最後以上のアイテムを追加します。 (並べ替えの安定性を維持するために、先頭に厳密な「より小さい」を使用する必要があることに注意してください。)

マージコードをここに貼り付けるのが最も簡単です。

    int i = 0;
    for ( ; i < stack.size(); ++i) {
        if (!stack[i])
            break;
        run = merge(run, stack[i], comp);
        stack[i] = nullptr;
    }
    if (i < stack.size()) {
        stack[i] = run;
    } else {
        stack.Push_back(run);
    }

リスト(d a g i b e c f j h)のソートを検討してください(実行を無視)。スタックの状態は次のように進みます。

    [ ]
    [ (d) ]
    [ () (a d) ]
    [ (g), (a d) ]
    [ () () (a d g i) ]
    [ (b) () (a d g i) ]
    [ () (b e) (a d g i) ]
    [ (c) (b e) (a d g i ) ]
    [ () () () (a b c d e f g i) ]
    [ (j) () () (a b c d e f g i) ]
    [ () (h j) () (a b c d e f g i) ]

次に、最後に、これらすべてのリストをマージします。

Stack [i]の項目(実行)の数は0または2 ^ iであり、スタックサイズは1 + log2(nruns)によって制限されていることに注意してください。各要素はスタックレベルごとに1回マージされるため、O(n log m)比較されます。ティムソートは、フィボナッチ数列のようなものを使用してスタックを維持しますが、これは2のべき乗を使用しますが、ここではティムソートと非常に類似しています。

実行の累積では、既にソートされたデータを利用するため、ベストケースの複雑さは、ソート済みのリスト(1回の実行)に対してO(n)です。昇順と降順の両方のランを蓄積しているため、ランは常に少なくとも長さ2になります(これにより、最大スタック深度が少なくとも1つ減少します。 O(n log n)、予想どおり、高度にランダム化されたデータ。

(うーん... 2回目の更新。)

または、ウィキペディアで bottom-up mergesort を参照してください。

1
Stan Switzer

それを配列にコピーして、並べ替えることができます。

  • 配列O(n)へのコピー、

  • ソートO(nlgn)(マージソートのような高速アルゴリズムを使用する場合)、

  • 必要に応じてリンクリストO(n)にコピーして、

だからO(nlgn)になります。

リンクリストの要素の数がわからない場合、配列のサイズがわからないことに注意してください。 Javaでコーディングしている場合、たとえばArraylistを使用できます。

1
Shirin

ここでできるのはマージソートです。

0
ypnos