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


95

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


31
ご存知のように、O(nlogn)は比較ベースのソートの範囲です。O(n)のパフォーマンスを提供できる非比較ベースの並べ替え(たとえば、並べ替えのカウント)がありますが、データに対する追加の制約が必要です。
MAK、

回答:


99

実行時に O(N log N)以上のことはできないと期待するのは妥当です。

ただし、興味深いのは、インプレースで安定して、最悪の場合の動作などをソートできるかどうかを調査することです。

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

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

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

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

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


3
これは単一のリンクリスト用ではありません。彼のCコードは* prevと* nextを使用しています。
LE

3
@LE実際には両方のためです。の署名listsortが表示されている場合は、パラメータを使用して切り替えることができますint is_double
csl 2013年

1
@LE:単一リンクリストのみをサポートするlistsortCコードのPythonバージョンを示します
jfs

O(kn)は理論的に線形であり、バケットソートで実現できます。適切なk(ビット数/ソートするオブジェクトのサイズ)を想定すると、少し高速になる可能性があります
Adam

74

いくつかの要因に応じて、実際にリストを配列にコピーしてからクイックソートを使用する方が速い場合があります。

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

Mergesortは並列処理が優れているため、それが必要な場合は、Mergesortの方が適切な選択かもしれません。リンクされたリストで直接実行すると、はるかに高速になります。

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

---編集

私は私の仮説をテストすることにしclock()、intのリンクされたリストをソートするために(を使用して)時間を測定する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が搭載されていますが、RAMは0.6GHzしかないため、キャッシュは非常に重要です。


2
良いコメントですが、リストから配列にデータをコピーするための一定でないコスト(リストを走査する必要があります)、およびクイックソートの最悪の場合の実行時間を考慮する必要があります。
csl

1
O(n * log n)は理論的にはO(n * log n + n)と同じで、コピーのコストを含みます。十分に大きなnの場合、コピーのコストは実際には問題になりません。リストを最後まで1回トラバースするのはn回です。
Dean J

1
@DeanJ:理論的にはそうですが、元のポスターがマイクロ最適化が重要であるケースを提示していることを覚えておいてください。その場合、リンクリストを配列に変換するために費やした時間を考慮する必要があります。コメントは洞察に富んでいますが、実際にパフォーマンスが向上するとは完全には確信していません。非常に小さいNで機能する可能性があります。
-csl

1
@csl:実際には、大きなNの場合は局所性の利点が有効になると思います。キャッシュミスが主要なパフォーマンス効果であると仮定すると、copy-qsort-copyアプローチでは、コピーで約2 * Nのキャッシュミスが発生します。プラスqsortのミス数。これはN log(N)のごく一部になります(qsortのほとんどのアクセスは最近アクセスした要素に近い要素へのアクセスであるため)。比較ソートの割合が高くなるとキャッシュミスが発生するため、マージソートのミスの数はN log(N)の大きな割合になります。したがって、Nが大きい場合、この用語が支配的となり、マージソートの速度が低下します。
スティーブジェソップ

2
@Steve:qsortはドロップインの置き換えではないというのはあなたの言うとおりですが、私のポイントはqsortとmergesortの違いではありません。qsortがすぐに利用できるようになったとき、私はマージソートの別のバージョンを書く気がしませんでした。標準ライブラリがある方法あなた自身の圧延よりも便利。
ヨルゲンFogh

8

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

リストに同じ要素が多数あることを利用する他の種類の並べ替え(カウントの並べ替えなど)、またはリスト内の要素の予想されるいくつかの分布は、特にうまくいくとは思えませんが高速ですリンクされたリスト。


8

これは、このトピックに関する素晴らしい小さなペーパーです。彼の経験的な結論は、Treesortが最も優れており、QuicksortとMergesortがそれに続くことです。堆積物ソート、バブルソート、セレクションソートのパフォーマンスが非常に悪い。

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

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


5

何度も述べたように、一般データの比較ベースのソートの下限は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に比例して複雑さを軽減するためです。ただし注意が必要ですが、これらにより、考慮できない複雑さが他にもいくつか追加されます(たとえば、Counting SortとRadix sortはどちらも、並べ替える数値のサイズに基づいた係数O(n + k )ここで、kは、たとえば、Counting Sortの最大数のサイズです)。

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


3

A 基数ソート特にそれは数字の各値に対応するヘッドポインタのテーブルを作るのは簡単なので、リンクリストに適しています。


