Является ли LinkedList действительно быстрее, чем ArrayList в случае вставки в середине списка? - программирование
Подтвердить что ты не робот

Является ли LinkedList действительно быстрее, чем ArrayList в случае вставки в середине списка?

- В чем разница между LinkedList и ArrayList? Когда предпочтительнее использовать LinkedList?

Я думаю, что каждый разработчик Java слышал этот вопрос на собеседовании хотя бы один раз.

- Связанный список предпочтительнее, если вы хотите вставлять элементы в середине списка.

Это общий ответ на этот вопрос. Все это знают. Каждый раз, когда вы задаете вопрос о различии между реализациями 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

AL время: 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

AL время: 3103051

О, ArrayList все же быстрее, чем LinkedList. Какова причина? Был ли этот миф разорен? Или, может быть, я ошибаюсь?

enter image description here

4b9b3361

Ответ 1

Разоренный

Не совсем. Здесь

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

вы не просто вставляете элемент; вы платите стоимость итерации от начала до i каждый раз. Естественно, что O(i).

С другой стороны, список должен быть довольно большим, прежде чем вы действительно увидите преимущества производительности вставки среднего списка. System.arraycopy является быстродействующей операцией, а с другой стороны, каждая вставка в LinkedList требует выделения экземпляра node.

Таким образом, ArrayList является лучшим выбором для 99% или более случаев в реальном мире, а использование узкого преимущества LinkedList требует большой осторожности.

Общие замечания по микрообнаружению JVM

Я также должен предупредить вас, что ваш бенчмаркинг плохо страдает. Существует довольно значительный контрольный список вещей, на которые следует обратить внимание, когда вы микробенчарируете на JVM, например:

  • всегда разогревайте код, чтобы дать возможность компилятору JIT;
  • Будьте очень осторожны в интерпретации результатов nanoTime из-за ошибок точности/точности. Сделайте чтение, по крайней мере, в миллисекундах (миллионы наносекунд), чтобы обеспечить надежность;
  • контролировать побочные эффекты сборщика мусора;
  • и др.

Поэтому совет должен использовать готовый каркас для микрообнаружения, например OpenJDK jmh.

Ответ 2

Чтобы продемонстрировать (in) эффект операции add(), лучше использовать объект 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));
    }
}

Мои результаты показывают, что использование ListIterator в LinkedList дает наилучшую производительность для вставки элементов в "средний":

LL time:     237819474
AL time:      31410507
LL iterator:   5423172
AL iterator:  23975798

Ответ 3

У вашего теста есть предвзятость - он не измеряет обычную разницу в производительности.

Общие замечания о структуре LinkedList (по сравнению с ArrayList для большого списка):

  • добавление/удаление node на голове или хвосте очень быстро
  • Получение элемента из середины очень медленно
  • Получение элемента становится быстрее (линейно) по мере приближения к концу списка
  • Получение элемента из головы или хвоста приближается к скорости ArrayList
  • Добавление/удаление элемента где-то посередине - это две операции: get plus node insertion
  • Если вы используете ListIterator, вы можете добавить/удалить node где-то посередине и избежать операции get - очень быстрая операция

Ваш тест намеревается проверить (5).

Но он всегда выполняет худший случай - добавление/удаление элемента точно посередине.

Ваш микро-тест дает систематическую ошибку. Вам необходимо равномерно или произвольно распространять местоположение добавления/удаления. Или выполните макро-бенчмаркинг с реалистичными сложными и сложными приложениями.

Интересно прочитать о проблеме создания точного микро-теста: Теория и практика Java: анатомия ошибочного микрообъектива

Ответ 4

Я переписал программу Matej, чтобы случайным образом выбрать метод и запустить массив из 50 проб для каждого метода. Если вы берете в среднем самую быструю половину испытаний в каждой категории, то результат следующий:

LL: 570
AL: 120
Итератор LL: 1
AL iterator: 60

LL-итератор занимает много времени сортировки. В худших случаях производительность снижается в 15 раз из-за разминки (первый цикл) и gc (случайные всплески на несортированных данных).

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;
    }
}

Ответ 5

Некоторую осторожность следует выполнять с помощью простого профилирования следующим образом:

  • Сбор мусора может произойти в непредсказуемое время, замедляя непредсказуемая часть.
  • JRE работает медленнее, когда он начинается, а затем "прогревается".

Чтобы обойти это, сделайте профилирование в цикле, повторяя оба случая, много раз лучше в случайном порядке, и принимайте типичные значения, а не конечности. Иногда это приводит к различным результатам.

Ответ 6

поскольку ArrayList сохраняет значения последовательно таким образом 1: быстрее добавить значения (просто добавьте значение в последний индекс) 2: медленнее обновлять или удалять (нужно будет пройти весь список до того, как мы перейдем к node)

так как список массивов работает над концепцией LinkedList 1: медленнее при вставке (необходимо найти ссылку на предыдущее или следующее значение) 2: быстрее при обновлении (так как только точное node может быть достигнуто с помощью ссылки)

эта ссылка может быть отнесена

Ответ 7

в идеальном сценарии, который вы всегда вставляете в отсортированный список. Сначала вы находите индекс вставки с помощью двоичного механизма поиска, а затем вставляете его в этот индекс. Кроме того, при этом вы не можете использовать один и тот же проигрыватель все время. Вы установили итератор в New Evaluation. Таким образом, в этом сценарии реальной жизни, вставка которого выполняется быстрее.