【发布时间】:2023-11-09 08:38:01
【问题描述】:
有人能用英语解释一下非递归归并排序是如何工作的吗?
谢谢
【问题讨论】:
-
虽然 bobbymcr 提供了一个很好的答案,但还要注意递归和迭代在形式上是等价的。见*.com/questions/159590/…
有人能用英语解释一下非递归归并排序是如何工作的吗?
谢谢
【问题讨论】:
对这个有兴趣了吗?可能不是。那好吧。这里什么都没有。
合并排序的洞察力在于,您可以将两个(或多个)小的排序记录合并到一个较大的排序运行中,您可以通过简单的流式操作“读取第一条/下一条记录”和“追加记录”——这意味着您不需要一次在 RAM 中设置大数据:您可以只使用两条记录,每条记录都来自不同的运行。如果您可以跟踪文件中排序运行的开始和结束位置,则可以简单地重复合并相邻运行对(到临时文件中)直到文件被排序:这需要对数的文件通过次数。
单个记录被简单排序:每次合并两个相邻运行时,每个运行的大小加倍。所以这是跟踪的一种方法。另一种是在运行的优先级队列上工作。从队列中取出两个最小的运行,合并它们,然后将结果排入队列——直到只剩下一个运行。如果您希望数据自然地从排序运行开始,这是合适的。
在处理大量数据集的实践中,您会想要利用内存层次结构。假设您有千兆字节的 RAM 和 TB 的数据。为什么不一次合并一千次运行?事实上,你可以做到这一点,运行的优先级队列可以提供帮助。这将显着减少您必须对文件进行排序才能对其进行排序的次数。一些细节留给读者练习。
【讨论】:
以防万一有人仍然潜伏在这个线程中......我已经调整了上述 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 日编辑:修复了影响奇数列表的错误
【讨论】:
我是新来的。 我已经修改了 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;
【讨论】:
您希望使用非递归 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 将在共享负载的多台机器上并行实现...
【讨论】:
非递归合并排序通过考虑输入数组上 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)。
【讨论】:
b[m++]=a[i++]; 与 b[m]=a[i]; i++; m++;。
递归和非递归归并排序的时间复杂度都是 O(nlog(n))。这是因为这两种方法都以一种或另一种方式使用堆栈。
在非递归方法中 用户/程序员定义和使用堆栈
在递归方法中,系统内部使用堆栈来存储递归调用的函数的返回地址
【讨论】:
part_size,最初为 1)和要合并的第一个此类分区的索引(next_part,最初为 0)。对于每个“步骤”,合并大小为part_size 的分区,从next_part 和next_part+part_size 开始,然后将next_part 碰撞part_size*2。如果那会从数组的末尾掉下来,...
part_size 并将 next_part 设置为零。无需递归。
循环遍历元素,并在必要时通过交换两个元素来对每个相邻的两个组进行排序。
现在,处理两个组的组(任意两个,最有可能是相邻的组,但您可以使用第一个和最后一个组)将它们合并到一个组中,重复从每个组中选择最低值的元素,直到所有 4 个元素合并成一个 4 组。现在,你只有 4 组加上一个可能的余数。围绕前面的逻辑使用循环,再次执行所有操作,但这次以 4 人为一组工作。此循环一直运行,直到只有一个组。
【讨论】:
引用Algorithmist:
自下而上的归并排序是 合并的非递归变体 sort,其中数组按 一连串的传球。期间每 通过,将数组分成块 m 的大小。 (最初,m = 1)。 每两个相邻的块合并 (如在正常的归并排序中),以及 下一次传球是用两倍大的 m 的值。
【讨论】: