回答:
実行時に 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つのアルゴリズムのパフォーマンスを向上させる一定の要因を隠している場合があります。
listsort
が表示されている場合は、パラメータを使用して切り替えることができますint is_double
。
listsort
CコードのPythonバージョンを示します
いくつかの要因に応じて、実際にリストを配列にコピーしてからクイックソートを使用する方が速い場合があります。
これがより高速になる理由は、リンクされたリストよりもアレイのキャッシュパフォーマンスがはるかに優れているためです。リスト内のノードがメモリ内に分散している場合、場所全体でキャッシュミスが発生している可能性があります。また、配列が大きい場合は、とにかくキャッシュミスが発生します。
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しかないため、キャッシュは非常に重要です。
比較ソート(つまり、要素の比較に基づくソート)は、おそらくより速くなることはできませんn log n
。基礎となるデータ構造が何であるかは関係ありません。Wikipediaを参照してください。
リストに同じ要素が多数あることを利用する他の種類の並べ替え(カウントの並べ替えなど)、またはリスト内の要素の予想されるいくつかの分布は、特にうまくいくとは思えませんが高速ですリンクされたリスト。
これは、このトピックに関する素晴らしい小さなペーパーです。彼の経験的な結論は、Treesortが最も優れており、QuicksortとMergesortがそれに続くことです。堆積物ソート、バブルソート、セレクションソートのパフォーマンスが非常に悪い。
Ching-Kuang Sheneによるリンクドリストソーティングアルゴリズムの比較研究
http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.31.9981
何度も述べたように、一般データの比較ベースのソートの下限は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の最大数のサイズです)。
また、完全なハッシュ(または少なくともすべての値を異なる方法でマッピングするハッシュ)を持つオブジェクトがある場合、それらのハッシュ関数でカウントまたは基数ソートを使用してみてください。
A 基数ソート特にそれは数字の各値に対応するヘッドポインタのテーブルを作るのは簡単なので、リンクリストに適しています。
マージソートは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 );
}
O(n lg n)
は比較ベースではない(例:基数ソート)よりも高速です。定義により、比較ソートは、全順序を持つ(つまり、比較できる)ドメインに適用されます。
私が知っているように、コンテナが何であれ、最良のソートアルゴリズムはO(n * log n)です-単語の広い意味でのソート(マージソート/クイックソートなどのスタイル)は低くできないことが証明されています。リンクされたリストを使用しても、実行時間は短縮されません。
O(n)で実行される唯一のアルゴリズムは、実際のソートではなくカウント値に依存する「ハック」アルゴリズムです。
O(n lg c)
ます。すべての要素が一意である場合c >= n
、なので、に比べて時間がかかりますO(n lg n)
。
次に、リストを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つのリストに交互に実行を追加するだけです。
配列にコピーしてから並べ替えることができます。
配列O(n)へのコピー、
O(nlgn)のソート(merge sortなどの高速アルゴリズムを使用する場合)、
必要に応じてリンクリストO(n)にコピーします。
だから、O(nlgn)になります。
リンクされたリストの要素数がわからない場合は、配列のサイズがわかりません。Javaでコーディングしている場合は、たとえばArraylistを使用できます。