Каков самый быстрый алгоритм сортировки связанного списка?

Мне любопытно, если O (N log n) - лучшее, что может сделать связанный список.

11 ответов


разумно ожидать, что вы не можете сделать лучше, чем O (N log N) в времени.

, самое интересное это исследовать, будет ли она на месте, стабилизированно, его наихудшее поведение и так далее.

Симона tatham, шпатлевки славы, объясняет, как сортировка связанного списка с помощью merge sort. Он завершает свое выступление следующими замечаниями:

Как и любой уважающий себя алгоритм сортировки, это имеет время работы O (N log N). Поскольку это Mergesort, в худшем случае время работы по-прежнему O(N log N); патологических случаев нет.

требование к вспомогательному хранению мало и постоянн (т. е. немного переменных внутри сортируя режим). Благодаря изначально различному поведению связанных списков из массивов эта реализация Mergesort позволяет избежать затрат на вспомогательное хранилище O( N), обычно связанных с алгоритм.

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

Как упоминает @Jørgen Fogh ниже, нотация big-O может скрывать некоторые постоянные факторы, которые могут заставить один алгоритм работать лучше из-за локальности памяти, из-за низкого количества элементов и т. д.


в зависимости от ряда факторов, на самом деле может быть быстрее скопировать список в массив, а затем использовать Quicksort.

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

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

поскольку оба алгоритма работают в O (n * log n), принятие обоснованного решения будет включать профилирование их обоих на машине, на которой вы хотите их запустить.

- - - EDIT

Я решил проверить свою гипотезу и написал C-программу, которая измеряла время (используя clock()) принято сортировать связанный список ints. Я пробовал со связанным списком, где каждый узел был выделен с 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 секунды!--5-->

массив с qsort: 0.420000 секунд

упакованный список с сортировкой слияния: 0.112000 секунд

N = 100000000:

фрагментированный список с сортировкой слияния: 364.797000 секунд

массив с qsort: 61.166000 секунд

упакованный список с сортировкой слияния: 16.525000 секунд

вывод:

по крайней мере на моей машине, копирование в массив дорогого стоит для повышения производительности кэша, так как вы редко имеете полностью упакованный связанный список в реальной жизни. Следует отметить, что моя машина имеет 2.8 GHz Phenom II, но только 0.6 GHz RAM, поэтому Кэш очень важен.


сортировка сравнения (т. е. на основе сравнения элементов) не может быть быстрее, чем n log n. Не имеет значения, что лежит в основе структуры данных. См.Википедия.

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


Как говорилось много раз, нижняя граница сортировки на основе сравнения для общих данных будет O(N log n). Кратко resummarize эти аргументы, есть N! различные способы сортировки списка. Любое дерево сравнения, которое имеет n! (который находится в O(n^n)) возможные окончательные сорта будут нуждаться по крайней мере log (n!) как его высота: это дает вам o(log(n^n)) нижняя граница, которая является O(N log n).

Итак, для общих данных в связанном списке, наилучшая возможная сортировка, которая будет работа над любыми данными, которые могут сравнивать два объекта, будет O (N log n). Однако, если у вас есть более ограниченная область вещей для работы, вы можете улучшить время, необходимое (по крайней мере пропорционально n). Например, если вы работаете с целыми числами не больше некоторого значения, вы можете использовать Подсчет Вроде или Radix Сортировать, поскольку они используют конкретные объекты, которые вы сортируете, чтобы уменьшить сложность с пропорциональностью n. Будьте осторожны, хотя, это добавляет некоторые другие вещи к сложности, которую вы можете не учитывать (например, подсчет сортировки и сортировка Radix добавляют факторы, основанные на размере сортируемых чисел, O(n+k), где k-размер наибольшего числа для подсчета сортировки, например).

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


Это хорошая небольшая статья на эту тему. Его эмпирический вывод заключается в том, что древесный корень лучше всего, а затем Quicksort и Mergesort. Сортировка осадка, сортировка пузырьков, сортировка выбора выполняют очень плохо.

СРАВНИТЕЛЬНОЕ ИССЛЕДОВАНИЕ АЛГОРИТМОВ СОРТИРОВКИ СВЯЗАННЫХ СПИСКОВ by Ching-Kuang Shene

http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.31.9981


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


сортировка слиянием не требует доступа O(1) и является O ( n ln n ). Нет известных алгоритмов сортировки общих данных лучше, чем O ( n ln n ).

специальные алгоритмы данных, такие как сортировка radix ( ограничивает размер данных ) или сортировка гистограммы ( подсчитывает дискретные данные), могут сортировать связанный список с более низкой функцией роста, если вы используете другую структуру с доступом O(1) в качестве временного хранилища.

другой класс специальных данных-это своего рода сравнение почти сортированный список с k элементами не в порядке. Это можно отсортировать в 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 );
}

Не прямой ответ на ваш вопрос, но если вы используете Пропустить, он уже отсортирован и имеет время поиска O(log N).


Как я знаю, лучший алгоритм сортировки-O (n*log n), независимо от контейнера - было доказано, что сортировка в широком смысле слова (стиль mergesort/quicksort и т. д.) не может быть ниже. Использование связанного списка не даст вам лучшего времени выполнения.

единственный алгоритм, который работает в O (n), - это алгоритм "взлома", который опирается на подсчет значений, а не на сортировку.


Mergesort-лучшее, что вы можете сделать здесь.


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

сложность-O (N log m), где n-количество элементов, а m-количество запусков. В лучшем случае-O(n) (если данные уже отсортированы), а в худшем-O(N log n), как и ожидалось.

требуется временная память O (log m); сортировка выполняется на месте в списках.

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

суть алгоритма:

    while list not empty
        accumulate a run from the start of the list
        merge the run with a stack of merges that simulate mergesort's recursion
    merge all remaining items on the stack

накопление пробегов не требует много объяснений, но хорошо воспользоваться возможностью аккумулировать как восходящие, так и нисходящие пробеги (обратные). Здесь он добавляет элементы, меньшие, чем глава выполнения, и добавляет элементы, большие или равные концу выполнения. (Обратите внимание, что добавление должно использовать strict less-than для сохранения сортировки стабильность.)

проще всего просто вставить код слияния здесь:

    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 i 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), как и ожидалось, для данных, которые сильно рандомизированы.

(Хм... Второе обновление.)

или просто посмотреть Википедию на снизу вверх mergesort.