为什么递归合并排序优于迭代合并排序,即使后者具有辅助 space 复杂性?

Why is recursive Merge Sort preferred over iterative Merge Sort even though the latter has auxillary space complexity?

在研究合并排序算法时,我很好奇这种排序算法是否可以进一步优化。发现存在具有相同时间复杂度但更好 O(1) space 复杂度的合并排序算法的迭代版本。就性能而言,迭代方法总是优于递归方法。那为什么它在任何常规算法课程中都不那么常见并且很少被谈论呢? Here's the link to Iterative Merge Sort algorithm

Found out that there exists Iterative version of Merge Sort algorithm with same time complexity but even better O(1) space complexity

您链接到的合并排序的迭代 bottom-up 实现没有 O(1) space 复杂性。它维护数组的副本,因此这表示 O(n) space 复杂度。因此,这使得额外的 O(logn) 堆栈 space (对于递归实现)与总的 space 复杂度无关。

在您的问题标题和一些评论中,您使用了 “辅助 space 复杂性” 等词。这就是我们通常所说的 space 复杂性,但您似乎暗示这个术语意味着 constant space 复杂性。这不是真的。 “辅助”是指输入所用的space以外的space。这个术语没有告诉我们实际的复杂性。

如果您认为它具有 O(1) space 的复杂性,请再看一遍。它们具有大小为 n 的原始数组 A,以及大小为 n 的辅助数组 temp。 (实际上只需要 n/2 但他们保持简单。)

他们需要第二个数组的原因是当你合并时,你将底部区域复制到临时区域,然后从它原来的位置开始合并回来。

所以权衡就是这样。递归解决方案涉及的繁琐位更少,概念更清晰,但在两个解决方案共享的 O(n) 内存开销之上增加了 O(log(n)) 内存开销。当您尝试交流概念时,这是一个直接的胜利。

此外,在实践中我相信递归也是一种胜利。

在迭代方法中,您不断地遍历整个数组。在大型数组的情况下,这意味着数据进入缓存进行一次传递,被操作,然后在您加载数组的其余部分时丢失。只需要在下一次通过时再次加载。

相比之下,在递归方法中,对于相当于前几遍的操作,您将它们加载到缓存中,对它们进行完全排序,然后继续。 (你赢得多少次胜利在很大程度上取决于数据类型、内存布局和 CPU 缓存的大小。)当你将太多数据合并到适合缓存。算法课程通常会忽略此类底层细节,但它们对实际性能非常重要。

递归自顶向下合并排序主要是教育性的。大多数实际的库使用混合插入排序和自下而上合并排序的一些变体,使用插入排序创建小的排序 运行s ,这些 运行s 将在偶数次合并传递中合并,以便在原始之间来回合并临时数组以原始数组中的排序数据结束(除了数组末尾的单例 运行s 之外,合并中没有复制操作,这可以通过选择适当的初始 运行 大小来避免对于插入排序(注意 - 这在我的示例代码中没有完成,我只使用 运行 大小 32 或 64,而像 Timsort 这样的更高级的方法确实选择了合适的 运行 大小)。

自下而上稍微快一些,因为数组指针和索引将保存在寄存器中(假设优化编译器),而自上而下是将数组指针和索引推入|从堆栈中弹出。


虽然我不确定 OP 实际上意味着合并排序的 O(1) space 复杂度,但它是可能的,但它比传统的 O(n) 合并排序慢了大约 50% )辅助space。现在主要是研究(教育)工作。代码相当复杂。 Link 到示例代码。其中一个选项是根本没有额外的缓冲区。基准 table 适用于相对较少的键(最多 32767 个键)。对于大量的键,这个例子最终比优化插入+自下而上的归并排序慢了大约 50%(std::stable_sort 被泛化了,比如每次比较都使用指向函数的指针,所以它是未完全优化)。

https://github.com/Mrrl/GrailSort


示例混合插入 + 自底向上合并排序 C++ 代码(省略原型):

void MergeSort(int a[], size_t n)           // entry function
{
    if(n < 2)                               // if size < 2 return
        return;
    int *b = new int[n];
    MergeSort(a, b, n);
    delete[] b;
}

void MergeSort(int a[], int b[], size_t n)
{
size_t s;                                   // run size 
    s = ((GetPassCount(n) & 1) != 0) ? 32 : 64;
    {                                       // insertion sort
        size_t l, r;
        size_t i, j;
        int t;
        for (l = 0; l < n; l = r) {
            r = l + s;
            if (r > n)r = n;
            l--;
            for (j = l + 2; j < r; j++) {
                t = a[j];
                i = j-1;
                while(i != l && a[i] > t){
                    a[i+1] = a[i];
                    i--;
                }
                a[i+1] = t;
            }
        }
    }

    while(s < n){                           // while not done
        size_t ee = 0;                      // reset end index
        size_t ll;
        size_t rr;
        while(ee < n){                      // merge pairs of runs
            ll = ee;                        // ll = start of left  run
            rr = ll+s;                      // rr = start of right run
            if(rr >= n){                    // if only left run
                rr = n;                     //   copy left run
                while(ll < rr){
                    b[ll] = a[ll];
                    ll++;
                }
                break;                      //   end of pass
            }
            ee = rr+s;                      // ee = end of right run
            if(ee > n)
                ee = n;
            Merge(a, b, ll, rr, ee);
        }
        std::swap(a, b);                    // swap a and b
        s <<= 1;                            // double the run size
    }
}

void Merge(int a[], int b[], size_t ll, size_t rr, size_t ee)
{
    size_t o = ll;                          // b[]       index
    size_t l = ll;                          // a[] left  index
    size_t r = rr;                          // a[] right index
    while(1){                               // merge data
        if(a[l] <= a[r]){                   // if a[l] <= a[r]
            b[o++] = a[l++];                //   copy a[l]
            if(l < rr)                      //   if not end of left run
                continue;                   //     continue (back to while)
            while(r < ee)                   //   else copy rest of right run
                b[o++] = a[r++];
            break;                          //     and return
        } else {                            // else a[l] > a[r]
            b[o++] = a[r++];                //   copy a[r]
            if(r < ee)                      //   if not end of right run
                continue;                   //     continue (back to while)
            while(l < rr)                   //   else copy rest of left run
                b[o++] = a[l++];
            break;                          //     and return
        }
    }
}

size_t GetPassCount(size_t n)               // return # passes
{
    size_t i = 0;
    for(size_t s = 1; s < n; s <<= 1)
        i += 1;
    return(i);
}