19

LinkedList-とはどう違いArrayListますか? ?を使用するのが望ましいのはLinkedListいつですか?

すべての Java 開発者は、面接で少なくとも 1 回はこの質問を聞いたことがあると思います。

- リストの途中にアイテムを挿入できるようにしたい場合は、リンクされたリストが適しています。

この質問に対する一般的な答えです。誰もがそれを知っています。List 実装の違いについて質問するたびに、次のような回答が得られます。

LinkedList はいつ使用する必要がありますか? エレメント間または開始時に効率的な除去が必要になるのはいつですか?

ここから

挿入コストについて言及するのを忘れていました。LinkedList では、正しい位置を取得すると、挿入コストが発生しますがO(1)、ArrayList ではO(n)、挿入ポイントを超えるすべての要素を移動する必要があります。

ここから

リストの途中 (優先度キューなど) に項目を挿入できるようにする場合は、配列よりも連結リストの方が適しています。

ここから

ArrayList は、空きになったスロットを削除するために配列の一部をコピーする必要があるため、遅くなります。LinkedList は、いくつかの参照を操作するだけです。

ここから

もっと...

しかし、自分でそれを再現しようとしたことがありますか? 昨日試してみたところ、次の結果が得られました。

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class Test {
    public static void main(String... args) {
        final int MAX_VAL = 10000;
        List<Integer> linkedList = new LinkedList<Integer>();
        List<Integer> arrayList = new ArrayList<Integer>();
        for(int i = 0; i < MAX_VAL; i++) {
            linkedList.add(i);
            arrayList.add(i);
        }
        long time = System.nanoTime();
        for(int i = 0; i < MAX_VAL; i++) {
            linkedList.add(MAX_VAL/2, i);
        }
        System.out.println("LL time: " + (System.nanoTime() - time));
        time = System.nanoTime();
        for(int i = 0; i < MAX_VAL; i++) {
            arrayList.add(MAX_VAL/2, i);
        }
        System.out.println("AL time: " + (System.nanoTime() - time));
    }
}

出力:

LLタイム:114098106

アル時間: 24121889

それで、それは何ですか?なぜ LinkedList はそれほどひどいのでしょうか? 追加する代わりに削除操作を試す必要がありますか? では、試してみましょう:

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class Test {
    public static void main(String... args) {
        final int MAX_VAL = 10000;
        List<Integer> linkedList = new LinkedList<Integer>();
        List<Integer> arrayList = new ArrayList<Integer>();
        for(int i = 0; i < MAX_VAL; i++) {
            linkedList.add(i);
            arrayList.add(i);
        }
        long time = System.nanoTime();
        for(int i = 0; i < MAX_VAL/2; i++) {
            linkedList.remove(MAX_VAL/2);
        }
        System.out.println("LL time: " + (System.nanoTime() - time));
        time = System.nanoTime();
        for(int i = 0; i < MAX_VAL/2; i++) {
            arrayList.remove(MAX_VAL/2);
        }
        System.out.println("AL time: " + (System.nanoTime() - time));
    }
}

出力:

LLタイム:27581163

アル時間: 3103051

おお、ArrayList は LinkedList よりもまだ高速です。理由は何ですか?この神話は破られましたか?それとも私が間違っているのでしょうか?

ここに画像の説明を入力

4

8 に答える 8

28

バステッド

あまり。ここ

for(int i = 0; i < MAX_VAL; i++) {
    linkedList.add(MAX_VAL/2, i);
}

アイテムを挿入するだけではありません。最初からi毎回反復するコストを支払います。当然O(i)です。

一方、リストの途中に挿入することによるパフォーマンス上の利点を実際に目にするには、リストがかなり大きくなければなりません。System.arraycopyは非常に高速な操作であり、一方で、への挿入ごとにLinkedListノード インスタンスの割り当てが必要になります。

要約するArrayListと、実世界のケースの 99% 以上で がより適切な選択であり、a の狭い利点を活用するには細心のLinkedList注意が必要です。

JVM のマイクロベンチマークに関する一般的な注意事項

また、あなたのベンチマーク コードにはひどく欠陥があることも警告しておきます。JVM でマイクロベンチチャークを行う場合に注意すべき事項の非常に大きなチェックリストがあります。たとえば、次のとおりです。

  • 常にコードをウォームアップして、JIT コンパイラーがそれに到達できるようにします。
  • nanoTime精度/精度の問題のため、結果の解釈には十分注意してください。信頼性を確保するために、読み取り値を少なくともミリ秒 (数百万ナノ秒) 単位で増やします。
  • ガベージ コレクターの誤った副作用を制御します。

