Глава 5. Двусвязный список
В этой главе рассматривается предыдущая задача и вводится ещё одна реализация интерфейса List — двусвязный список.
5.1 Анализ результатов производительности
Во время предыдущей задачи мы использовали Profiler.java для измерения времени выполнения различных операций над ArrayList и LinkedList при разных масштабах задач. Мы построили график зависимости времени выполнения от масштаба задачи на логарифмической шкале и оценили наклон полученной кривой, который отражает основную экспоненту отношения между временем выполнения и масштабом задачи.
Например, когда мы добавляли элементы в конец ArrayList с помощью метода add, мы обнаружили, что общее время выполнения n добавлений пропорционально n. То есть оценка наклона близка к 1. Мы пришли к выводу, что выполнение n добавлений имеет сложность O(n), поэтому среднее время добавления одного элемента является постоянным, или O(1), что соответствует нашим ожиданиям на основе анализа алгоритма.
Эта задача требует от вас заполнить тело profileArrayListAddBeginning, которое проверяет производительность добавления нового элемента в начало ArrayList. Согласно нашему анализу, мы ожидаем, что каждое добавление будет линейным, поскольку оно должно сдвигать другие элементы вправо, поэтому мы ожидаем, что сложность n добавлений будет квадратичной.
Это решение можно найти в каталоге solution репозитория.
public static void profileArrayListAddBeginning() {
Timeable timeable = new Timeable() {
List<String> list;
public void setup(int n) {
list = new ArrayList<String>();
}
public void timeMe(int n) {
for (int i=0; i<n; i++) {
list.add(0, "a string");
}
}
};
int startN = 4000;
int endMillis = 10000;
runProfiler("ArrayList add beginning", timeable, startN, endMillis);
}
Этот метод практически идентичен profileArrayListAddEnd. Единственное отличие заключается в методе timeMe, который использует двухпараметрическую версию метода add для вставки нового элемента по индексу 0. Также мы увеличили endMillis, чтобы получить дополнительную точку данных.
Результаты времени (слева — масштаб задачи, справа — время в миллисекундах):
Масштаб задачи | Время (мс) |
---|---|
4000 | 14 |
8000 | 35 |
16000 | 150 |
32000 | 604 |
64000 | 2518 |
128000 | 11555 |
График 5.1 показывает зависимость времени выполнения от масштаба задачи.
График 5.1: Анализ результатов: зависимость времени выполнения добавления n элементов в начало ArrayList от масштаба задачи
Обратите внимание, что линия на графике не означает, что алгоритм является линейным. Напротив, если для любого показателя k время выполнения пропорционально n^k, мы ожидаем увидеть линию с наклоном k. В этом случае мы ожидаем, что общая сложность n добавлений пропорциональна n^2, поэтому мы ожидаем линию с наклоном 2. Фактически, оценка наклона составляет 1,992, что очень близко. Вероятно, только ложные данные могут быть настолько хорошими.
5.2 Анализ производительности методов LinkedList
Ранее в задаче вы также анализировали производительность добавления новых элементов в начало LinkedList. Согласно нашему анализу, мы ожидали, что каждый вызов add займёт некоторое время, так как в связанном списке нам не нужно перемещать существующие элементы; мы можем добавить новый узел в начало. Поэтому мы ожидали, что общая сложность n добавлений будет линейной.
Вот решение:
public static void profileLinkedListAddBeginning() {
Timeable timeable = new Timeable() {
List<String> list;
public void setup(int n) {
list = new LinkedList<String>();
}
public void timeMe(int n) {
for (int i=0; i<n; i++) {
list.add(0, "a string");
}
}
};
int startN = 128000;
int endMillis = 2000;
runProfiler("LinkedList add beginning", timeable, startN, endMillis);
}
Мы внесли лишь несколько изменений, заменив ArrayList на LinkedList и скорректировав startN и endMillis для получения хорошего диапазона данных. Результаты измерений более разбросаны, чем в предыдущей партии данных; результаты следующие:
Масштаб задачи | Время (мс) |
---|---|
128000 | 16 |
256000 | 19 |
512000 | 28 |
1024000 | 77 |
2048000 | 330 |
4096000 | 892 |
8192000 | 1047 |
16384000 | 4755 |
График 5.2 показывает эти результаты графически.
График 5.2: Анализ результатов: зависимость времени выполнения добавления n элементов в начало LinkedList от масштаба задачи
Линия не совсем прямая, и наклон не точно равен 1, а оценка наименьших квадратов составляет 1,23. Однако результаты показывают, что общая сложность n добавлений, по крайней мере, приблизительно равна O(n), поэтому каждое добавление занимает постоянное время.
5.3 Добавление в конец LinkedList
Добавление элементов в начало — это операция, для которой мы ожидаем, что LinkedList будет работать быстрее, чем ArrayList. Однако для добавления элементов в конец мы ожидаем, что LinkedList замедлится. В моей реализации мы должны пройти по всему списку, чтобы добавить элемент в конец, что является линейной операцией. Поэтому мы ожидаем квадратичную общую сложность для n добавлений.
Однако это не так. Вот код:
public static void profileLinkedListAddEnd() {
Timeable timeable = new Timeable() {
List<String> list;
public void setup(int n) {
list = new LinkedList<String>();
}
public void timeMe(int n) {
for (int i=0; i<n; i++) {
list.add("a string");
}
}
};
int startN = 64000;
int endMillis = 1000;
runProfiler("LinkedList add end", timeable, startN, endMillis);
}
Здесь приведены результаты:
Масштаб задачи | Время (мс) |
---|---|
64000 | 9 |
128000 | 9 |
256000 | 21 |
512000 | 24 |
1024000 | 78 |
2048000 | 235 |
4096000 | 851 |
8192000 | 950 |
16384000 | 6160 |
График 5.3 показывает эти результаты графически.
График 5.3: Анализ результатов: зависимость времени выполнения добавления n элементов в конец LinkedList от масштаба задачи
Опять же, измеренные значения сильно разбросаны, линия не полностью прямая, но оценка наклона составляет 1,19, что близко к добавлению элементов в начале, а не очень близко к 2, как ожидалось на основе анализа. На самом деле он близок к 1, что указывает на то, что добавление элементов в конце является постоянной операцией. Как это возможно?
5.4 Двусвязный список
Мой список реализован как MyLinkedList, используя односвязный список; то есть каждый элемент содержит ссылку на следующий элемент, и сам объект MyArrayList имеет ссылку на первый узел.
Однако, если вы прочитаете документацию LinkedList по адресу http://thinkdast.com/linked, там говорится:
Реализация интерфейсов List и Deque с использованием двусвязного списка. [...] Все операции могут выполняться аналогично двунаправленному списку, обходя список с того конца, который ближе к указанному индексу.
Если вы не знакомы с двусвязными списками, вы можете прочитать больше информации на http://thinkdast.com/doublelist, но вкратце:
Поэтому мы можем начать с любого конца списка и перемещаться по нему в любом направлении. Таким образом, мы можем добавлять и удалять элементы в начале и в конце списка за постоянное время!
В таблице ниже приведены ожидаемые характеристики производительности ArrayList, MyLinkedList (односвязный список) и LinkedList (двусвязный список):
MyArrayList |
MyLinkedList |
LinkedList |
|
---|---|---|---|
add (в конец) |
1 | n | 1 |
add (в начало) |
n | 1 | 1 |
add (обычное) |
n | n | n |
get /set
|
1 | n | n |
indexOf / lastIndexOf
|
n | n | n |
isEmpty /size
|
1 | 1 | 1 |
remove (из конца) |
1 | n | 1 |
remove (из начала) |
n | 1 | 1 |
remove (обычное) |
n | n | n |
Для вставки и удаления элементов в начале списка реализация двусвязного списка превосходит ArrayList. Для вставки и удаления в конце списка обе структуры работают одинаково хорошо. Поэтому единственное преимущество ArrayList — это операции get и set, которые в списках требуют линейного времени, даже для двусвязных списков.
Если вы знаете, что время выполнения вашего приложения зависит от времени, необходимого для операций get и set элементов, то ArrayList может быть лучшим выбором. Если время выполнения зависит от добавления и удаления элементов в начало или конец списка, то LinkedList может оказаться более подходящим.
Однако помните, что эти рекомендации основаны на предположении о большом масштабе задачи. Есть и другие факторы, которые следует учитывать:
В целом, анализ алгоритмов даёт некоторые указания по выбору структуры данных, но только если:
Как инженер-программист с долгим опытом работы, вам вряд ли придётся сталкиваться с такой ситуацией.
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )