非递归合并排序

时间:2009-10-13 02:03:21

标签: algorithm mergesort

有人可以用英语解释非递归合并排序的工作原理吗?

由于

8 个答案:

答案 0 :(得分:19)

非递归合并排序通过考虑输入数组上的窗口大小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)。

答案 1 :(得分:15)

循环遍历元素,并在必要时通过交换两个对每个相邻的两个组进行排序。

现在,处理两组的组(任意两组,最可能是相邻的组,但你可以使用第一组和最后一组)将它们合并为一组,重复选择每组中最低值的元素,直到所有4个元素都是合并为一组4.现在,你只有4个组加上可能的余数。使用前一个逻辑的循环,再次执行所有操作,除非此时工作为4个组。此循环运行,直到只有一个组。

答案 2 :(得分:8)

引自Algorithmist

  

自下而上合并排序是一个   合并的非递归变体   sort,数组按其排序   一系列的传球。在每个期间   传递,数组被分成块   大小 m 。 (最初, m = 1 )。   每两个相邻的块都被合并   (如在正常的合并排序中),和   下一次传球是用两倍大的   值 m

答案 3 :(得分:4)

递归和非递归合并排序具有相同的时间复杂度O(nlog(n))。这是因为两种方法都以一种或另一种方式使用堆栈。

在非递归方法中    用户/程序员定义并使用堆栈

在递归方法中,系统在内部使用堆栈来存储递归调用的函数的返回地址

答案 4 :(得分:4)

您希望使用非递归MergeSort的主要原因是为了避免递归堆栈溢出。例如,我正在尝试按字母数字顺序对1亿条记录进行排序,每条记录的长度约为1千字节(= 100千兆字节)。订单(N ^ 2)排序将需要10 ^ 16次操作,即每次比较操作即使在0.1微秒也要花费数十年的时间。订单(N log(N))合并排序将花费少于10 ^ 10次操作或少于一小时以相同的操作速度运行。但是,在MergeSort的递归版本中,1亿个元素排序导致对MergeSort()的5000万次递归调用。每个堆栈递归几百个字节,这会溢出递归堆栈,即使该进程很容易适应堆内存。使用堆上动态分配的内存进行合并排序 - 我正在使用上面的Rama Hoetzlein提供的代码,但我在堆上使用动态分配的内存而不是使用堆栈 - 我可以用我的1亿条记录排序非递归合并排序,我不会溢出堆栈。网站“Stack Overflow”的适当对话!

PS:感谢代码,Rama Hoetzlein。

PPS:堆上100千兆字节?!!好吧,它是Hadoop集群上的虚拟堆,MergeSort将在共享负载的几台机器上并行实现......

答案 5 :(得分:1)

我是新来的。 我修改了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;

答案 6 :(得分:0)

以防任何人仍然潜伏在这个帖子中...我已经调整了Rama Hoetzlein的非递归合并排序算法来排序双链表。这种新排序是就地的,稳定的,并且避免了在其他链表合并排序实现中划分代码的时间成本高昂的列表。

// 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:修正了影响奇数列表的错误

答案 7 :(得分:0)

对这个感兴趣吗?可能不会。那好吧。这里什么都没有。

merge-sort的见解在于,您可以将两个(或几个)小型排序的记录合并为一个较大的排序的运行,并且可以使用类似流的简单操作“读取第一条/下一条记录”和“追加记录”-这意味着您不需要一次在RAM中设置大数据集:您只需两条记录即可获得,每条记录均来自不同的运行。如果您只能跟踪已排序的运行在文件中的开始和结束位置,则可以简单地重复将成对的相邻运行(合并到临时文件中)直到对文件进行排序:这需要对数遍历文件。

单个记录的排序很简单:每次合并两个相邻的运行时,每个运行的大小都会加倍。因此,这是一种跟踪方式。另一种是在运行的优先级队列上工作。从队列中进行两次最小的运行,合并它们,然后将结果排队-直到仅剩一个运行为止。如果您希望数据自然地从已排序的运行开始,则此方法很合适。

在实践中,使用大量数据集,您将需要利用内存层次结构。假设您有GB的RAM和TB的数据。为什么不一次合并一千个运行?确实,您可以做到这一点,运行的优先级队列可以提供帮助。这将大大减少您必须对文件进行分类才能通过的次数。一些细节留给读者练习。