Подтвердить что ты не робот

Как связанный список может достичь времени сортировки O (n log n)?

Мне любопытно, в первую очередь, почему std::list и std::forward_list включают функции сортировки в качестве функций-членов, в отличие от любого другого контейнера стандартной библиотеки. Но более интересным для меня является то, что CPPReference и CPlusPlus утверждают, что эта сортировка выполняется в O (n log n) времени.

Я даже не могу представить, как можно сортировать контейнер без произвольного доступа к элементам. Поэтому я собрал тест, используя forward_list, чтобы сделать его как можно более трудным.

#include <chrono>
#include <cstdint>
#include <deque>
#include <forward_list>
#include <iostream>
#include <random>

using std::endl;
using namespace std::chrono;

typedef nanoseconds::rep length_of_time;
constexpr int TEST_SIZE = 25000;


class Stopwatch
{
    public:
        void start_timing();
        void end_timing();
        length_of_time get_elapsed_time() const;
    private:
        time_point<high_resolution_clock> start;
        time_point<high_resolution_clock> end;
        length_of_time elapsed_time = 0;
};


void Stopwatch::start_timing()
{
    start = high_resolution_clock::now();
}


void Stopwatch::end_timing()
{
    end = high_resolution_clock::now();
    auto elapsed = end - start;
    auto elapsed_nanoseconds = duration_cast<nanoseconds>(elapsed);
    elapsed_time = elapsed_nanoseconds.count();
}


length_of_time Stopwatch::get_elapsed_time() const
{
    return elapsed_time;
}


std::mt19937_64 make_random_generator()
{
    using namespace std::chrono;
    auto random_generator = std::mt19937_64();
    auto current_time = high_resolution_clock::now();
    auto nanos = duration_cast<nanoseconds>(
            current_time.time_since_epoch()).count();
    random_generator.seed(nanos);
    return random_generator;
}


int main()
{
    Stopwatch timer;
    std::deque<length_of_time> times;
    auto generator = make_random_generator();
    for (int i = 1; i <= TEST_SIZE; i++) {
        std::forward_list<uint64_t> container;
        for (int j = 1; j <= i; j++) {
            container.push_front(generator());
        }
        timer.start_timing();
        container.sort();
        timer.end_timing();
        times.push_back(timer.get_elapsed_time());
        container.clear();
    }

    for (const auto& time: times) {
        std::cout << time << endl;
    }
}

Цифры этой программы выводят следующий график:

forward list sorting time

Что действительно похоже на рост O (n log n) (хотя шипы на каждой трети пути интересны). Как библиотека это делает? Возможно, скопируйте в контейнер, который поддерживает сортировку, сортировку и копирование?

4b9b3361

Ответ 1

Связанные списки могут быть отсортированы в O (n log n) с помощью Mergesort.

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

Тот факт, что для этого требуется специализированный алгоритм, специально настроенный для структуры списка, также является причиной того, что sort является функцией-членом списка, а не отдельной функцией.


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

Здесь пример операции слияния в С++:

struct Node {
    Node* next;
    int val;
};

Node* merge(Node* a, Node* b) {
    Node fake_head(nullptr, 0);

    Node* cur = &fake_head;
    while (a && b) {
        if (a->val < b->val) { cur->next = a; a = a->next; }
        else                 { cur->next = b; b = b->next; }
        cur = cur->next;
    }

    cur->next = a ? a : b;
    return fake_head.next;
}

Ответ 2

Пример кода сортировки слияния снизу вверх с использованием массива указателей на списки, где array [i] указывает на список размером 2 ^ я (кроме последнего указателя указывает на список неограниченного размера). Так стандартная библиотека шаблонов HP/Microsoft реализует std:: list:: sort.

#define NUMLISTS 32                     /* number of lists */

typedef struct NODE_{
struct NODE_ * next;
int data;                               /* could be any comparable type */
}NODE;

NODE * MergeLists(NODE *, NODE *);

NODE * SortList(NODE *pList)
{
NODE * aList[NUMLISTS];                 /* array of lists */
NODE * pNode;
NODE * pNext;
int i;
    if(pList == NULL)                   /* check for empty list */
        return NULL;
    for(i = 0; i < NUMLISTS; i++)       /* zero array */
        aList[i] = NULL;
    pNode = pList;                      /* merge nodes into aList[] */
    while(pNode != NULL){
        pNext = pNode->next;
        pNode->next = NULL;
        for(i = 0; (i < NUMLISTS) && (aList[i] != NULL); i++){
            pNode = MergeLists(aList[i], pNode);
            aList[i] = NULL;
        }
        if(i == NUMLISTS)
            i--;
        aList[i] = pNode;
        pNode = pNext;
    }
    pNode = NULL;                       /* merge array into one list */
    for(i = 0; i < NUMLISTS; i++)
        pNode = MergeLists(aList[i], pNode);
    return pNode;
}

NODE * MergeLists(NODE *pSrc1, NODE *pSrc2)
{
NODE *pDst = NULL;                      /* destination head ptr */
NODE **ppDst = &pDst;                   /* ptr to head or prev->next */
    while(1){
        if(pSrc1 == NULL){
            *ppDst = pSrc2;
            break;
        }
        if(pSrc2 == NULL){
            *ppDst = pSrc1;
            break;
        }
        if(pSrc2->data < pSrc1->data){  /* if src2 < src1 */
            *ppDst = pSrc2;
            pSrc2 = *(ppDst = &(pSrc2->next));
            continue;
        } else {                        /* src1 <= src2 */
            *ppDst = pSrc1;
            pSrc1 = *(ppDst = &(pSrc1->next));
            continue;
        }
    }
    return pDst;
}

Другой, но более медленный способ слияния сортировки списка аналогичен сортировке по ленте 4 (весь последовательный доступ). Исходный список разбит на два списка. Каждый список считается потоком прогонов, где начальный размер выполнения равен 1. В этом примере счетчики используются для отслеживания границ запуска, поэтому он немного сложнее и медленнее, чем массив методов указателей. Запуск из двух списков входных данных объединяется, чередуясь между двумя выходными списками. После каждого прохода слияния размер пробега удваивается, направление слияния изменяется, так что выходные списки становятся входными списками и наоборот. Сортировка выполняется, когда все прогоны заканчиваются только одним из выходных списков. Если стабильность не требуется, то границы выполнения могут быть определены как любой node, за которым следует не по порядку node, и это позволит использовать естественный порядок с исходным списком.

NODE * SortList(NODE * pList)
{
NODE *pSrc0;
NODE *pSrc1;
NODE *pDst0;
NODE *pDst1;
NODE **ppDst0;
NODE **ppDst1;
int cnt;

    if(pList == NULL)                   /* check for null ptr */
        return NULL;
    if(pList->next == NULL)             /* if only one node return it */
        return pList;
    pDst0 = NULL;                       /* split list */
    pDst1 = NULL;
    ppDst0 = &pDst0;
    ppDst1 = &pDst1;
    while(1){
        *ppDst0 = pList;
        pList = *(ppDst0 = &pList->next);
        if(pList == NULL)
            break;
        *ppDst1 = pList;
        pList = *(ppDst1 = &pList->next);
        if(pList == NULL)
            break;
    }
    *ppDst0 = NULL;
    *ppDst1 = NULL;
    cnt = 1;                            /* init run size */
    while(1){
        pSrc0 = pDst0;                  /* swap merge direction */
        pSrc1 = pDst1;
        pDst0 = NULL;
        pDst1 = NULL;
        ppDst0 = &pDst0;
        ppDst1 = &pDst1;
        while(1){                       /* merge a set of runs */
            if(MergeRuns(&ppDst0, &pSrc0, &pSrc1, cnt))
                break;
            if(MergeRuns(&ppDst1, &pSrc0, &pSrc1, cnt))
                break;
        }
        cnt <<= 1;                      /* bump run size */
        if(pDst1 == NULL)               /* break if done */
            break;
    }
    return pDst0;
}       

