簡體   English   中英

鏈接列表如何實現O(n log n)排序時間?

[英]How could a linked list achieve O(n log n) sorting time?

我很好奇,首先,為什么std::liststd::forward_list包括排序函數作為成員函數,與其他標准庫容器不同。 但對我來說更有趣的是, CPPReferenceCPlusPlus都聲稱這種排序是在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;
    }
}

該程序輸出的數字給出了以下圖表:

轉發列表排序時間

這確實看起來像O( n log n )增長(盡管每三分之一的峰值都很有趣)。 圖書館是如何做到這一點的? 也許復制到支持排序,排序和復制的容器?

鏈接列表可以使用MergesortO(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;
}

使用指向列表的指針數組的自下而上合並排序的示例代碼,其中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);
}

Mergesort是O(nlogn)的鏈表。 我不知道C ++的默認排序函數是什么,但我懷疑它的mergesort。

我沒有這里的標准,但CPPReference聲明sort的復雜性是Nlog(N) 比較 這意味着即使快速排序也是標准的符合實現,因為它將是Nlog(N)比較(但不是Nlog(N)時間)。

您從未知長度的未排序列表開始。 假設元素編號為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)。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM