【问题标题】:How to do merge sort without using additional arrays for splitting the initial array?如何在不使用其他数组拆分初始数组的情况下进行合并排序?
【发布时间】:2021-07-11 19:54:23
【问题描述】:

我试图解决一个问题,该问题要求编写归并排序代码,但不使用额外的数组来对初始数组进行分区。我想写的代码几乎是好的,但我面临的问题是我无法弄清楚如何在排序时维护和更新数组。我知道问题出在合并功能上。

如何修复代码?

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

void PrintArray(int A[], int n)
{
    for(int i=0; i < n; i++)
        printf("%d ", A[i]);
    printf("\n");
}

void merge(int A[], int left, int mid, int right, int n){
  
int B[n];

int i = left, j = mid+1, k=0;

while(i<=mid && j <= right){

  if(A[i]>=A[j]){
    B[k++] = A[i++];
  }

  else {
    B[k++] = A[j++];
  }

}

while(i<=mid){
  B[k++] = A[i++];
}

while(j<=right){
  B[k++] = A[j++];
}

for(i=0; i<n; i++){
  A[i] = B[i];
}

}

void MergeSort(int A[], int left, int right, int n)
{ 
  if(left<right){
    int mid;
    mid = floor((left+right)/2);
    MergeSort(A,left,mid,n/2);
    MergeSort(A,mid+1,right,n/2);
    merge(A,left,mid,right,n);
  }

  else return;
}

int main()
{
    int n;
    scanf("%d",&n);

    int A[n];

    for(int i=0; i < n; i++) scanf("%d", &A[i]);
        
    MergeSort(A, 0, n-1, n);
    PrintArray(A, n);
    return 0;
}

【问题讨论】:

  • en.wikipedia.org/wiki/Merge_sort 仅使用两个数组(源和目标)提供一堆算法。或者您正在寻找就地算法?
  • 您希望如何在单个数组中“分区”任何内容?如果将数组大小加倍并使用偏移量,则可以,但这与正确使用单独的数组进行分区没有什么不同。
  • @DavidC.Rankin 我想我们不需要真正对数组进行分区。我们可以使用变量 left、right 和 mid 对它进行虚拟分区,因为我们可以通过将第一个数组从左到中限制,然后将另一个从 mid+1 到右,虚拟地创建两个数组。
  • @EugeneSh。我认为使用单个额外数组作为临时存储进行排序,然后将其复制回初始数组将被视为就地算法,但正如 Craig 提到的那样,这种方法有时会导致堆栈溢出,并不是真正的就地方法,所以我想我的方法不准确。但是我这样尝试是因为问题特别指出我们不能创建 2 个额外的数组来进行分区并提供了一个代码结构来填充这就是为什么我们甚至不能像 Wikipedia 解决方案那样创建更多函数
  • @SuryanshManav 你有多个数组。您在main() 中声明了VLA A,在merge() 中声明了B。并且由于您从 MergeSort() 递归调用 merge(),因此您在每个递归级别创建了 3 个以上的 VLA。到最后,您可能已经使用了几十个数组。

标签: arrays c sorting mergesort


【解决方案1】:

merge 的最后一个for 循环中,更改:

A[i] = B[i];

进入:

A[left + i] = B[i];

编辑:即使在修复之后,排序仍然是错误的。最终循环的正确修复是:

for (i = left;  i <= right;  ++i)
    A[i] = B[i - left];

原来的for (i = 0; i &lt; n; ++i) 不起作用,因为只传递n / 2 可能传递的值比需要的值小一。通过这个新修复,n 根本不需要传递给 merge。所以,n 实际上只需要公共功能。请参阅下面的更新部分。


旁注:

您根本不需要使用floor。这对于整数数学来说是多余的[并且可能使结果不太准确]。

您正在按 reverse 顺序排序(例如,3, 2, 1 而不是 1, 2, 3)。要按 升序 顺序排序,在 merge 中,将:if (A[i] &gt;= A[j]) 更改为 if (A[i] &lt;= A[j])

您没有创建一个 initial 额外数组,但您在 merge 的堆栈中有 B,因此,您正在使用辅助/临时数组.无论您是在merge 的开头从A 复制到B,还是在merge 的末尾从B 复制回A,这都是正确的

因此,您没有真正的“就地”算法。

事实上,对于足够大的数组,在堆栈上放置B 会导致堆栈溢出。为B 使用堆分配可能会更好。

您可以将它放在mergeSort 的全局/公共“包装”函数中(例如mergeSortPublic)。开始做(例如)B = malloc(sizeof(int) * n),最后做free(B)。您可以将 B 设为全局范围或将其作为额外的 arg 传递给您的合并函数


更新:

这是一个添加了诊断测试的完全清理版本。

由于merge 中最终循环的变化,它不再需要n 值。因此,mergeSort 不再需要 mergeSortPub 更改。

我重构了merge 中的第一个循环,通过不重新获取已经获取的数组值来稍微快一些。优化器可能发现了这种加速,但我认为最好明确说明。

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

void
PrintArray(int A[], int n)
{
    int totlen = 0;

    for (int i = 0; i < n; i++) {
        totlen += printf(" %d", A[i]);
        if (totlen >= 72) {
            printf("\n");
            totlen = 0;
        }
    }

    if (totlen > 0)
        printf("\n");
}

void
merge(int A[], int left, int mid, int right, int *B)
{

    int i = left,
        j = mid + 1,
        k = 0;

    int Ai = A[i];
    int Aj = A[j];

    while (i <= mid && j <= right) {
        if (Ai <= Aj) {
            B[k++] = Ai;
            Ai = A[++i];
        }
        else {
            B[k++] = Aj;
            Aj = A[++j];
        }
    }

    while (i <= mid)
        B[k++] = A[i++];

    while (j <= right)
        B[k++] = A[j++];

    // original code
#if 0
    for (i = 0; i < n; i++)
        A[i] = B[i];
#endif

    // first fix -- still broken
#if 0
    for (i = 0; i < n; i++)
        A[left + i] = B[i];
#endif

    // correct fix
#if 1
    for (i = left;  i <= right;  ++i)
        A[i] = B[i - left];
#endif
}

void
MergeSort(int A[], int left, int right, int *B)
{
    if (left < right) {
        int mid = (left + right) / 2;
        MergeSort(A, left, mid, B);
        MergeSort(A, mid + 1, right, B);
        merge(A, left, mid, right, B);
    }
}

void
MergeSortPub(int A[], int n)
{
    int *B = malloc(sizeof(*B) * n);

    MergeSort(A,0,n - 1,B);

    free(B);
}

void
dotest(int tstno)
{

    int n = rand() % 1000;

    int *A = malloc(sizeof(*A) * n);

    for (int i = 0;  i < n;  ++i)
        A[i] = n - i;

    MergeSortPub(A,n);

    int old = A[0];
    int bad = 0;
    for (int i = 1;  i < n;  ++i) {
        int cur = A[i];
        if (cur < old) {
            if (! bad)
                printf("dotest: %d -- i=%d old=%d cur=%d\n",tstno,i,old,cur);
            bad = 1;
        }
        old = cur;
    }

    if (bad) {
        PrintArray(A,n);
        exit(1);
    }
}

int
main(void)
{
    int n;

#if 0
    scanf("%d", &n);

    int A[n];

    for (int i = 0; i < n; i++)
        scanf("%d", &A[i]);

    MergeSortPub(A, n);
    PrintArray(A, n);
#else
    for (int tstno = 1;  tstno <= 1000;  ++tstno)
        dotest(tstno);
#endif

    return 0;
}

【讨论】:

  • 非常感谢 Craig 您的 cmets 并更正了代码。我根据您的建议更新了 for 循环,它现在通过了所有测试结果。但我想我们仍然需要在函数中使用“n”来声明 B 数组,否则我们怎么知道大小?此外,唯一允许的两个函数是 Merge 和 MergeSort 函数,因为问题提供了代码模板,其中已经存在这些参数。是的,我按降序排序,因为问题是以这种方式提出的。
  • 老实说,我并不真正理解就地算法的含义,所以我认为这种方法是一种就地算法,但正如你提到的,它可能会因为大型输入出现溢出错误,所以我得到一个现在的想法。另外,我现在明白了。使用 floor 进行整数计算根本没有用,也许我想要的是 ceil LOL,但我很高兴它在按照您的建议更新 for 循环后工作。我一直在研究它,当我把它贴在这里时,我一无所知。再次感谢您的时间和精力。 ;))
  • 此方法使用了一个与原始数组大小相同的附加数组。
【解决方案2】:

合并排序的一些变体除了局部变量外不使用任何额外的空间。这种优化的实现比较复杂,比传统的归并排序慢 50% 左右,而且这些实现大多用于学术研究。

有一篇关于一种变体的 wiki 文章,即插入和合并排序的混合体。

https://en.wikipedia.org/wiki/Block_sort

链接到此 github 存储库中 grailsort.h 中更优化的版本。 void GrailSort(SORT_TYPE *arr,int Len) 函数不使用任何额外的缓冲区。

https://github.com/Mrrl/GrailSort

【讨论】:

    猜你喜欢
    • 2013-04-13
    • 2012-03-24
    • 1970-01-01
    • 1970-01-01
    • 2011-01-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多