web-dev-qa-db-ja.com

非再帰マージソート

誰かが英語で非再帰的マージソートがどのように機能するのか説明できますか?

ありがとう

26
DarthVader

要素をループし、必要に応じて2つを交換することで、隣接する2つすべてのグループを並べ替えます。

ここで、2つのグループのグループ(任意の2つ、最も可能性の高い隣接グループですが、最初と最後のグループを使用できます)を処理して1つのグループにマージし、4つの要素すべてがグループ4になりました。これで、4のグループと残りの可能性のあるグループしかなくなりました。前のロジックの周りのループを使用して、今回は4のグループで機能することを除いて、もう一度繰り返します。このループは、グループが1つになるまで実行されます。

15
DigitalRoss

非再帰的マージソートは、入力配列に対してウィンドウサイズ1,2,4,8,16..2 ^ nを考慮することで機能します。各ウィンドウ(以下のコードでは「k」)では、隣接するすべてのウィンドウのペアが一時スペースにマージされ、配列に戻されます。

これが私の単一の関数、Cベースの非再帰マージソートです。入力と出力は「a」にあります。 'b'の一時ストレージ。ある日、適切なバージョンが欲しいのですが。

float a[50000000],b[50000000];
void mergesort (long num)
{
    int rght, wid, rend;
    int i,j,m,t;

    for (int k=1; k < num; k *= 2 ) {       
        for (int left=0; left+k < num; left += k*2 ) {
            rght = left + k;        
            rend = rght + k;
            if (rend > num) rend = num; 
            m = left; i = left; j = rght; 
            while (i < rght && j < rend) { 
                if (a[i] <= a[j]) {         
                    b[m] = a[i]; i++;
                } else {
                    b[m] = a[j]; j++;
                }
                m++;
            }
            while (i < rght) { 
                b[m]=a[i]; 
                i++; m++;
            }
            while (j < rend) { 
                b[m]=a[j]; 
                j++; m++;
            }
            for (m=left; m < rend; m++) { 
                a[m] = b[m]; 
            }
        }
    }
}

ちなみに、これがO(n log n)であることを証明することも非常に簡単です。ウィンドウサイズの外側のループは2の累乗で大きくなるため、kの繰り返しはlog nです。内部ループでカバーされるウィンドウは多数ありますが、指定されたkのすべてのウィンドウは入力配列を正確にカバーするため、内部ループはO(n)です。内部ループと外部ループを組み合わせる:O(n)* O(log n)= O(n log n)。

19
Rama Hoetzlein

Algorithmist からの引用:

ボトムアップマージソートは、マージソートの非再帰的な変形であり、配列は一連のパスによってソートされます。各パスの間、配列はサイズmのブロックに分割されます。 (最初はm = 1)。隣接する2つのブロックはすべてマージされ(通常のマージソートの場合と同様)、次のパスはmの2倍の値で行われます。

8
bobbymcr

非再帰的なMergeSortを使用する主な理由は、再帰スタックオーバーフローを回避するためです。たとえば、1億のレコードをソートしようとしています。各レコードの長さは約1 kByte(= 100ギガバイト)で、英数字順にソートします。 order(N ^ 2)の並べ替えには10 ^ 16の演算が必要です。つまり、比較演算ごとに0.1マイクロ秒でも実行するには数十年かかります。注文(N log(N))マージソートは、同じ操作速度で実行するために10 ^ 10未満の操作または1時間未満かかります。ただし、MergeSortの再帰バージョンでは、1億の要素の並べ替えにより、MergeSort()が5,000万回再帰的に呼び出されます。スタックの再帰あたり数百バイトで、プロセスがヒープメモリ内に簡単に収まる場合でも、再帰スタックがオーバーフローします。ヒープに動的に割り当てられたメモリを使用してマージソートを実行します-上記のRama Hoetzleinによって提供されたコードを使用していますが、スタックを使用する代わりにヒープに動的に割り当てられたメモリを使用しています-1億レコードを非再帰的なマージソートであり、スタックをオーバーフローさせません。ウェブサイト「Stack Overflow」にふさわしい会話!

