これについて 前の投稿 をフォローしていました:
LinkedListの場合
- getはO(n)です
- addはO(1)です
- removeはO(n)です
- Iterator.removeはO(1)です
ArrayListの場合
- getはO(1)です
- add is O(1) amortized、ただしO(n)配列のサイズを変更してコピーする必要があるため最悪の場合
- removeはO(n)です
そのため、これを見て、たとえば5000000個の要素に対してコレクションに順次挿入を行うと、LinkedList
はArrayList
を上回ると結論付けました。
そして、コレクションを反復することでコレクションから要素を取得するだけの場合、つまり中間で要素を取得しない場合でも、LinkedList
は `ArrayListを凌outします。
上記の2つのステートメントを検証するために、サンプルプログラムを以下に記述しました…しかし、上記のステートメントが間違っていることが判明したことに驚いています。
どちらの場合も、ArrayList
はLinkedlist
を外しました。コレクションからの追加と取得にLinkedList
よりも時間がかかりませんでした。私が間違っていることはありますか、またはLinkedList
およびArrayList
に関する最初のステートメントがサイズ5000000のコレクションに当てはまりませんか?
要素の数を50000に減らすと、LinkedList
のパフォーマンスが向上し、初期ステートメントが成り立つため、サイズについて言及しました。
long nano1 = System.nanoTime();
List<Integer> arr = new ArrayList();
for(int i = 0; i < 5000000; ++i) {
arr.add(i);
}
System.out.println( (System.nanoTime() - nano1) );
for(int j : arr) {
;
}
System.out.println( (System.nanoTime() - nano1) );
long nano2 = System.nanoTime();
List<Integer> arrL = new LinkedList();
for(int i = 0; i < 5000000; ++i) {
arrL.add(i);
}
System.out.println( (System.nanoTime() - nano2) );
for(int j : arrL) {
;
}
System.out.println( (System.nanoTime() - nano2) );
Big-Oの複雑さは漸近的な振る舞いを表し、実際の実装速度を反映していない可能性があることに注意してください。各操作の速度ではなく、リストのサイズによって各操作のコストがどのように増加するかを説明します。たとえば、次のadd
の実装はO(1)ですが、高速ではありません。
_public class MyList extends LinkedList {
public void add(Object o) {
Thread.sleep(10000);
super.add(o);
}
}
_
ArrayListがうまく機能しているのは、内部バッファサイズがかなり積極的に増加し、再割り当てが多数発生しないためです。バッファーのサイズを変更する必要がない場合、ArrayListのadd
sは高速になります。
また、この種のプロファイリングを行うときは非常に注意する必要があります。プロファイリングコードを変更してウォームアップフェーズを実行し(JITが結果に影響を与えずに最適化を実行できるようにします)、複数の実行で結果を平均することをお勧めします。
_private final static int WARMUP = 1000;
private final static int TEST = 1000;
private final static int SIZE = 500000;
public void perfTest() {
// Warmup
for (int i = 0; i < WARMUP; ++i) {
buildArrayList();
}
// Test
long sum = 0;
for (int i = 0; i < TEST; ++i) {
sum += buildArrayList();
}
System.out.println("Average time to build array list: " + (sum / TEST));
}
public long buildArrayList() {
long start = System.nanoTime();
ArrayList a = new ArrayList();
for (int i = 0; i < SIZE; ++i) {
a.add(i);
}
long end = System.nanoTime();
return end - start;
}
... same for buildLinkedList
_
(sum
はオーバーフローする可能性があるので、System.currentTimeMillis()
を使用した方がよいことに注意してください)。
また、コンパイラーは空のget
ループを最適化することも可能です。ループが実際に何かを実行して、適切なコードが呼び出されるようにします。
これは悪いベンチマークIMOです。
ArrayList
のサイズ変更はコストがかかります。 ArrayList
をnew ArrayList(500000)
として作成した場合、一度に作成するだけで、すべての割り当ては非常に安価になります(1つは事前に割り当てられた配列)list.get
_のランダムな選択として大きなコレクションのサイズの10%をフィードすると、最初または最後の要素以外のものを取得するのにリンクリストがひどいことに気付くでしょう。Arraylistの場合:jdk getは期待どおりです:
_public E get(int index) {
RangeCheck(index);
return elementData[index];
}
_
(基本的には、インデックス付き配列要素を返すだけです。
リンクリストの場合:
_public E get(int index) {
return entry(index).element;
}
_
似ている?そうでもない。エントリはプリミティブ配列ではなくメソッドであり、それが何をしなければならないかを見てください:
_private Entry<E> entry(int index) {
if (index < 0 || index >= size)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+size);
Entry<E> e = header;
if (index < (size >> 1)) {
for (int i = 0; i <= index; i++)
e = e.next;
} else {
for (int i = size; i > index; i--)
e = e.previous;
}
return e;
}
_
そうです、たとえばlist.get(250000)
を要求した場合、先頭から開始して、次の要素を繰り返し処理する必要があります。 250000アクセス程度(アクセスが少ない方に応じて、先頭または末尾から始まるコードに最適化があります)
ArrayListは、LinkedListよりも単純なデータ構造です。 ArrayListには、連続したメモリ位置に単一のポインター配列があります。割り当てられたサイズを超えて配列が拡張された場合にのみ、再作成する必要があります。
LinkedListは、ノードのチェーンで構成されています。各ノードは個別に割り当てられ、他のノードへのフロントポインターとバックポインターが割り当てられます。
これはどういう意味ですか?途中で挿入、スプライス、途中で削除などを行う必要がない限り、通常はArrayListの方が高速です。必要なメモリ割り当てが少なく、参照の局所性がはるかに優れています(プロセッサのキャッシュにとって重要です)。
なぜあなたが得た結果が「大きなO」の特性と矛盾しないかを理解するために。最初の原則に戻る必要があります。すなわち 定義 。
f(x) and g(x)=は、実数のサブセットで定義された2つの関数です。
_f(x) = O(g(x)) as x -> infinity
_十分な大きさのxの場合、f(x)は、最大で定数にg(x)を絶対値で乗じたものです。 f(x) = O(g(x))正の実数Mと実数x0が存在する場合にのみ
_|f(x)| <= M |g(x)| for all x > x_0.
_多くのコンテキストでは、変数xが無限大になるにつれて成長率に関心があるという仮定は述べられておらず、より単純にf(x) = O(g(x ))。
したがって、ステートメントadd1 is O(1)
は、サイズNのリストに対する_add1
_操作の時間コストが定数Cに向かう傾向があることを意味します。add1 Nは無限に向かう傾向があります。
ステートメントadd2 is O(1) amortized over N operations
は、N個の_add2
_操作のシーケンスのいずれかのaverage時間コストが定数Cに向かう傾向があることを意味しますadd2 Nは無限に向かう傾向があります。
言っていないのは、これらの定数Cadd1 およびCadd2 あります。実際、LinkedListがベンチマークでArrayListより遅い理由は、Cadd1 Cより大きいadd2。
教訓は、大きなO表記では絶対的なパフォーマンスや相対的なパフォーマンスさえ予測できないということです。予測されるのは、制御変数が非常に大きくなるときのパフォーマンス関数のshapeだけです。これは知っておくと便利ですが、知っておく必要があることをすべて伝えているわけではありません。
1)基礎となるデータ構造 ArrayListとLinkedListの最初の違いは、ArrayListがArrayに対応し、LinkedListがLinkedListに対応しているという事実です。これにより、パフォーマンスがさらに異なります。
2)LinkedListはDequeを実装します ArrayListとLinkedListのもう1つの違いは、Listedインターフェースとは別に、LinkedListがDequeインターフェースも実装することです。 Deque関数。 )ArrayListに要素を追加 ArrayListに要素を追加すると、O(1) Arrayのサイズ変更をトリガーしない場合は操作になり、その場合はOになります(log(n))、一方、LinkedListに要素を追加することは、ナビゲーションを必要としないため、O(1)操作です。
4)位置から要素を削除する特定のインデックスから要素を削除するには、たとえばremove(index)を呼び出すことにより、ArrayListはO(n)に近づけるコピー操作を実行しますが、LinkedListはO(n/2)になるポイントまで移動する必要があります。近接度に基づいて、どちらの方向からも横断できます。
5)ArrayListまたはLinkedListでの反復反復は、LinkedListとArrayListの両方に対するO(n)操作です。nは要素の数です。
6)位置から要素を取得する=== get(index)操作はO(1) ArrayListで、O(n/2) LinkedListでは、そのエントリまで移動する必要があるため。ただし、Big O表記ではO(n/2)はただO(nそこの定数を無視するため。
7)Memory LinkedListは、ラッパーオブジェクトであるEntryを使用します。これは、ArrayListが配列にデータを格納するだけで、前後にデータと2つのノードを格納する静的なネストクラスです。
そのため、Arrayが1つのArrayから別のArrayにコンテンツをコピーするときにサイズ変更操作を実行する場合を除き、ArrayListの場合のメモリ要件はLinkedListよりも少ないようです。
配列が十分に大きい場合、その時点で大量のメモリが必要になり、ガベージコレクションがトリガーされ、応答時間が遅くなる可能性があります。
ArrayListとLinkedListの上記のすべての違いから、remove()またはget()よりも頻繁にadd()操作を行う場合を除き、ほとんどすべての場合にArrayListがLinkedListよりも良い選択であるように見えます。
リンクリストは内部でそれらの位置の参照を保持し、O(1)時間でアクセスできるため、特に開始または終了から要素を追加または削除する場合、ArrayListよりもリンクリストを変更する方が簡単です。
つまり、要素を追加する位置に到達するためにリンクリストを走査する必要はありません。その場合、加算はO(n)操作になります。たとえば、リンクリストの途中で要素を挿入または削除します。
私の意見では、Javaでの実際の目的のほとんどにLinkedListではなくArrayListを使用します。
Big-O表記は絶対的なタイミングに関するものではなく、相対的なタイミングに関するものであり、あるアルゴリズムの数を別のアルゴリズムと比較することはできません。
タプル数の増減に対して同じアルゴリズムがどのように反応するかについての情報のみを取得します。
1つのアルゴリズムは1つの操作に1時間、2つの操作に2時間、O(n)であり、別のアルゴリズムはO(n)であり、1つの操作に1ミリ秒かかります。 2つの操作に対して2ミリ秒。
JVMで測定する場合の別の問題は、hotspot-compilerの最適化です。何もしないループは、JITコンパイラーによって除去される場合があります。
3番目に考慮する必要があるのは、キャッシュを使用してガベージコレクションを実行するOSとJVMです。
LinkedListの適切な使用例を見つけるのは困難です。 Dequeuインターフェイスのみを使用する必要がある場合は、おそらくArrayDequeを使用する必要があります。 Listインターフェースを本当に使用する必要がある場合、LinkedListはランダム要素にアクセスする際の動作が非常に悪いため、常にArrayListを使用するようにという提案をよく耳にします。
残念ながら、リストの先頭または中央の要素を削除または挿入する必要がある場合、ArrayListにもパフォーマンスの問題があります。
ただし、ArrayListとLinkedListの両方の長所を組み合わせたGapListと呼ばれる新しいリスト実装があります。 ArrayListとLinkedListの両方のドロップイン置換として設計されているため、ListとDequeの両方のインターフェイスを実装します。また、ArrayListによって提供されるすべてのパブリックメソッドが実装されます(ensureCapacty、trimToSize)。
GapListの実装は、(ArrayListが行うように)インデックスによる要素への効率的なランダムアクセスを保証し、同時に(LinkedListが行うように)リストの先頭と末尾への要素の効率的な追加と削除を保証します。
GapListの詳細については、 http://Java.dzone.com/articles/gaplist-%E2%80%93-lightning-fast-list を参照してください。
O表記分析は重要な情報を提供しますが、それには限界があります。定義により、O表記分析では、すべての操作の実行にほぼ同じ時間がかかると見なされますが、これは正しくありません。 @seandが指摘したように、リンクリストは内部でより複雑なロジックを使用して要素を挿入およびフェッチします(ソースコードを見て、Ctrlキーを押しながらIDEでクリックできます)。 ArrayListは、内部的に要素を配列に挿入し、そのサイズをたまに増やすだけで済みます(これはo(n)操作であっても、実際には非常に高速に実行できます)。
乾杯
追加または削除は、2段階の操作として分離できます。
LinkedList:インデックスnに要素を追加する場合、ポインタを0からn-1に移動し、いわゆるO(1)追加操作:削除操作は同じです。
ArraryList:ArrayListはRandomAccessインターフェイスを実装します。これは、O(1)の要素にアクセスできることを意味します。
インデックスnに要素を追加すると、O(1)のn-1インデックスに移動し、n-1の後に要素を移動して、nスロットに要素を設定します。
移動操作はSystem.arraycopy
と呼ばれるネイティブメソッドによって実行され、非常に高速です。
public static void main(String[] args) {
List<Integer> arrayList = new ArrayList<Integer>();
for (int i = 0; i < 100000; i++) {
arrayList.add(i);
}
List<Integer> linkList = new LinkedList<Integer>();
long start = 0;
long end = 0;
Random random = new Random();
start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
linkList.add(random.nextInt(100000), 7);
}
end = System.currentTimeMillis();
System.out.println("LinkedList add ,random index" + (end - start));
start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
arrayList.add(random.nextInt(100000), 7);
}
end = System.currentTimeMillis();
System.out.println("ArrayList add ,random index" + (end - start));
start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
linkList.add(0, 7);
}
end = System.currentTimeMillis();
System.out.println("LinkedList add ,index == 0" + (end - start));
start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
arrayList.add(0, 7);
}
end = System.currentTimeMillis();
System.out.println("ArrayList add ,index == 0" + (end - start));
start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
linkList.add(i);
}
end = System.currentTimeMillis();
System.out.println("LinkedList add ,index == size-1" + (end - start));
start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
arrayList.add(i);
}
end = System.currentTimeMillis();
System.out.println("ArrayList add ,index == size-1" + (end - start));
start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
linkList.remove(Integer.valueOf(random.nextInt(100000)));
}
end = System.currentTimeMillis();
System.out.println("LinkedList remove ,random index" + (end - start));
start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
arrayList.remove(Integer.valueOf(random.nextInt(100000)));
}
end = System.currentTimeMillis();
System.out.println("ArrayList remove ,random index" + (end - start));
start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
linkList.remove(0);
}
end = System.currentTimeMillis();
System.out.println("LinkedList remove ,index == 0" + (end - start));
start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
arrayList.remove(0);
}
end = System.currentTimeMillis();
System.out.println("ArrayList remove ,index == 0" + (end - start));
}