O(n)でソートする簡単なプログラムを作成しました。これは非常にメモリ効率が悪いですが、それは重要ではありません。
ソートにはHashMap
の背後にある原則を使用します。
public class NLogNBreak {
public static class LinkedListBack {
public LinkedListBack(int val){
first = new Node();
first.val = val;
}
public Node first = null;
public void insert(int i){
Node n = new Node();
n.val = i;
n.next = first;
first = n;
}
}
private static class Node {
public Node next = null;
public int val;
}
//max > in[i] > 0
public static LinkedListBack[] sorted(int[] in, int max){
LinkedListBack[] ar = new LinkedListBack[max + 1];
for (int i = 0; i < in.length; i++) {
int val = in[i];
if(ar[val] == null){
ar[val] = new LinkedListBack(val);
} else {
ar[val].insert(val);
}
}
return ar;
}
}
それで、これはファンキーな形式で結果を返しますが、一種のO(n)としてカウントされますか?
あなたの質問に直接答えるには:
では、このΩ(n log n)バリアとは何ですか?それはどこから来たのですか?そして、どのようにそれを壊しますか?
Ω(nlog n)バリアは、任意の比較ベースソートアルゴリズムの平均ケース速度の情報理論上の下限です。配列要素を区別するために適用できる操作が、ある種の比較を実行することだけである場合、平均的な場合、ソートアルゴリズムはΩ(nlog n)よりも優れたパフォーマンスを発揮できません。
これがなぜであるかを理解するために、実行中の任意の時点でのアルゴリズムの状態について考えてみましょう。アルゴリズムの実行中に、入力要素の順序付け方法に関するある程度の情報を取得できます。アルゴリズムに入力要素の元の順序に関する情報Xのセットがある場合、アルゴリズムは状態Xにあるとしましょう。
Ω(nlog n)引数(および後で説明するいくつかの関連する引数)の要点は、アルゴリズムが入力内容に基づいて多数の異なる状態に入る機能を備えている必要があることです。今のところ、ソートアルゴリズムへの入力がn個の異なる値の配列であると仮定しましょう。アルゴリズムは、順序付けられた方法以外にこれらの要素について何も伝えることができないため、ソートされる値が何であるかは実際には重要ではありません。重要なのは、これらのn個の要素の相対的な順序です。
ここで重要なステップとして、n個の入力要素を順序付ける独自の方法がf(n)あると仮定し、ソートアルゴリズムが少なくともf(n)異なる状態。これが当てはまる場合、アルゴリズムが常に同じ状態にグループ化する配列内の要素の2つの異なる順序が必要です。これが発生した場合、並べ替えアルゴリズム2つの入力配列の両方を正しくソートできない可能性があります。この背後にある理由は、アルゴリズムが2つの配列を同じように処理するため、最初の配列の要素を並べ替えるために使用する手順は、使用する手順と同じになるためです。 2番目の配列の要素を並べ替えるには2つの配列が同じではないため、2つのケースのいずれかで位置がずれている要素が少なくとも1つ必要です。したがって、並べ替えアルゴリズムは次のことを行う必要があります。 f(n)さまざまな状態に入ることができます。
しかし、アルゴリズムはどのようにしてこれらの異なる状態に入ることができますか?さて、これについて考えてみましょう。最初、アルゴリズムには要素の順序に関する情報がまったくありません。最初の比較を行うとき(たとえば、要素A [i]とA [j]の間)、アルゴリズムは2つの状態のいずれかになります。1つはA [i] <A [j]で、もう1つはA [i]です。 > A [j]。より一般的には、アルゴリズムが行うすべての比較は、最良の場合、比較の結果に基づいてアルゴリズムを2つの新しい状態のいずれかに置くことができます。したがって、アルゴリズムが存在できる状態を説明する大きな二分木構造を考えることができます。各状態には、行われた比較の結果に基づいてアルゴリズムがどの状態になるかを説明する最大2つの子があります。ツリーのルートからリーフまでのパスをたどると、特定の入力に対してアルゴリズムによって行われる一連の比較が得られます。できるだけ早く並べ替えるために、比較の数をできるだけ少なくしたいので、このツリー構造の高さをできるだけ小さくしたいと思います。
今、私たちは2つのことを知っています。まず、アルゴリズムが入ることができるすべての状態を二分木と考えることができます。次に、その二分木には少なくともf(n)異なるノードが含まれている必要があります。これを考えると、構築できる最小の二分木は少なくともΩ(logf(log f( n))。これは、配列要素の順序付けにf(n)異なる可能な方法がある場合、少なくともΩ(log)を作成する必要があることを意味します。 f(n))平均の比較。そうしないと、十分に異なる状態に入ることができないためです。
Ω(nlog n)を打ち負かすことができないという証明を結論付けるために、配列にn個の異なる要素がある場合、n個あることに注意してください。要素を順序付けるさまざまな可能な方法。 スターリングの近似を使用すると、その対数nが得られます! =Ω(nlog n)であるため、入力シーケンスを並べ替えるには、平均的なケースで少なくともΩ(n log n)の比較を行う必要があります。
上で見たものでは、すべて異なる配列要素がn個ある場合、Ω(n log n)よりも速く比較ソートでそれらをソートできないことがわかりました。ただし、この開始時の仮定は必ずしも有効ではありません。並べ替えたい配列の多くには、要素が重複している可能性があります。たとえば、次のような0と1のみで構成される配列を並べ替えたいとします。
0 1 0 1 1 1 0 0 1 1 1
この場合、nがあることはnottrueです!長さnのゼロと1の異なる配列。実際、2つしかありませんn そのうちの。上記の結果から、これはΩ(log 2)でソートできるはずであることを意味します。n)=純粋に比較ベースのソートアルゴリズムを使用したΩ(n)時間。実際、私たちは絶対にこれを行うことができます。これが私たちがそれを行う方法のスケッチです:
これが機能することを確認するために、0が最初の要素である場合、「less」配列は空になり、「equal」配列にはすべて0が含まれ、「greater」配列にはすべて1が含まれます。次に、それらを連結すると、すべてのゼロがすべてのゼロの前に配置されます。それ以外の場合、1が最初の要素である場合、less
配列はゼロを保持し、equal
配列はゼロを保持し、greater
配列は空になります。したがって、それらの連結は、必要に応じて、すべてゼロの後にすべて1が続きます。
実際には、このアルゴリズムは使用しません(以下で説明するように、カウントソートを使用します)が、可能な入力の数があれば、比較ベースのアルゴリズムでΩ(n log n)を実際に打ち負かすことができることを示しています。アルゴリズムへの変換は小さいです。
一部の比較ベースの並べ替えアルゴリズムは、重複する値が複数ある入力に対して非常に迅速に機能することが知られています。たとえば、 特別なパーティション分割ステップを使用したクイックソート は、入力配列内の重複した要素を利用できることが知られています。
この説明はすべて、配列要素で許可されている操作が比較のみである、比較ベースの並べ替えについて話していることを前提としています。ただし、並べ替える要素について詳しく知っていて、単純な比較を超えてそれらの要素に対して操作を実行できる場合は、上記の範囲のいずれも当てはまりません。アルゴリズムのすべての状態の二分木を構築することになった最初の仮定を破っているので、それらの境界がまだ保持されていると疑う理由はありません。
たとえば、入力値が| U |のみを持つユニバースから取得されていることがわかっている場合です。その中の要素は、巧妙なアルゴリズムを使用してO(n + | U |)時間で並べ替えることができます。 | U |を作成することから始めます元の配列の要素を配置できる別のバケット。次に、配列全体を反復処理し、すべての配列要素を対応するバケットに分散します。最後に、最小の要素のコピーを保持するバケットから始まり、最大の要素のコピーを含むバケットで終わる各バケットにアクセスし、見つかったすべての値を連結します。たとえば、値1〜5で構成される配列を並べ替える方法を見てみましょう。この開始配列がある場合:
1 3 4 5 2 3 2 1 4 3 5
次に、これらの要素を次のようにバケットに入れることができます。
Bucket 1 2 3 4 5
-------------
1 2 3 4 5
1 2 3 4 5
3
バケット間で繰り返し、それらの値を連結すると、次のようになります。
1 1 2 2 3 3 3 4 4 5 5
これは確かに、元の配列のソートされたバージョンです!ここでの実行時間はO(n)元の配列要素をバケットに分散する時間です。次にO(n + | U |)時間ですべてのバケットを反復して要素を元に戻します。 | U | = O(n)の場合、これはO(n)時間で実行され、Ω(n log n)の並べ替えの障壁を破ることに注意してください。
整数をソートする場合は、O(n lg | U |)で実行される 基数ソート を使用することで、これよりもはるかに優れた方法を実行できます。プリミティブint
sを扱っている場合、lg | U |通常は32または64なので、これは非常に高速です。特にトリッキーなデータ構造を実装する場合は、 van Emde Boas Tree を使用して、0からU-1までの整数を時間O(n lg lg U)で並べ替えることができます。整数は、ブロックで操作できるビットのグループで構成されているという事実。
同様に、要素が文字列であることがわかっている場合は、文字列から trie を作成し、トライ全体を繰り返してすべての文字列を再構築することで、非常にすばやく並べ替えることができます。または、文字列を大きなベース(たとえば、ASCIIテキストの場合はベース128)で記述された数値と見なし、上記の整数ソートアルゴリズムの1つを使用することもできます。
これらのいずれの場合でも、情報理論の障壁を打ち負かすことができる理由は、障壁の最初の仮定を破っている、つまり、比較しか適用できないためです。入力要素を数値、文字列、またはより構造を明らかにするその他のものとして扱うことができれば、すべての賭けは無効になり、非常に効率的に並べ替えることができます。
お役に立てれば!
それは Radix Sort
そして、はい、それはnlog(n)バリアを破ります。これは、 Comparison Model
。比較モデルにリンクされているウィキペディアのページで、それを使用する種類のリストと、使用しない種類のリストを確認できます。
基数ソートは、各要素をその値に基づいてバケットに入れ、最後にすべてのバケットを再び連結することによってソートします。可能な値の数が有限である整数のような型でのみ機能します。
通常、基数ソートは、バケットの数を減らすために、一度に1バイトまたはニブルで実行されます。ウィキペディアの記事を参照するか、詳細を検索してください。
負の数を並べ替えて、それを改善するために使用するバケットにのみメモリを割り当てるようにすることもできます。