PS:コードをありがとう、Rama Hoetzlein。

PPS:ヒープで100ギガバイト?まあ、それはHadoopクラスターの仮想ヒープであり、MergeSortは、負荷を共有する複数のマシンに並行して実装されます...

4
K. Ricci

再帰的マージソートと非再帰的マージソートはどちらも、O(nlog(n))と同じ時間の複雑さを持っています。これは、両方のアプローチがスタックをどちらか一方の方法で使用するためです。

非再帰的アプローチでは、ユーザー/プログラマーがスタックを定義して使用します

再帰的アプローチでは、スタックはシステムによって内部的に使用され、再帰的に呼び出される関数の戻りアドレスを格納します

4
Vivek Dani

私はここでは新人です。 Rama Hoetzleinソリューションを変更しました(アイデアをありがとう)。私のマージソートは最後のコピーバックループを使用していません。さらに、挿入ソートにフォールバックします。私は自分のラップトップでそれをベンチマークしました、そしてそれは最速です。再帰バージョンよりも優れています。ちなみに、それはJavaであり、降順から昇順でソートされます。もちろん、それは反復的です。マルチスレッドにすることができます。コードは複雑になっています。したがって、興味がある人は、見てください。

コード:

    int num = input_array.length;
    int left = 0;
    int right;
    int temp;
    int LIMIT = 16;
    if (num <= LIMIT)
    {
        // Single Insertion Sort
        right = 1;
        while(right < num)
        {
            temp = input_array[right];
            while(( left > (-1) ) && ( input_array[left] > temp ))
            {
                input_array[left+1] = input_array[left--];
            }
            input_array[left+1] = temp;
            left = right;
            right++;
        }
    }
    else
    {
        int i;
        int j;
        //Fragmented Insertion Sort
        right = LIMIT;
        while (right <= num)
        {
            i = left + 1;
            j = left;
            while (i < right)
            {
                temp = input_array[i];
                while(( j >= left ) && ( input_array[j] > temp ))
                {
                    input_array[j+1] = input_array[j--];
                }
                input_array[j+1] = temp;
                j = i;
                i++;
            }
            left = right;
            right = right + LIMIT;
        }
        // Remainder Insertion Sort
        i = left + 1;
        j = left;
        while(i < num)
        {
            temp = input_array[i];
            while(( j >= left ) && ( input_array[j] > temp ))
            {
                input_array[j+1] = input_array[j--];
            }
            input_array[j+1] = temp;
            j = i;
            i++;
        }
        // Rama Hoetzlein method
        int[] temp_array = new int[num];
        int[] swap;
        int k = LIMIT;
        while (k < num)
        {
            left = 0;
            i = k;// The mid point
            right = k << 1;
            while (i < num)
            {
                if (right > num)
                {
                    right = num;
                }
                temp = left;
                j = i;
                while ((left < i) && (j < right))
                {
                    if (input_array[left] <= input_array[j])
                    {
                        temp_array[temp++] = input_array[left++];
                    }
                    else
                    {
                        temp_array[temp++] = input_array[j++];
                    }
                }
                while (left < i)
                {
                    temp_array[temp++] = input_array[left++];
                }
                while (j < right)
                {
                    temp_array[temp++] = input_array[j++];
                }
                // Do not copy back the elements to input_array
                left = right;
                i = left + k;
                right = i + k;
            }
            // Instead of copying back in previous loop, copy remaining elements to temp_array, then swap the array pointers
            while (left < num)
            {
                temp_array[left] = input_array[left++];
            }
            swap = input_array;
            input_array = temp_array;
            temp_array = swap;
            k <<= 1;
        }
    }

    return input_array;
1
ytoamn

