Вставка в середину: ArrayList против LinkedList

Как-то на собеседовании мне задали вопрос: какая реализация списка выполнит вставку в середину быстрее: ArrayList или LinkedList? С первого взгляда вопрос простой — нужно посчитать алгоритмическую сложность каждого варианта и сравнить их. Вставку в середину можно разделить на две операции: поиск середины списка и саму вставку. Для ArrayList поиск элемента по индексу не составляет труда, так как элементы списка находятся в массиве. Алгоритмическая сложность составляет O (1). В случае LinkedList придётся последовательно перебрать все элементы, пока не доберёмся до нужного элемента. Сложность будет O (n). Вставка в ArrayList связана со сдвигом всех элементов, находящихся после точки вставки, поэтому алгоритмическая сложность этой операции O (n). В LinkedList вставка заключается в создании нового связующего объекта и установки ссылок на него у соседних элементов. Сложность O (1). В сумме сложность вставки в середину у ArrayList и у LinkedList получается одинаковая — O (n). Эти рассуждения я показал интервьюеру, на что он мне задал вопрос: «Так что всё-таки быстрее: пробежаться по элементам или сместить элементы?». Я быстро прикинув, что операция чтения по сути быстрее операции записи, сказал, что LinkedList справится быстрее.
Когда я пришёл домой, я задумался над этим вопросом. Поискал решение этой задачи на форумах. Кто-то был согласен со мной, но многие учли, что операция смещения производится native методом, который работает быстрее, поэтому ArrayList выполнит вставку в середину быстрее. Не получив окончательного и неопровержимого ответа, я решил провести собственное исследование.

Сперва я углубился в изучение исходного кода этих классов. Вставка элемента в ArrayList проходит так: сначала, при необходимости, массив увеличивается, затем все элементы, стоящие после места вставки сдвигаются на число позиций, равное количеству вставляемых элементов (я рассматриваю один элемент), и в конце встаёт на свое место вставляемый элемент. Массив увеличивается со скоростью n*1,5, где n — размер массива, а минимальное увеличение при стандартных условиях (capacity=10) — 5 элементов. В связи с этим, операцией по увеличению массива при расчёте алгоритмической сложности вставки можно пренебречь. Сдвиг элементов, находящихся после точки вставки обладает алгоритмической сложностью O (n). Таким образом общая сложность всё равно остаётся O (n). Да, мы держим в уме, что операция увеличения массива незначительно повышает сложность, но нативность действий с массивом увеличивает скорость работы.

Поиск элемента в LinkedList начинается в цикле от края списка. Если искомый элемент находится в первой половине списка, то поиск идёт с начала, в обратном случае — с конца. Так как мы вставляем именно в середину, то в цикле пройдём ровно половину элементов. Сложность O (n). Сама вставка, как я уже писал выше, заключается в создании объекта и указании ссылок на него. Сложность O (1). Опять же ничего нового я не выяснил: общая сложность осталась O (n), при этом держим в уме, что создание объекта — «дорогая» операция.

Анализ исходного кода ситуацию не разъяснил, поэтому стал писать тесты. Я решил исследовать зависимость от двух параметров: начальный размер списка и количество вставок подряд (количество итераций).

Пример исходного кода
package com.jonasasx.liststest;

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

public class Main {

        private static final int        MAX_SIZE                = 1000;
        private static final int        MAX_ITERATIONS  = 10000;
        private static final float      STEP_SIZE               = 2f;
        private static final float      STEP_ITERATIONS = 5;
        private static final int        TESTS_COUNT             = 100;