1
このトピックについて詳しく説明するか、リンクされたリストで基数ソートのリソースリンクを提供してください。
LoveToCode

2

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

基数ソート(データのサイズを制限する)やヒストグラムソート(離散データをカウントする)などの特別なデータアルゴリズムは、一時的なストレージとしてO(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 );
}

5
n log nより速い比較ベースのソートアルゴリズムは存在しないことが証明ています。
Artelius、2009年

9
いいえ、全く比較に基づくソートアルゴリズムことが証明されています一般的なデータには、より高速ではないn個のnログイン
ピートKirkham

いいえ、並べ替えアルゴリズムO(n lg n)は比較ベースではない(例:基数ソート)よりも高速です。定義により、比較ソートは、全順序を持つ(つまり、比較できる)ドメインに適用されます。
bdonlan 2009年

3
@bdonlan「一般的なデータ」のポイントは、ランダムな入力よりも制約された入力の方が高速なアルゴリズムがあることです。制限的なケースでは、入力データが既にソートされているように制約されている場合にリストをソートする簡単なO(1)アルゴリズムを記述できます
Pete Kirkham

そして、それは比較ベースのソートではありません。比較ソートは一般データをすでに処理しているため(また、ビッグO表記は、行われた比較の数を表すため)、「一般データについて」という修飾子は冗長です。
スティーブジェソップ

1

質問への直接の回答ではありませんが、スキップリストを使用する場合、リストは既に並べ替えられており、検索時間はO(ログN)です。


1
予想される O(lg N)検索時間-スキップリストはランダム性に依存するため、保証はされません。信頼できない入力を受け取っている場合は、入力の供給者がRNGを予測できないことを確認してください。そうしないと、最悪の場合のパフォーマンスをトリガーするデータが送信されます
bdonlan

1

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

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


3
これはハックアルゴリズムではなく、O(n)では実行されません。O(cn)で実行されます。ここで、cは並べ替える最大値(実際には、最高値と最低値の差です)であり、整数値でのみ機能します。O(n)とO(cn)の間には違いがあります。ソートする値の明確な上限を指定できない限り(したがって、定数によってバインドされない限り)、複雑さを複雑にする2つの要因があります。
DivineWolfwood

厳密に言えば、それはで実行されO(n lg c)ます。すべての要素が一意である場合c >= n、なので、に比べて時間がかかりますO(n lg n)
bdonlan 2009年

1

次に、リストを1回だけトラバースし、実行を収集してから、mergesortと同じ方法でマージをスケジュールする実装を示します。

複雑さはO(n log m)です。ここで、nはアイテムの数、mは実行の数です。最良のケースはO(n)(データが既にソートされている場合)で、最悪のケースはO(n log n)です。

O(log m)一時メモリが必要です。並べ替えはリスト上で行われます。

(以下に更新。コメンター1つは、ここで説明する必要があるという点で優れています)

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

    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);
    }

リスト(dagibecfjh)をソートすることを検討してください(実行は無視)。スタックの状態は次のように進行します。

    [ ]
    [ (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]の項目(実行)の数はゼロまたは2 ^ iであり、スタックサイズは1 + log2(nruns)によって制限されることに注意してください。各要素はスタックレベルごとに1回マージされるため、O(n log m)比較です。ここではTimsortにかなり類似していますが、Timsortは2のべき乗を使用するフィボナッチシーケンスのようなものを使用してスタックを維持します。

ランの累積は、既にソートされたデータを利用するため、すでにソートされたリスト(1回の実行)の場合、最良の場合の複雑さはO(n)です。昇順と降順の両方を累積しているので、実行は常に長さ2以上になります(これにより、最大スタック深度が少なくとも1だけ減り、最初に実行を見つけるコストがかかります)。高度にランダム化されたデータのO(n log n)。

(ええと... 2番目の更新。)

または、ボトムアップマージソートのウィキペディアを参照してください。


「反転入力」を使用して実行の作成を適切に実行することは、すばらしいタッチです。O(log m)追加のメモリは必要ありません-1つが空になるまで2つのリストに交互に実行を追加するだけです。
greybeard 2015

1

配列にコピーしてから並べ替えることができます。

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

  • O(nlgn)のソート(merge sortなどの高速アルゴリズムを使用する場合)、

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

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

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


これはJørgenFoghの答えに何を追加しますか?
greybeard

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