int MergeRuns(NODE ***pppDst, NODE **ppSrc0, NODE **ppSrc1, int cnt)
{
NODE **ppDst = *pppDst;
NODE *pSrc0  = *ppSrc0;
NODE *pSrc1  = *ppSrc1;
int cnt0, cnt1;

    cnt0 = cnt;
    cnt1 = cnt;
    if(pSrc0 == NULL){                      /* if end data src0 */
        *ppDst = NULL;
        *pppDst = ppDst;
        return(1);
    }
    if(pSrc1 == NULL){                      /* if end data src1 */
        do{                                 /*   copy rest of src0 */
            *ppDst = pSrc0;
            pSrc0 = *(ppDst = &(pSrc0->next));
        }while(pSrc0);
        *ppDst = NULL;
        *pppDst = ppDst;
        return(1);
    }
    while(1){
        if(pSrc1->data < pSrc0->data){      /* if src1 < src0 */
            *ppDst = pSrc1;                 /*  move src1 */
            pSrc1 = *(ppDst = &(pSrc1->next));
            if(pSrc1 != NULL && --cnt1)     /*  if not end run1, continue */
                continue;
            do{                             /*    copy run0 */
                *ppDst = pSrc0;
                pSrc0 = *(ppDst = &(pSrc0->next));
            }while(pSrc0 != NULL && --cnt0);
            break;
        } else {                            /* else src0 <= src1 */
            *ppDst = pSrc0;                 /*  move src0 */
            pSrc0 = *(ppDst = &(pSrc0->next));
            if(pSrc0 != NULL && --cnt0)     /*  if not end run0, continue */
                continue;
            do{                             /*    copy run1 */
                *ppDst = pSrc1;
                pSrc1 = *(ppDst = &(pSrc1->next));
            }while(pSrc1 != NULL && --cnt1);
            break;
        }
    }
    *ppSrc0 = pSrc0;                        /* update ptrs, return */
    *ppSrc1 = pSrc1;
    *ppDst  = NULL;
    *pppDst = ppDst;
    return(0);
}

Ответ 3

Mergesort - O (nlogn) для связанных списков. Я не знаю, что функция сортировки по умолчанию для С++, но я подозреваю ее mergesort.

Ответ 4

У меня здесь нет стандарта, но CPPReference утверждает, что сложность сортировки - это Nlog (N) сравнения. Это означает, что даже быстрая сортировка будет стандартной совместимой реализацией, так как это будет сравнение Nlog (N) (но не Nlog (N)).

Ответ 5

Вы начинаете с несортированного списка неизвестной длины. Скажем, что элементы пронумерованы 0, 1, 2, 3...

В первом проходе вы создали два связанных списка, каждый из которых состоит из пар чисел в отсортированном порядке. Список 0 начинается с элементов 0 и 1 в отсортированном порядке. Список 1 начинается с элементов 2 и 3 в отсортированном порядке. Элементы 4 и 5 добавляются в список 0 в отсортированном порядке, 6 и 7 добавляются в список 1 и т.д. Очевидно, следует проявлять осторожность, чтобы не перевыполнить конец первоначального списка.

Во втором проходе вы объедините эти два списка, чтобы создать два связанных списка, каждый из которых состоит из наборов из 4 чисел в отсортированном порядке. Каждый раз, когда вы объединяете два элемента из списка 0 и два элемента из списка 1. Следующий наименьший элемент, очевидно, каждый раз, когда тот, который находится впереди списка.

Во втором проходе вы объединяете эти списки в два связанных списка, каждый из которых состоит из наборов из 8 отсортированных номеров, затем 16, затем 32 и т.д., пока результирующий список не будет содержать n или более чисел. Если n = 2 ^ k, то выполняется k = log2 (n), поэтому это принимает O (n log n).