链接列表如何实现O(n log n)排序时间?

时间:2015-04-28 03:20:44

标签: c++ sorting linked-list time-complexity

我很好奇,首先,为什么std::liststd::forward_list包含排序函数作为成员函数,与其他标准库容器不同。但对我来说更有意思的是,CPPReferenceCPlusPlus都声称此排序是在O( n log n )时间内完成的。< / p>

我甚至无法想象如何在没有随机访问元素的情况下对容器进行排序。因此,我使用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 )增长(尽管每三分之一的峰值都很有趣)。图书馆是如何做到这一点的?也许复制到一个支持排序,排序和复制的容器?

5 个答案:

答案 0 :(得分:19)

可以使用Mergesort O(n log n)中对链接列表进行排序。

有趣的是,由于链接列表已经具有适当的结构,因此使用Mergesort对链接列表进行排序只需要 O(1)额外空间。

这需要专门针对列表结构调整的专门算法,这也是sort是列表成员函数的原因,而不是单独的函数。

至于它是如何工作的 - 你需要的只是合并操作。合并操作有两个列表。您查看两个列表的头部,并删除最小的头并将其附加到结果列表中。你一直这样做,直到所有的头都被合并到大名单中 - 完成。

这是C ++中的示例合并操作:

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;
}

答案 1 :(得分:4)

自下而上合并排序的示例代码,使用指向列表的指针数组,其中array [i]指向大小为2 ^ i的列表(除了最后一个指针指向无限大小的列表)。这就是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 * 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);
}

答案 2 :(得分:2)

对于链接列表,

Mergesort是O(nlogn)。我不知道C ++的默认排序函数是什么,但我怀疑它是mergesort。

答案 3 :(得分:2)

我这里没有标准,但是{​​{3}}声明排序的复杂性是Nlog(N)比较。这意味着即使快速排序也是标准的符合实现,因为它将是Nlog(N)比较(但不是Nlog(N)时间)。

答案 4 :(得分:0)

您从未知长度的未排序列表开始。假设元素编号为0,1,2,3 ......

在第一遍中,您创建了两个链接列表,每个链接列表按排序顺序包含一对数字。列表0以排序顺序的元素0和1开始。列表1以排序顺序从元素2和3开始。元素4和5按排序顺序添加到列表0,6和7添加到列表1,依此类推。显然必须注意不要超过原始列表的末尾。

在第二遍中,您合并这两个列表以创建两个链接列表,每个链接列表按排序顺序组成4个数字。每次组合List 0中的两个元素和List 1中的两个元素时,下一个最小元素显然是每次在列表前面的元素。

在第二遍中,将这些列表合并为两个链接列表,每个列表包含8个已排序数字的集合,然后是16个,然后是32个,依此类推,直到结果列表包含n个或更多数字。如果n = 2 ^ k则存在k = log2(n)次通过,因此这需要O(n log n)。