ArrayList Против LinkedList

я предыдущий пост на этом, что говорит:

Для LinkedList

  • get is O (n)
  • add is O (1)
  • удалить - Это O (n)
  • итератор.удалить is O (1)

Для ArrayList

  • get is O (1)
  • add-O(1) амортизируется, но O (n) в худшем случае, так как массив должен быть изменен и скопирован
  • удалить - Это O (n)

Итак, посмотрев на это, я пришел к выводу, что если мне нужно сделать только последовательную вставку в моей коллекции, скажем, 5000000 элементов,LinkedList будет позади ArrayList.

и если мне нужно просто извлечь элементы из коллекции, повторяя, т. е. не захватывая элемент посередине, все еще LinkedList будет превосходить `ArrayList.

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

ArrayList класс Linkedlist в обоих случаях. Это заняло меньше времени, чем LinkedList для добавления, а также извлечения их из коллекции. Есть ли что-то, что я делаю неправильно, или первоначальные утверждения о LinkedList и ArrayList не выполняется для коллекций размером 5000000?

я упомянул размер, потому что если я уменьшу количество элементов до 50000,LinkedList выполняет лучше и первоначальные операторы hold истинный.

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

9 ответов


помните, что сложность big-O описывает асимптотическое поведение и может не отражать фактическую скорость реализации. Он описывает, как стоимость каждой операции растет с размером списка, а не скорость каждой операции. Например, следующая реализация add за O(1), но не быстро:

public class MyList extends LinkedList {
    public void add(Object o) {
        Thread.sleep(10000);
        super.add(o);
    }
}

Я подозреваю, что в вашем случае ArrayList работает хорошо, потому что он увеличивает размер внутреннего буфера довольно агрессивно, поэтому не будет большого количества перераспределения. Когда буфер не нужно изменять размер, ArrayList будет иметь faster adds.

вы также должны быть очень осторожны, когда вы делаете такого рода профилирования. Я бы предложил вам изменить код профилирования, чтобы выполнить фазу прогрева (так что 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.

  • нужно повторить в цикле несколько раз, чтобы разогреть jvm
  • нужно сделать что-то в вашем итеративном цикле или его можно оптимизировать array
  • ArrayList изменяет размер, что дорого. Если бы вы построили ArrayList as new ArrayList(500000) вы построили бы одним ударом, и тогда все распределения были бы довольно дешевыми (один предварительный резервированный массив)
  • вы не указываете свою память JVM - она должна быть запущена с -xMs = = - Xmx (все предварительно выделено) и достаточно высоко, что GC, вероятно, не будет запущен
  • этот тест не охватывает самый неприятный аспект LinkedList-random access. (итератор не обязательно одно и то же). Если вы кормите скажем 10% от размера большой коллекции как случайный выбор list.get вы найдете linkedlists ужасны для захвата чего-либо, кроме первого или последнего элемента.

для arraylist: JDK get является чего и следовало ожидать:

public E get(int index) {
    RangeCheck(index);

    return elementData[index];
}

(в основном просто верните элемент индексированного массива.,

для linkedlist:

public E get(int index) {
    return entry(index).element;
}

похож? Не совсем. entry-это метод, а не примитивный массив, и посмотрите, что он должен делать:

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


чтобы понять, почему полученные вами результаты не противоречат характеристике "big O". Мы должны вернуться к первым принципам, то есть определение.

пусть f(x) и g (x) - две функции, определенные на некотором подмножестве вещественных чисел. Один пишет

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), означает, что стоимость времени add1 операция над списком размера N стремится к константе cдобавьте 1 как N стремится к бесконечности.

и заявление add2 is O(1) amortized over N operations означает, что в среднем стоимость одной последовательности из N add2 операции имеют тенденцию к постоянному Cadd2 как N стремится к бесконечности.

что не говорит, что эти константы Cдобавьте 1 и Cadd2 есть. На самом деле причина того, что LinkedList медленнее, чем ArrayList в вашем бенчмарке, заключается в том, что Cдобавьте 1 больше, чем Cadd2.

урок в том, что большой O нотация не предсказывает абсолютную или даже относительную производительность. Все, что он предсказывает, это формы функции производительности, поскольку управляющая переменная становится очень большой. Это полезно знать, но это не говорит вам всего, что вам нужно знать.


в Big-O-нотации речь идет не об абсолютных таймингах, а об относительных таймингах, и вы не можете сравнивать числа одного алгоритма с другим.

вы получаете только информацию о том, как тот же алгоритм реагирует на увеличение или уменьшение числа кортежей.

один алгоритм может занять час для одной операции и 2h для двух операций и равен O(n), а другой-O(n), и занимает одну миллисекунду для одной операции и две миллисекунды для двух операций.

еще одна проблема, если измерение с помощью JVM-это оптимизация hotspot-компилятора. Цикл "ничего не делать" может быть устранен JIT-компилятором.

третья вещь, которую нужно рассмотреть, - это ОС и JVM, используя кэши и запустив сборку мусора.


трудно найти хороший вариант использования для LinkedList. Если вам нужно только использовать интерфейс Dequeu, вы, вероятно, должны использовать ArrayDeque. Если вам действительно нужно использовать интерфейс List, вы часто услышите предложение использовать always ArrayList, потому что LinkedList ведет себя очень плохо при доступе к случайному элементу.

к сожалению, также ArrayList имеет свои проблемы с производительностью, если элементы в начале или в середине списка должны быть удалены или вставленный.

однако есть новая реализация списка, называемая GapList, которая сочетает в себе сильные стороны как ArrayList, так и LinkedList. Он был разработан как замена выпадающего списка для ArrayList и LinkedList и поэтому реализует как список интерфейсов, так и Deque. Также реализованы все общедоступные методы, предоставляемые ArrayList (ensureCapacty, trimToSize).

реализация GapList гарантирует эффективный случайный доступ к элементам по индексу (как это делает ArrayList) и в то же время эффективное добавление и удаление элементов В и из головы и хвоста списка (как это делает LinkedList).

вы найдете более подробную информацию о GapList в http://java.dzone.com/articles/gaplist-%E2%80%93-lightning-fast-list.


1) Базовая Структура Данных Первое различие между ArrayList и LinkedList заключается в том, что ArrayList поддерживается Array, а LinkedList-LinkedList. Это приведет к дальнейшим различиям в показателях.

2) LinkedList реализует Deque Другое различие между ArrayList и LinkedList заключается в том, что помимо интерфейса List LinkedList также реализует интерфейс Deque, который предоставляет first in first out операции для add() и poll () и нескольких других функций Deque. 3) добавление элементов в ArrayList Добавление элемента в ArrayList-это операция O(1), если он не вызывает изменения размера массива, в этом случае он становится O(log(n)), с другой стороны, добавление элемента в LinkedList-это операция O(1), поскольку она не требует навигации.

4) удаление элемента из позиции Чтобы удалить элемент из определенного индекса, например, путем вызова remove (index), ArrayList выполняет операцию копирования, которая делает его близким к O(n), в то время как LinkedList должен пройти к той точке, которая также делает его O(n/2), поскольку он может пройти с любого направления на основе близости.

5) итерация по ArrayList или LinkedList Итерация-это операция O(n) для LinkedList и ArrayList, где n-номер элемента.

6) извлечение элемента из позиции Операция get (index) O(1) в ArrayList, а его O (n/2) в LinkedList, поскольку ему нужно пройти до этой записи. Хотя в большой o обозначение O(n/2) просто O (n), потому что мы игнорируем константы там.

7) памяти LinkedList использует объект-оболочку Entry, который является статическим вложенным классом для хранения данных и двух узлов next и previous, а ArrayList просто хранит данные в массиве.

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

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

из всех вышеперечисленных различий между ArrayList vs LinkedList, похоже, что ArrayList является лучшим выбором, чем LinkedList почти во всех случаях, за исключением случаев, когда вы делаете частую операцию add (), чем remove () или get().

легче измените связанный список, чем ArrayList, особенно если вы добавляете или удаляете элементы из начала или конца, потому что связанный список внутренне сохраняет ссылки на эти позиции, и они доступны в O(1) Время.

другими словами, вам не нужно пересекать связанный список, чтобы достичь позиции, в которой вы хотите добавить элементы, в этом случае добавление становится операцией O(n). Например, вставка или удаление элемента в середину списка.

в моем мнение, используйте ArrayList над LinkedList для большинства практических целей в Java.


o notation analysis предоставляет важную информацию, но имеет свои ограничения. По определению o notation analysis считает, что каждая операция занимает приблизительно одно и то же время для выполнения, что неверно. Как отметил @seand, связанные списки внутренне используют более сложную логику для вставки и извлечения элементов (взгляните на исходный код, вы можете нажать ctrl+click в своей IDE). ArrayList внутренне должен только вставлять элементы в массив и время от времени увеличивать его размер (который даже будучи операцией o(n), на практике может быть выполнено довольно быстро).

Ура


вы можете отделить добавить или удалить как двухэтапную операцию.

LinkedList: если вы добавляете элемент в индекс n, вы можете переместить указатель с 0 на n-1, затем вы можете выполнить так называемую операцию O(1) add. Операция удаления такая же.


ArraryList: ArrayList реализует интерфейс RandomAccess, что означает, что он может получить доступ к элементу в O(1).
Если вы добавляете элемент в индекс N, то можно перейти к индексу n-1 в O (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));

}