したがって、OpenJDK の jmhなどの既製のマイクロベンチマーク フレームワークを使用することをお勧めします。

于 2013-05-29T08:21:18.190 に答える
9

add() 操作の (非) 効果を実証するには、 listオブジェクトの代わりにListIteratorオブジェクトを使用することをお勧めします。リンクされたリストで add() メソッドを直接使用する場合は、リストの先頭から開始し、項目を挿入する位置まで繰り返す必要があります。この部分は O( n ) かかります。ListIterator を使用すると、要素を追加する位置が保持され、アルゴリズムは毎回リストの途中まで反復する必要がなくなります。

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;

public class Test {
    public static void main(String... args) {
        final int MAX_VAL = 10000;
        List<Integer> linkedList = new LinkedList<Integer>();
        List<Integer> arrayList = new ArrayList<Integer>();
        for(int i = 0; i < MAX_VAL; i++) {
            linkedList.add(i);
            arrayList.add(i);
        }
        long time = System.nanoTime();


        for(int i = 0; i < MAX_VAL; i++) {
            linkedList.add(MAX_VAL/2, i);
        }
        System.out.println("LL time:\t" + (System.nanoTime() - time));

        time = System.nanoTime();
        for(int i = 0; i < MAX_VAL; i++) {
            arrayList.add(MAX_VAL/2, i);
        }
        System.out.println("AL time:\t" + (System.nanoTime() - time));


        //Reset the lists
        linkedList = new LinkedList<Integer>();
        arrayList = new ArrayList<Integer>();
        for(int i = 0; i < MAX_VAL; i++) {
            linkedList.add(i);
            arrayList.add(i);
        }

        time = System.nanoTime();
        ListIterator<Integer> li = linkedList.listIterator(MAX_VAL/2);
        for(int i = 0; i < MAX_VAL; i++) {
            li.add(i);
        }
        System.out.println("LL iterator:\t" + (System.nanoTime() - time));

        time = System.nanoTime();
        ListIterator<Integer> ali = arrayList.listIterator(MAX_VAL/2);
        for(int i = 0; i < MAX_VAL; i++) {
            ali.add(i);
        }
        System.out.println("AL iterator:\t" + (System.nanoTime() - time));
    }
}

私の結果は、LinkedList で ListIterator を使用すると、「中間」に要素を挿入するための最高のパフォーマンスが得られることを示しています。

LL time:     237819474
AL time:      31410507
LL iterator:   5423172
AL iterator:  23975798
于 2013-05-29T08:38:01.177 に答える
1

私は Matej のプログラムを書き直して、メソッドをランダムに選択し、メソッドごとに 50 回の試行の配列を実行しました。各カテゴリの試行の最速の半分の平均を取ると、結果は次のようになります。

LL: 570
AL: 120
LL イテレーター: 1
AL イテレーター: 60

LL イテレーターは、ソーターに多くの時間がかかります。最悪の場合、ウォームアップ (最初のサイクル) と gc (ソートされていないデータでのランダムなスパイク) の両方が原因で、パフォーマンスが 15 分の 1 に低下します。

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Random;

public class TestList {

    public static void main(String... args) {
        final int MAX_VAL = 10000;
        int[] currentIndex = {0, 0, 0, 0};
        int[] remaining = {50, 50, 50, 50};
        int[][] sequence = new int[4][50];

        while (keepWorking(remaining)) { //run 50 tests for each case at random

            int currentMethod = chooseMethod(remaining); //choose case. Probability is higher for tests with less trials

            switch (currentMethod) { //run a test based on the choice
                case 0:
                    sequence[currentMethod][currentIndex[currentMethod]] = getLL(MAX_VAL);
                    break;
                case 1:
                    sequence[currentMethod][currentIndex[currentMethod]] = getAL(MAX_VAL);
                    break;
                case 2:
                    sequence[currentMethod][currentIndex[currentMethod]] = getLLIt(MAX_VAL);
                    break;
                default:
                    sequence[currentMethod][currentIndex[currentMethod]] = getALIt(MAX_VAL);
                    break;
            }

            remaining[currentMethod]--;
            currentIndex[currentMethod]++;
        }

        for (int[] ar : sequence) {
            Arrays.sort(ar);
        }

        System.out.println("Time (us\nLL    \tAL\tLL incr\t AL incr");
        for (int i = 0; i < sequence[0].length; i++) {
            System.out.println(sequence[0][i] + "\t" + sequence[1][i] + "\t" + sequence[2][i] + "\t" + sequence[3][i]);
        }
        System.out.println("\nTime normalized to fastest run of a method\nLL\tAL\tLL incr\t AL incr");
        for (int i = 0; i < sequence[0].length; i++) {
            System.out.print(i);
            for (int j = 0; j < sequence.length; j++) {  //to 4
                int a = sequence[j][i] / (sequence[j][0]/100); //to keep result within the scope of int
                System.out.print("\t" + a);
            }
            System.out.println();
        }
    }

