Вопрос (ы) о временной сложности "изменения размера" массива в Java - PullRequest
1 голос
/ 15 апреля 2020

ПРИМЕЧАНИЕ: Поскольку заголовок уже намекает, этот вопрос не о заданной c java.util.ArrayList реализации списка на основе массива, а о необработанных массивах самих себя и как они могут вести себя в «чистой» (то есть полностью неоптимизированной) реализации списка на основе массива. Я решил упомянуть java.util.ArrayList, потому что это наиболее яркий пример списка на основе массива в Java, хотя он технически не "чист", поскольку использует предварительное распределение для сокращения времени работы add(). Если вы хотите знать, почему я задаю этот конкретный вопрос c, не заинтересовавшись оптимизацией предварительного распределения java.util.ArrayList(), я добавил небольшое объяснение моего варианта использования ниже.

Общеизвестно, что вы можете получить доступ к элементам в списках на основе массива (например, Java ArrayList<E>) с временной сложностью O(1), а для добавления элементов в этот список потребуется O(n). Со связанными списками все наоборот (для дважды связанного списка вы могли бы оптимизировать доступ к половине времени выполнения).

Причина, по которой добавление элементов в список на основе массива занимает O(n) в том, что массив не может быть просто изменен, но должен быть перераспределен и перезаполнен. Самый простой способ сделать это:

String arr[] = new String[n];
//...
String newElem = "foo";
String[] newArr = new String[n + 1];
int i = 0;
for (String elem : arr) {
    newArr[i] = arr[i++];
}
newArr[i] = newElem;
arr = newArr;

Сложность времени O(n) хорошо видна благодаря функции для l oop. Но есть и другие способы скопировать массивы в Java, например, System.arraycopy().

Придерживаться ванили для решения l oop, даже сжатие массив займет O(n), поскольку массив имеет фиксированный размер и для его «сжатия» необходимо скопировать все сохраняемые элементы в новый меньший массив.

Итак, вот мои вопросы, касающиеся таких операции с массивами и их временная сложность:

  1. Хотя ваниль для l oop всегда будет принимать O(n), возможно ли, что System.arraycopy() оптимизирует операцию добавления, если есть Достаточно места в памяти, чтобы расширить массив на месте, а это означает, что он оставит исходный массив на своем месте и просто добавит новый элемент в конце его?

  2. Как сокращение операция всегда может быть выполнена теоретически с O(1), всегда ли System.arraycopy() оптимизирует эту операцию до O(1)?

  3. Если System.arraycopy() равно не способен использовать эти оптимизации, есть ли ее путь в Java для фактического использования тех оптимизаций, которые возможны в теории ИЛИ будет массив "изменение размера" всегда займет O(n), независимо от того, при каких обстоятельствах?

TL; DR: есть ли ситуация, в которой "изменение размера" массива в Java займет менее O(n)?

Дополнительная информация:

Я использую openJDK11 ( самый новый выпуск), но если ответ окажется зависимым от JVM, я хотел бы знать, как другие JVM будут вести себя в сравнении.

Для любопытных

, которые хотят знать что я хочу сделать с этой информацией:

Я работаю над новой реализацией java.util.List, а именно гибридным списком, который может хранить данные в массиве и в связанном буфере. В некоторых случаях буфер будет сброшен в массив, что, конечно, требует изменения размера существующего массива. Но помимо этой идеи, я хочу использовать как можно больше других оптимизаций в части массива. Чтобы вообще избежать изменения размера массива, я экспериментировал с идеей сохранения массива в постоянном размере, но управления его «допустимым» диапазоном с некоторыми другими полями. Это означает, что если вы вытолкнете последний элемент массива, это уменьшит не массив, а диапазон допустимых элементов. Затем, при вставке новых элементов в часть массива, прежний недопустимый раздел может использоваться для сдвига значений, в основном повторно используя пространство, которое ранее использовалось теперь удаленным элементом. Если операции вставки превышают фактический размер массива, элементы все еще могут быть переданы в связанный буфер, чтобы избежать изменения размера. Для дальнейшей оптимизации я выбрал использование середины массива в качестве оси при удалении определенных элементов. Теперь допустимый диапазон может не начинаться с начала массива. По сути, это означает, что если вы удаляете элемент слева от оси, все элементы между началом допустимого диапазона и удаленным элементом смещаются в направлении оси, вправо. Удаление элемента справа от оси работает соответственно. Таким образом, после некоторого удаления массив может выглядеть следующим образом:

[null null|elem0 elem1 elem2||elem3 elem4 elem5|null null null]

(где | в начале и в конце помечают допустимый диапазон, а || обозначает ось)

Итак, как все это связано с моим вопросом?

Все эти оптимизации основаны на утверждении, что изменение размера массива дорого по времени, а именно O(n). Поэтому изменение размера массива по возможности избегается. Эти оптимизации могут показаться изящными, но код, реализующий их, может стать довольно грязным, , особенно , при реализации пакетных операций (addAll(), removeAll(), retainAll() ...). Итак, если окажется, что сама операция изменения размера массива в некоторых случаях может быть менее дорогой (особенно сокращение), я бы исключил многие из тех оптимизаций, которые затем оказываются бесполезными, делая код lot проще в этом процессе.

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

...