誰かがこのスレッドにまだ潜んでいる場合に備えて...私は上記のラマヘッツラインの非再帰マージソートアルゴリズムを適合させて、二重リンクリストをソートしました。この新しい並べ替えはインプレースで安定しており、他のリンクリストのマージ並べ替えの実装にある時間のかかるリスト分割コードを回避します。

// MergeSort.cpp
// Angus Johnson 2017
// License: Public Domain

#include "io.h"
#include "time.h"
#include "stdlib.h"

struct Node {
    int data;
    Node *next;
    Node *prev;
    Node *jump;
};

inline void Move2Before1(Node *n1, Node *n2)
{
    Node *prev, *next;
    //extricate n2 from linked-list ...
    prev = n2->prev;
    next = n2->next;
    prev->next = next; //nb: prev is always assigned
    if (next) next->prev = prev;
    //insert n2 back into list ...  
    prev = n1->prev;
    if (prev) prev->next = n2;
    n1->prev = n2;
    n2->prev = prev;
    n2->next = n1;
}

void MergeSort(Node *&nodes)
{
    Node *first, *second, *base, *tmp, *prev_base;

    if (!nodes || !nodes->next) return;
    int mul = 1;
    for (;;) {
        first = nodes;
        prev_base = NULL;
        //sort each successive mul group of nodes ...
        while (first) {
            if (mul == 1) {
                second = first->next;
                if (!second) { 
                  first->jump = NULL;
                  break;
                }
                first->jump = second->next;
            }
            else
            {
                second = first->jump;
                if (!second) break;
                first->jump = second->jump;
            }
            base = first;
            int cnt1 = mul, cnt2 = mul;
            //the following 'if' condition marginally improves performance 
            //in an unsorted list but very significantly improves
            //performance when the list is mostly sorted ...
            if (second->data < second->prev->data) 
                while (cnt1 && cnt2) {
                    if (second->data < first->data) {
                        if (first == base) {
                            if (prev_base) prev_base->jump = second;
                            base = second;
                            base->jump = first->jump;
                            if (first == nodes) nodes = second;
                        }
                        tmp = second->next;
                        Move2Before1(first, second);
                        second = tmp;
                        if (!second) { first = NULL; break; }
                        --cnt2;
                    }
                    else
                    {
                        first = first->next;
                        --cnt1;
                    }
                } //while (cnt1 && cnt2)
            first = base->jump;
            prev_base = base;
        } //while (first)
        if (!nodes->jump) break;
        else mul <<= 1;
    } //for (;;)
}

void InsertNewNode(Node *&head, int data)
{
    Node *tmp = new Node;
    tmp->data = data;
    tmp->next = NULL;
    tmp->prev = NULL;
    tmp->jump = NULL;
    if (head) {
        tmp->next = head;
        head->prev = tmp;
        head = tmp;
    }
    else head = tmp;
}

void ClearNodes(Node *head)
{
    if (!head) return;
    while (head) {
        Node *tmp = head;
        head = head->next;
        delete tmp;
    }
}

int main()
{  
    srand(time(NULL));
    Node *nodes = NULL, *n;
    const int len = 1000000; //1 million nodes 
    for (int i = 0; i < len; i++)
        InsertNewNode(nodes, Rand() >> 4);

    clock_t t = clock();
    MergeSort(nodes);    //~1/2 sec for 1 mill. nodes on Pentium i7. 
    t = clock() - t;
    printf("Sort time: %d msec\n\n", t * 1000 / CLOCKS_PER_SEC);

    n = nodes;
    while (n)
    {
        if (n->prev && n->data < n->prev->data) { 
            printf("oops! sorting's broken\n"); 
            break;
        }
        n = n->next;
    }
    ClearNodes(nodes);
    printf("All done!\n\n");
    getchar();
    return 0;
}

2017-10-27を編集:奇数番号リストに影響するバグを修正

0
Angus Johnson