Производительность LinkedList vs ArrayList при сохранении упорядоченного списка

Я хочу сохранить упорядоченный List<Integer> размера <= 10 ^ 6. Каждый раз, когда будет добавлен новый элемент, я вызову метод Collections.sort() для сортировки нового элемента в списке. По моим знаниям ArrayList лучше, чем LinkedList. Но так как я часто вызываю метод sort(), я понял, что LinkedList будет работать лучше при сортировке списка и будет лучшим выбором по сравнению с ArrayList, так как нет смещения элементов, как в случае ArrayList (использует array в качестве базовой структуры данных). Любые предложения, которые будут более эффективными.

Ответы

Ответ 1

Вы можете использовать Collections#binarySearch в отсортированном списке, чтобы найти правильную точку вставки. ArrayList, вероятно, будет работать лучше, чем LinkedList, особенно для больших размеров, но это легко проверить.

Я запускал микро-тест различных методов: используя сортировку после каждой вставки или бинарный поиск для вставки в нужном месте, как с ArrayList (AL), так и с LinkedList (LL). Я также добавил Commons TreeList и guava TreeMultiset.

Выводы

  • лучший из тех, кто тестировал, использует TreeMultiset, но он не является списком строго говоря. Следующим лучшим вариантом является использование ArrayList + binarySearch
  • ArrayList работает лучше, чем LinkedList во всех ситуациях, и последний занял несколько минут, чтобы заполнить 100 000 элементов (ArrayList занял менее одной секунды).

Код лучшего исполнителя, для справки:

@Benchmark public ArrayList<Integer> binarySearchAL() {
  ArrayList<Integer> list = new ArrayList<> ();

  Random r = new Random();
  for (int i = 0; i < n; i++) {
    int num = r.nextInt();
    int index = Collections.binarySearch(list, num);
    if (index >= 0) list.add(index, num);
    else list.add(-index - 1, num);
    current = list.get(0); //O(1), to make sure the sort is not optimised away
  }
  return list;
}

Полный код на bitbucket.

Полные результаты

В столбце "Бенчмарк" содержится имя тестируемого метода (baseLine просто заполняет список без его сортировки, другие методы имеют явные имена: AL = ArrayList, LL = LinkedList, TL = Commons TreeList, treeMultiSet = guava), (n) - размер списка, Score - время, затраченное миллисекундами.

Benchmark                            (n)  Mode  Samples     Score     Error  Units
c.a.p.SO28164665.baseLine            100  avgt       10     0.002 ±   0.000  ms/op
c.a.p.SO28164665.baseLine           1000  avgt       10     0.017 ±   0.001  ms/op
c.a.p.SO28164665.baseLine           5000  avgt       10     0.086 ±   0.002  ms/op
c.a.p.SO28164665.baseLine          10000  avgt       10     0.175 ±   0.007  ms/op
c.a.p.SO28164665.binarySearchAL      100  avgt       10     0.014 ±   0.001  ms/op
c.a.p.SO28164665.binarySearchAL     1000  avgt       10     0.226 ±   0.006  ms/op
c.a.p.SO28164665.binarySearchAL     5000  avgt       10     2.413 ±   0.125  ms/op
c.a.p.SO28164665.binarySearchAL    10000  avgt       10     8.478 ±   0.523  ms/op
c.a.p.SO28164665.binarySearchLL      100  avgt       10     0.031 ±   0.000  ms/op
c.a.p.SO28164665.binarySearchLL     1000  avgt       10     3.876 ±   0.100  ms/op
c.a.p.SO28164665.binarySearchLL     5000  avgt       10   263.717 ±   6.852  ms/op
c.a.p.SO28164665.binarySearchLL    10000  avgt       10   843.436 ±  33.265  ms/op
c.a.p.SO28164665.sortAL              100  avgt       10     0.051 ±   0.002  ms/op
c.a.p.SO28164665.sortAL             1000  avgt       10     3.381 ±   0.189  ms/op
c.a.p.SO28164665.sortAL             5000  avgt       10   118.882 ±  22.030  ms/op
c.a.p.SO28164665.sortAL            10000  avgt       10   511.668 ± 171.453  ms/op
c.a.p.SO28164665.sortLL              100  avgt       10     0.082 ±   0.002  ms/op
c.a.p.SO28164665.sortLL             1000  avgt       10    13.045 ±   0.460  ms/op
c.a.p.SO28164665.sortLL             5000  avgt       10   642.593 ± 188.044  ms/op
c.a.p.SO28164665.sortLL            10000  avgt       10  1182.698 ± 159.468  ms/op
c.a.p.SO28164665.binarySearchTL      100  avgt       10    0.056 ±  0.002  ms/op
c.a.p.SO28164665.binarySearchTL     1000  avgt       10    1.083 ±  0.052  ms/op
c.a.p.SO28164665.binarySearchTL     5000  avgt       10    8.246 ±  0.329  ms/op
c.a.p.SO28164665.binarySearchTL    10000  avgt       10  735.192 ± 56.071  ms/op
c.a.p.SO28164665.treeMultiSet        100  avgt       10    0.021 ±  0.001  ms/op
c.a.p.SO28164665.treeMultiSet       1000  avgt       10    0.288 ±  0.008  ms/op
c.a.p.SO28164665.treeMultiSet       5000  avgt       10    1.809 ±  0.061  ms/op
c.a.p.SO28164665.treeMultiSet      10000  avgt       10    4.283 ±  0.214  ms/op