    public static boolean keepWorking(int[] remaining) {

        for (int i = 0; i < remaining.length; i++) {
            if (remaining[i] > 0) {
                return true;
            }
        }
        return false;
    }

    public static int chooseMethod(int[] rem) {
        int[] bins = new int[rem.length];
        for (int i = 0; i < rem.length; i++) {
            for (int j = i; j < rem.length; j++) {
                bins[j] += rem[i];
            }
        }
        int randomNum = new Random().nextInt(bins[rem.length - 1]);
        for (int i = 0; i < bins.length; i++) {
            if (randomNum < bins[i]) {
                return i;
            }
        }
        return -1;
    }

    public static int getLL(int MAX_VAL) {

        List<Integer> linkedList = new LinkedList<>();
        for (int i = 0; i < MAX_VAL; i++) {
            linkedList.add(i);
        }
        long time = System.nanoTime();

        for (int i = 0; i < MAX_VAL; i++) {
            linkedList.add(MAX_VAL / 2, i);
        }
        return (int) (System.nanoTime() - time)/1000;
    }

    public static int getAL(int MAX_VAL) {

        List<Integer> arrayList = new ArrayList<>(MAX_VAL);
        for (int i = 0; i < MAX_VAL; i++) {
            arrayList.add(i);
        }
        long time = System.nanoTime();
        for (int i = 0; i < MAX_VAL; i++) {
            arrayList.add(MAX_VAL / 2, i);
        }
        return (int) (System.nanoTime() - time)/1000;
    }

    public static int getLLIt(int MAX_VAL) {

        List<Integer> linkedList = new LinkedList<>();
        for (int i = 0; i < MAX_VAL; i++) {
            linkedList.add(i);
        }

        long time = System.nanoTime();

        ListIterator<Integer> li = linkedList.listIterator(MAX_VAL / 2);
        for (int i = 0; i < MAX_VAL; i++) {
            li.add(i);
        }
        return (int) (System.nanoTime() - time)/1000;
    }

    public static int getALIt(int MAX_VAL) {

        List<Integer> arrayList = new ArrayList<>(MAX_VAL);
        for (int i = 0; i < MAX_VAL; i++) {
            arrayList.add(i);
        }

        long time = System.nanoTime();
        ListIterator<Integer> ali = arrayList.listIterator(MAX_VAL / 2);
        for (int i = 0; i < MAX_VAL; i++) {
            ali.add(i);
        }
        return (int) (System.nanoTime() - time)/1000;
    }
}
于 2016-08-09T16:47:22.700 に答える
0

理想的なシナリオでは、常に並べ替えられたリストに挿入します。まず、バイナリ検索メカニズムを使用して挿入インデックスを見つけ、次にそのインデックスに挿入します。また、これを行っている間、常に同じリストイテレータを使用することはできません。イテレータを毎回新しいインデックスの場所に設定します。したがって、この実際のシナリオでは、どちらの挿入が高速か.

于 2016-10-12T02:11:33.917 に答える
0

次のような単純なプロファイリングには注意が必要です。

  • 予測できない時間にガベージ コレクションが発生し、予測できない部分の速度が低下する場合があります。
  • JRE は、最初の起動時とその後の「ウォームアップ」時に遅くなります。

これを回避するには、ループ内でプロファイリングを行い、両方のケースをランダムな順序で何度も繰り返し、端ではなく典型的な値を取得します。これにより、異なる結果が得られることがあります。

于 2013-05-29T08:33:53.183 に答える