Мне любопытно, может ли O (n log n) лучше всего связать список.
Какой самый быстрый алгоритм для сортировки связанного списка?
Ответ 1
Разумно ожидать, что вы не сможете сделать лучше, чем O (N log N) во время работы.
Однако интересная часть состоит в том, чтобы выяснить, можете ли вы ее сортировать in-place, стабильно, его худшее поведение и т.д.
Саймон Татхэм, известность Putty, объясняет, как сортировать связанный список с сортировкой слияния. Он заключает следующие замечания:
Как и любой алгоритм уважающего себя типа, это имеет время работы O (N log N). Поскольку это Mergesort, наихудшее время работы по-прежнему равно O (N log N); нет патологических случаев.
Требование дополнительного хранилища является небольшим и постоянным (т.е. несколькими переменными в рамках процедуры сортировки). Благодаря по-разному поведению связанных списков из массивов, эта реализация Mergesort исключает дополнительную стоимость хранения O (N), обычно связанную с алгоритмом.
Существует также пример реализации в C, который работает как для одиночных, так и для двусвязных списков.
Как упоминает ниже @Jørgen Fogh, нотация Big-O может скрыть некоторые постоянные факторы, которые могут привести к тому, что один алгоритм будет работать лучше из-за локальности памяти из-за небольшого количества элементов и т.д.
Ответ 2
В зависимости от ряда факторов, возможно, быстрее скопировать список в массив, а затем использовать Quicksort.
Причина, по которой это может быть быстрее, заключается в том, что массив намного лучше кэш-памяти, чем связанный список. Если узлы в списке распределены в памяти, вы может генерировать пропуски кэша повсюду. Опять же, если массив большой, вы все равно получите пропуски кэша.
Mergesort лучше параллелизуется, поэтому это может быть лучший выбор, если это то, что вы хотите. Это также намного быстрее, если вы выполняете его непосредственно в связанном списке.
Поскольку оба алгоритма выполняются в O (n * log n), принятие обоснованного решения предполагает их профилирование как на машине, на которой вы хотели бы запустить их.
--- EDIT
Я решил проверить свою гипотезу и написал C-программу, которая измерила время (используя clock()
), которое было принято для сортировки связанного списка int. Я попытался со связанным списком, где каждый node был назначен malloc()
и связанный список, где узлы были выложены линейно в массиве, поэтому производительность кэша была бы лучше. Я сравнивал их со встроенным qsort, который включал копирование всего из фрагментированного списка в массив и копирование результата обратно. Каждый алгоритм выполнялся на тех же 10 наборах данных, и результаты были усреднены.
Вот результаты:
N = 1000:
Фрагментированный список с сортировкой слиянием: 0.000000 секунд
Массив с qsort: 0.000000 секунд
Упакованный список с сортировкой слиянием: 0.000000 секунд
N = 100000:
Фрагментированный список с сортировкой слияния: 0.039000 секунд
Массив с qsort: 0.025000 секунд
Упакованный список с сортировкой слиянием: 0.009000 секунд
N = 1000000:
Фрагментированный список с сортировкой слиянием: 1.162000 секунд
Массив с qsort: 0.420000 секунд
Упакованный список с сортировкой слияния: 0.112000 секунд
N = 100000000:
Фрагментированный список с сортировкой слияния: 364.797000 секунд
Массив с qsort: 61.166000 секунд
Упакованный список с сортировкой слияния: 16.525000 секунд
Вывод:
По крайней мере, на моей машине копирование в массив стоит того, чтобы улучшить производительность кеша, поскольку в реальной жизни у вас редко есть полностью упакованный список ссылок. Следует отметить, что моя машина имеет 2,8 ГГц Phenom II, но только 0,6 ГГц RAM, поэтому кеш очень важен.
Ответ 3
Сравнение сортировки (т.е. на основе сравнения элементов) не может быть быстрее, чем n log n
. Неважно, что такое базовая структура данных. См. Wikipedia.
Другие виды сортировки, которые используют множество идентичных элементов в списке (например, сортировка подсчета) или некоторые ожидаемые распределения элементов в списке, быстрее, хотя я не могу придумать особенно хорошо работают в связанном списке.
Ответ 4
Как указано много раз, нижняя граница сортировки на основе сравнения для общих данных будет O (n log n). Для кратковременного повторения этих аргументов существует n! различные способы сортировки списка. Любое дерево сравнения, которое имеет n! (который находится в O (n ^ n)) для возможных окончательных сортировок потребуется по крайней мере log (n!) как его высота: это дает вам нижнюю границу O (log (n ^ n)), которая равна O (n log n).
Таким образом, для общих данных в связанном списке наилучшим видом сортировки, который будет работать с любыми данными, которые могут сравнивать два объекта, будет O (n log n). Однако, если у вас есть более ограниченная область работы, вы можете улучшить время, которое требуется (по крайней мере, пропорционально n). Например, если вы работаете с целыми числами не больше некоторого значения, вы можете использовать Counting Sort или Radix Sort, поскольку они используют конкретные объекты, которые вы сортируете, чтобы уменьшить сложность с долей n. Будьте осторожны, однако, они добавляют некоторые другие вещи к сложности, которую вы не можете учитывать (например, подсчет сортировки и сортировка Radix добавляют как факторы, основанные на размере сортируемых чисел, O (n + k), где k - размер наибольшего числа для Counting Sort, например).
Кроме того, если у вас есть объекты, которые имеют идеальный хеш (или, по крайней мере, хэш, который отображает все значения по-разному), вы можете попробовать использовать сортировку counting или radix для своих хеш-функций.
Ответ 5
Это хорошая небольшая статья на эту тему. Его эмпирический вывод заключается в том, что Treesort лучше всего, за ним следуют Quicksort и Mergesort. Сорт осадка, сортировка пузырьков, сортировка сортировки выполняются очень плохо.
СРАВНИТЕЛЬНОЕ ИССЛЕДОВАНИЕ АЛГОРИТМОВ СОРТИРОВАННОГО СПИСКА by Ching-Kuang Shene
http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.31.9981
Ответ 6
A Radix sort особенно подходит для связанного списка, так как легко сделать таблицу главных указателей, соответствующую каждому возможному значению цифра.
Ответ 7
Сортировка слияния не требует доступа O (1) и является O (n ln n). Никакие известные алгоритмы для сортировки общих данных лучше, чем O (n ln n).
Специальные алгоритмы данных, такие как сортировка по методу "радикс" (ограничение размера данных) или сортировка гистограммы (подсчет дискретных данных), могут сортировать связанный список с более низкой функцией роста, если вы используете другую структуру с доступом O (1) как временное хранение.
Другим классом специальных данных является сортировка сортированного списка, отсортированного по порядку. Это можно отсортировать в операциях O (kn).
Копирование списка в массив и обратно будет O (N), поэтому любой алгоритм сортировки можно использовать, если пространство не является проблемой.
Например, если связанный список содержит uint_8
, этот код будет сортировать его в O (N) раз, используя сортировку гистограммы:
#include <stdio.h>
#include <stdint.h>
#include <malloc.h>
typedef struct _list list_t;
struct _list {
uint8_t value;
list_t *next;
};
list_t* sort_list ( list_t* list )
{
list_t* heads[257] = {0};
list_t* tails[257] = {0};
// O(N) loop
for ( list_t* it = list; it != 0; it = it -> next ) {
list_t* next = it -> next;
if ( heads[ it -> value ] == 0 ) {
heads[ it -> value ] = it;
} else {
tails[ it -> value ] -> next = it;
}
tails[ it -> value ] = it;
}
list_t* result = 0;
// constant time loop
for ( size_t i = 255; i-- > 0; ) {
if ( tails[i] ) {
tails[i] -> next = result;
result = heads[i];
}
}
return result;
}
list_t* make_list ( char* string )
{
list_t head;
for ( list_t* it = &head; *string; it = it -> next, ++string ) {
it -> next = malloc ( sizeof ( list_t ) );
it -> next -> value = ( uint8_t ) * string;
it -> next -> next = 0;
}
return head.next;
}
void free_list ( list_t* list )
{
for ( list_t* it = list; it != 0; ) {
list_t* next = it -> next;
free ( it );
it = next;
}
}
void print_list ( list_t* list )
{
printf ( "[ " );
if ( list ) {
printf ( "%c", list -> value );
for ( list_t* it = list -> next; it != 0; it = it -> next )
printf ( ", %c", it -> value );
}
printf ( " ]\n" );
}
int main ( int nargs, char** args )
{
list_t* list = make_list ( nargs > 1 ? args[1] : "wibble" );
print_list ( list );
list_t* sorted = sort_list ( list );
print_list ( sorted );
free_list ( list );
}
Ответ 8
Не прямой ответ на ваш вопрос, но если вы используете Список пропусков, он уже отсортирован и имеет O (log N) время поиска.
Ответ 9
Mergesort - лучшее, что вы можете здесь сделать.
Ответ 10
Как я знаю, лучшим алгоритмом сортировки является O (n * log n), независимо от контейнера. Было доказано, что сортировка в широком смысле слова (стиль mergesort/quicksort и т.д.) не может опускаться ниже. Использование связанного списка не даст вам лучшего времени работы.
Единственный алгоритм, который работает в O (n), является алгоритмом "взлома", который полагается на подсчет значений, а не на фактическую сортировку.
Ответ 11
Здесь реализована реализация, которая проходит только один раз, собирая прогоны, а затем спланирует слияния так же, как это делает mergesort.
Сложность - это O (n log m), где n - количество элементов, а m - количество прогонов. Наилучшим случаем является O (n) (если данные уже отсортированы), а наихудший - O (n log n), как ожидалось.
Требуется временная память O (log m); сортировка выполняется на месте в списках.
(обновлено ниже. commenter one делает хороший момент, чтобы описать его здесь)
Суть алгоритма:
while list not empty
accumulate a run from the start of the list
merge the run with a stack of merges that simulate mergesort recursion
merge all remaining items on the stack
Накопительные прогоны не требуют много объяснений, но полезно воспользоваться возможностью для того, чтобы накапливать как восходящие прогоны, так и нисходящие прогоны (обратные). Здесь он добавляет элементы, меньшие, чем голова прогона, и добавляет элементы, которые больше или равны концу прогона. (Обратите внимание, что добавление должно использовать строго меньшее, чем для сохранения стабильности сортировки.)
Проще всего просто вставить код слияния здесь:
int i = 0;
for ( ; i < stack.size(); ++i) {
if (!stack[i])
break;
run = merge(run, stack[i], comp);
stack[i] = nullptr;
}
if (i < stack.size()) {
stack[i] = run;
} else {
stack.push_back(run);
}
Рассмотрим сортировку списка (d a g я b e c f j h) (игнорирование прогонов). Состояние стека выполняется следующим образом:
[ ]
[ (d) ]
[ () (a d) ]
[ (g), (a d) ]
[ () () (a d g i) ]
[ (b) () (a d g i) ]
[ () (b e) (a d g i) ]
[ (c) (b e) (a d g i ) ]
[ () () () (a b c d e f g i) ]
[ (j) () () (a b c d e f g i) ]
[ () (h j) () (a b c d e f g i) ]
Затем, наконец, слейте все эти списки.
Обратите внимание, что количество элементов (прогонов) в стеке [i] равно нулю или 2 ^ i, а размер стека ограничен 1 + log2 (nruns). Каждый элемент объединяется один раз на уровень стека, следовательно, O (n log m). Здесь наблюдается сходство с Timsort, хотя Timsort поддерживает свой стек, используя что-то вроде последовательности Фибоначчи, где это использует полномочия двух.
Накопительные прогоны используют любые уже отсортированные данные, поэтому наилучшая степень сложности - O (n) для уже отсортированного списка (один запуск). Поскольку мы накапливаем как восходящие, так и убывающие прогоны, прогоны всегда будут иметь длину не менее 2. (Это уменьшает максимальную глубину стека, по крайней мере, на одну, оплачивая затраты на поиск прогонов в первую очередь.) Худшая сложность случая O (n log n), как и ожидалось, для данных с высокой степенью рандомизации.
(Um... Второе обновление.)
Или просто посмотрите wikipedia на снизу вверх по объему.