За 100 тыс. элементов:

c.a.p.SO28164665.binarySearchAL    100000  avgt        6  890.585 ± 68.730  ms/op
c.a.p.SO28164665.treeMultiSet      100000  avgt        6  105.273 ±  9.309  ms/op

Ответ 2

Поскольку java не имеет встроенного мультимножества, что является идеальной структурой данных для вашей ситуации, я предлагаю использовать TreeMultiset найденный в библиотеке guava.

Multisets позволяют дублировать элементы, а мультимножество дерева также добавит преимущества сохранения сортировки вашей коллекции.

Ответ 3

Вызов sort() в LinkedList является разрушительным для производительности, из-за реализации List.sort() по умолчанию преобразования List в массив для сортировки. Очень мало случаев, когда имеет смысл использовать LinkedList, хотя может показаться, что он должен быть эффективным.

Если вы хотите, чтобы коллекция всегда сортировалась, вы действительно должны использовать упорядоченную коллекцию, например, TreeSet или, возможно, даже PriorityQueue. Он будет обеспечивать более чистый код (а также более быструю сортировку), так как вам не нужно беспокоиться о вызове sort() самостоятельно все время.

Ответ 4

В Oracle Java/OpenJDK 7 или выше асимптотическая производительность обоих будет одинаковой. Collections.sort загружает список в массив, сортирует массив и загружает массив обратно в список, итерации через него (используя ListIterator), заменяя его элементы.

В обоих случаях это сортировка массива в основном отсортированном массиве (который является O(n) в OpenJDK 7 и выше, поскольку он использует timsort), плюс две итерации списка (которые в O(n) в обоих случаях - хотя я бы ожидал, что LinkedList будет иметь худший постоянный термин). В общем, это процесс O(n), но, скорее всего, медленнее для LinkedList.

Если вы являетесь объемными элементами вставки, объемная вставка будет O(n^2) в целом, что медленнее, чем вставлять их все и сортировать, или после предложения Smac89 использования TreeMultiset (оба будут O(n log(n))).

И просто для удовольствия, здесь действительно ужасный способ злоупотребления TreeSet, чтобы он мог хранить повторяющиеся элементы:

public class AwfulComparator<E extends Comparable<E>> implements Comparator<E> {
    public int compare(E o1, E o2) {
        int compared = o1.compareTo(o2);
        return (compared == 0)?1:compared; // Never compare equal
    }
}

new TreeSet<String>(new AwfulComparator<>());

Ответ 5

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

Используя обычные базовые классы Java, вы можете использовать любой из них:

PriorityQueue (in case you want to retain duplicates)
TreeSet (filter duplicates)

В любом случае будет проще всего прототипировать все версии и запустить некоторые тесты + профилирование.