【问题标题】:Non-Recursive Merge Sort非递归合并排序
【发布时间】:2023-11-09 08:38:01
【问题描述】:

有人能用英语解释一下非递归归并排序是如何工作的吗?

谢谢

【问题讨论】:

标签: algorithm mergesort


【解决方案1】:

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

合并排序的洞察力在于,您可以将两个(或多个)小的排序记录合并到一个较大的排序运行中,您可以通过简单的流式操作“读取第一条/下一条记录”和“追加记录”——这意味着您不需要一次在 RAM 中设置大数据:您可以只使用两条记录,每条记录都来自不同的运行。如果您可以跟踪文件中排序运行的开始和结束位置,则可以简单地重复合并相邻运行对(到临时文件中)直到文件被排序:这需要对数的文件通过次数。

单个记录被简单排序:每次合并两个相邻运行时,每个运行的大小加倍。所以这是跟踪的一种方法。另一种是在运行的优先级队列上工作。从队列中取出两个最小的运行,合并它们,然后将结果排入队列——直到只剩下一个运行。如果您希望数据自然地从排序运行开始,这是合适的。

在处理大量数据集的实践中,您会想要利用内存层次结构。假设您有千兆字节的 RAM 和 TB 的数据。为什么不一次合并一千次运行?事实上,你可以做到这一点,运行的优先级队列可以提供帮助。这将显着减少您必须对文件进行排序才能对其进行排序的次数。一些细节留给读者练习。

【讨论】:

    【解决方案2】:

    以防万一有人仍然潜伏在这个线程中......我已经调整了上述 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 日编辑:修复了影响奇数列表的错误

    【讨论】:

      【解决方案3】:

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

      【讨论】:

        【解决方案4】:

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

        PS:谢谢你的代码,Rama Hoetzlein。

        PPS:堆上 100 GB?!!嗯,它是 Hadoop 集群上的一个虚拟堆,MergeSort 将在共享负载的多台机器上并行实现...

        【讨论】:

          【解决方案5】:

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

          【讨论】:

          • 我在这里 *.com/questions/37366365/… 尝试了类似的方法,但我无法弄清楚如何处理不是 2^x 形式的输入大小,有什么帮助吗?
          • 您可以通过组合一些行来大大简化代码,例如 b[m++]=a[i++];b[m]=a[i]; i++; m++;
          • 通过压缩代码让事情变得难以理解一样有趣线。我建议将 j++ 和 m++ 行移动到单独的行中,如果不是更有意义的变量名,也许可以使用一些 cmets。并在您的作业之间使用一致的空格。除非您添加尾随空格,否则每个人都喜欢易于阅读的代码。磁盘空间从来都不是问题,它的编译方式都是一样的。复杂的代码是魔鬼。 :P
          • @Raishin 大多数雇主都在寻找聪明的程序员。
          • 这段代码非常适合不允许递归的 NVIDIA OptiX 程序。
          【解决方案6】:

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

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

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

          【讨论】:

          • 因为归并排序总是按照相同的顺序进行分区和排序操作,无论数据集中的项目的初始顺序如何,都不需要使用堆栈来跟踪下一个操作。所有需要的是已知要排序的分区的大小(part_size,最初为 1)和要合并的第一个此类分区的索引(next_part,最初为 0)。对于每个“步骤”,合并大小为part_size 的分区,从next_partnext_part+part_size 开始,然后将next_part 碰撞part_size*2。如果那会从数组的末尾掉下来,...
          • ...double part_size 并将 next_part 设置为零。无需递归。
          【解决方案7】:

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

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

          【讨论】:

          • 合并排序可以就地完成,但通常“很难”做到这一点。
          • Algorithmist 上的那个看起来并不难。但是,如果您对一个太大而无法放入内存的数据集进行排序,则会失去一些局部性
          • 啊,你说的是合并排序,而不是自下而上的合并排序
          • 我问的是非递归合并排序,自底向上是非递归合并排序。
          • 奇数长度的数组如何划分?似乎最后一个元素可能永远不会被排序。
          【解决方案8】:

          引用Algorithmist:

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

          【讨论】:

          • 是的,每种归并排序都是 n log(n)。