        public static void main(String[] args) throws InterruptedException {
                ArrayList arrayList;
                LinkedList linkedList;
                for (float size = 1; size < MAX_SIZE; size *= STEP_SIZE) {
                        for (float iterations = 1; iterations < MAX_ITERATIONS; iterations *= STEP_ITERATIONS) {
                                double sum = 0;
                                for (int k = 0; k < TESTS_COUNT; k++) {
                                        arrayList = new ArrayList<>();
                                        linkedList = new LinkedList<>();
                                        fillList(arrayList, (int) size);
                                        fillList(linkedList, (int) size);
                                        sum += Math.log10(calculateRatio(arrayList, linkedList, (int) iterations));
                                }
                                double logRatio = sum / TESTS_COUNT;
                                System.out.println(String.format("%07d\t%07d\t%07.2f\t%s", (int) size, (int) iterations, logRatio, logRatio > 0 ? "Linked" : "Array"));
                        }
                        System.out.println();
                }
        }

        static void fillList(List list, int size) {
                for (int i = 0; i < size; i++) {
                        list.add("0");
                }
        }

        static double calculateRatio(ArrayList arrayList, LinkedList linkedList, int iterations) {
                long l1 = calculateAL(arrayList, iterations);
                long l2 = calculateLL(linkedList, iterations);
                if (l1 == 0 || l2 == 0)
                        throw new java.lang.IllegalStateException();
                return (double) l1 / (double) l2;
        }

        static long calculateAL(ArrayList list, int m) {
                long t = System.nanoTime();
                for (int i = 0; i < m; i++) {
                        list.add(list.size() / 2, "0");
                }
                return System.nanoTime() - t;
        }

        static long calculateLL(LinkedList list, int m) {
                long t = System.nanoTime();
                for (int i = 0; i < m; i++) {
                        list.add(list.size() / 2, "0");
                }
                return System.nanoTime() - t;
        }
}



Для каждого размера списка и количества итераций создаются по одному массиву ArrayList и LinkedList, они заполняются одинаковыми объектами, после чего под замер скорости производится n вставок одного объекта в середину. В качестве сравниваемой величины я использую десятичный логарифм от отношения времён выполнения ArrayList к LinkedList. Когда это значение меньше нуля, ArrayList справляется быстрее, когда больше — быстрее LinkedList.

Привожу результаты теста в таблице:

  Итераций                    
Размер   1 2 4 8 16 32 64 128 256 512
  1 -0,12 0,01 -0,04 0,01 0,03 -0,04 -0,09 -0,19 -0,21 -0,31
  5 -0,14 0,02 -0,02 0,07 -0,08 0 -0,15 -0,31 -0,42 -0,52
  25 -0,12 0,2 0,19 0,13 0,07 0,04 -0,1 -0,29 -0,47 -0,56
  125 -0,31 -0,01 0,01 0 -0,03 -0,11 -0,17 -0,35 -0,48 -0,57
  625 -0,47 -0,49 -0,48 -0,45 -0,49 -0,51 -0,53 -0,6 -0,67 -0,78

И в графике:

image
На оси X указан изначальный размер списка, линии представляют разные количества итераций, а на оси Y отношение скорости работы двух реализаций списка.

Так как с каждой итерацией размер списка увеличивается, можно сделать общий вывод: LinkList работает быстрее при небольшом размере списка. Но самое главное: без чёткой конкретизации условий сравнивать скорость работы этих двух алгоритмов нельзя!

Чтобы увеличить точность, я отказался от параметра количества вставок и уменьшил шаг размера до одного. Для каждого из размеров я провёл тест тысячу раз и взял среднее значение. Получил график:
image
На графике ярко выражены всплески. Они находятся точно на тех местах, где ArrayList производит увеличение массива. Следовательно, можно сказать, что пренебрегать этой операцией нельзя, как я посчитал в начале, анализируя исходный код.

Общий вывод можно сделать такой: операция вставки в середину происходит в основном быстрее в ArrayList, но не всегда. С теоретической точки зрения нельзя однозначно сравнить скорость этих двух алгоритмов без учёта изначального размера списка.

Спасибо за внимание, надеюсь, кому-то моя работа покажется интересной и/или полезной.

© Habrahabr.ru