web-dev-qa-db-ja.com

Listを反復処理する方が、インデックスを作成するよりも高速になるのはなぜですか?

ADTリストのJavaドキュメント を読むと:

Listインターフェースは、リスト要素への位置(インデックス付き)アクセスのための4つのメソッドを提供します。リスト(Java配列など)はゼロベースです。これらの操作は、一部の実装(たとえば、LinkedListクラスなど)のインデックス値に比例して時間内に実行される場合があります。呼び出し元が実装を知らない場合、リスト内のインデックスは通常、リストを介してインデックスを作成するよりも望ましいです。

これはどういう意味ですか?私は描かれた結論を理解していません。

123
nomel7

リンクリストでは、各要素に次の要素へのポインタがあります。

_head -> item1 -> item2 -> item3 -> etc.
_

_item3_にアクセスするには、直接ジャンプできないため、item3に到達するまで、すべてのノードを頭から歩く必要があることが明確にわかります。

したがって、各要素の値を出力したい場合、次のように書くと:

_for(int i = 0; i < 4; i++) {
    System.out.println(list.get(i));
}
_

これはどうなりますか:

_head -> print head
head -> item1 -> print item1
head -> item1 -> item2 -> print item2
head -> item1 -> item2 -> item3 print item3
_

これは恐ろしく非効率的です。なぜなら、インデックスを作成するたびにリストの先頭から再起動し、すべてのアイテムを処理するからです。これは、リストを走査するためだけに、あなたの複雑さが実質的にO(N^2)であることを意味します!

代わりに私がこれをした場合:

_for(String s: list) {
    System.out.println(s);
}
_

次に何が起こるのですか:

_head -> print head -> item1 -> print item1 -> item2 -> print item2 etc.
_

すべてO(N)である単一のトラバーサルで。

ここで、ListであるArrayListの別の実装に進み、その1つは単純な配列に支えられています。その場合、配列は連続しているため、上記のトラバーサルは両方とも同等であり、任意の位置へのランダムなジャンプが可能です。

210
Tudor

答えはここに暗示されています:

これらの操作は、一部の実装(LinkedListクラスなど)のインデックス値に比例して実行される場合があることに注意してください。

リンクリストには固有のインデックスがありません。 .get(x)を呼び出すには、リストの実装が最初のエントリを見つけて.next() x-1回呼び出す(O(n)または線形時間の場合)アクセス)、ここで、配列に裏打ちされたリストは、backingarray[x] in O(1)または一定時間。

LinkedListのJavaDoc を見ると、コメントが表示されます。

すべての操作は、二重にリンクされたリストで予想されるとおりに実行されます。リストにインデックスを付ける操作は、指定されたインデックスに近い方の最初または最後からリストを走査します。

一方、 JavaDoc for ArrayList は対応する

Listインターフェイスのサイズ変更可能な配列の実装。オプションのリスト操作をすべて実装し、nullを含むすべての要素を許可します。 Listインターフェースの実装に加えて、このクラスは、リストを保存するために内部的に使用される配列のサイズを操作するメソッドを提供します。 (このクラスは、非同期である点を除いて、ベクターとほぼ同等です。)

sizeisEmptygetsetiterator、およびlistIterator操作は一定時間で実行されます。追加操作は償却された一定時間で実行されます。つまり、n個の要素を追加するにはO(n)時間が必要です。他のすべての操作は線形時間で実行されます。 LinkedList実装の場合と比較して。

「Java Collections Framework)のBig-Oサマリー」というタイトルの関連質問 このリソースを指す回答があります "Java Collections JDK6" あなたは役に立つかもしれません。

35
andersoj

iなど、ルックアップのオフセットを使用してリストを反復処理することは、Shlemiel the Painterのアルゴリズムに類似しています。

シュレミエルはストリートペインターとしての仕事を得て、道路の中央に点線を描きます。初日、彼はペンキの缶を道路に運び出し、300ヤードの道路を仕上げます。 「それはかなり良いです!」上司は「あなたは速い労働者だ!」と言います。そして彼にコペックを支払います。

翌日、シュレミエルは150ヤードしか達成できません。 「まあ、それは昨日ほどではありませんが、あなたはまだ速い労働者です。150ヤードは立派です」と彼にコペックを支払います。

翌日、シュレミエルは道路の30ヤードを塗ります。 「わずか30!」上司を叫ぶ。 「それは受け入れられない!あなたは最初の日にあなたが10回その仕事をした!何が起こっているのか?」

「仕方ない」とシュレミエルは言う。 「毎日、私はペイント缶からどんどん離れていきます!」

ソース

この小さな話は、内部で何が起こっているのか、なぜそれが非効率的であるのかを理解しやすくするかもしれません。

7
alex

受け入れられた答えは確かに正しいですが、小さな欠陥を指摘するかもしれません。チューダーの引用:

次に、ArrayListであるListのもう1つの実装に進みます。これは単純な配列によってサポートされています。 その場合、上記のトラバーサルは両方とも同等です。これは、配列が連続しているため、任意の位置にランダムにジャンプできるためです。

これは完全に真実ではありません。真実は、

ArrayListを使用すると、手書きのカウントループが約3倍高速になります。

ソース:パフォーマンスのための設計、GoogleのAndroid doc

手書きのループは、インデックス付きの繰り返しを指すことに注意してください。拡張forループで使用されるイテレーターが原因だと思われます。連続した配列に裏打ちされた構造では、ペナルティのマイナーパフォーマンスが生成されます。また、これはVectorクラスにも当てはまると思われます。

私のルールは、可能な限り強化されたforループを使用し、パフォーマンスを本当に重視する場合は、ArrayListsまたはVectorのいずれかにのみインデックス付き反復を使用することです。ほとんどの場合、これを無視することもできます。コンパイラはバックグラウンドでこれを最適化している可能性があります。

Androidでの開発のコンテキストでは、ArrayListのトラバーサルは両方とも必ずしも同等ではないことを指摘したいだけです。思考の糧。

7
Dhruv Gairola

LinkedListのi番目の要素を見つけるために、実装はi番目までのすべての要素を調べます。

そう

for(int i = 0; i < list.length ; i++ ) {
    Object something = list.get(i); //Slow for LinkedList
}
4
esej