【问题标题】:Merge sort algorithm in C not working as expectedC中的合并排序算法无法按预期工作
【发布时间】:2013-09-23 23:25:16
【问题描述】:

我正在尝试在 C 中实现归并排序算法。我了解该算法应该如何工作,但是我在实现时遇到了一些困难。

我知道它的实现有数百个示例和源代码,但我希望有人能帮助我理解为什么我的工作不正常。

我的代码在下面,在代码之后我解释了我到目前为止所做的尝试。

#include <stdio.h>

void merge(int a[], int L[], int R[],int nL, int nR) //nL and nR are the lengths of L[] and R[]
{
   int i = 0 , j = 0, k = 0;

    while(i<nL && j <nR)
    {
        if(L[i] <= R[j]){
            a[k] = L[i];
            i++;
        }
        else{
            a[k] = R[j];
            j++;
        }

        k++;
     }

     while(i < nL){
        a[k] = L[i];
        i++;
        k++;
     }

     while(j < nR) {
        a[k] = R[j];
        j++;
        k++;
     }
}   

void mergesort(int a[],int n)    //n is the length of a[]
{
   if(n < 2) return;     //BASE CASE
   int mid = n / 2;

   int left[mid];
   int right[n-mid];

   for(int i = 0; i < mid; i++)
   {
        left[i] = a[i];
   }

   for(int i = mid; i < n-1; i++)
   {
        right[i-mid] = a[i];
   }

   int nL = sizeof(left) / sizeof(left[0]);
   int nR = sizeof(right) / sizeof(right[0]);

   mergesort(left, nL);
   mergesort(right, nR);
   merge(a,left,right,nL,nR); 
}    


int main(void)
{
    printf("Initial:\n");
    printf("3 4 1 6\n");

    int numbers[4] = {3,4,1,6};
    int n = sizeof(numbers) / sizeof(int);

    mergesort(numbers,n);
    printf("Sorted:\n");
    for(int i =0 ; i < 4; i++)
    {
        printf("%d ", numbers[i]);
    }

    return 0;
}  

原样和未排序的数组[3,4,1,6] 的输出是0 0 1 3。 显然 1 和 3 相对于彼此的顺序是正确的,但开头的两个零显然是错误的。起初,在我看来,我将 4 和 6 插入到数组的右侧并且超出了数组的范围。

我使用了一些打印语句来尝试调试,但我无法弄清楚发生了什么。我什至尝试使用 gdb 跟踪我的代码,但我仍然无法对其进行排序。

有人对可能发生的事情有任何想法吗?

【问题讨论】:

  • 当您在调试器中运行它时,哪一行的行为与您的预期不同?
  • 这不是问题所在,但您不需要sizeof() 计算。您已经知道每个子序列的长度:midn-mid。假设您的 merge() 算法是正确的(一个很大的假设),您的右侧副本不正确,那么更可能是什么问题。它应该运行到 i &lt; n,而不是 i &lt; n-1
  • 同意 WhozCraig 的观点,for(int i = mid; i &lt; n-1; i++) 中的 -1 是不需要的。其余的,通过为自己提供一个函数 void dump_array(const char *tag, int n, int *a) { printf("%d:", n); for (int i = 0; i &lt; n; i++) printf("%s:%d:", a[i]); putchar('\n'); } 并经常调用它来进行调试:在进入 mergesort() (dump_array("enter", n, a);) 时,在创建左侧数组之后,在创建右侧数组之后,之后在合并结果后,您合并排序了左数组,合并排序了右数组。它将帮助您缩小问题范围。
  • 我无法支持乔纳森的评论。在处理分区排序算法(如合并排序、快速排序等)时,为您转储可疑序列的调试例程几乎是强制性的,以帮助您在开发过程中检查自己。
  • 哇:我的代码中有错别字——它们只能在 5 分钟内编辑。给自己提供一个函数void dump_array(const char *tag, int n, int *a) { printf("%s:%d:", tag, n); for (int i = 0; i &lt; n; i++) printf(" %3d", a[i]); putchar('\n'); }并经常调用它:在进入mergesort()(例如dump_array("--&gt;&gt;mergesort()", n, a);)时,创建左数组后,创建右数组后,合并排序后左数组,合并排序后合并结果后的正确数组 (dump_array("&lt;&lt;--mergesort()", n, a);)。它将帮助您缩小问题范围。

标签: c algorithm sorting mergesort


【解决方案1】:

编写merge() 代码的更接近惯用的方式是:

void merge(int a[], int L[], int R[],int nL, int nR)
{
    int i = 0, j = 0, k = 0;

    while (i < nL && j < nR)
    {
        if (L[i] <= R[j])
            a[k++] = L[i++];
        else
            a[k++] = R[j++];
    }
    while (i < nL)
        a[k++] = L[i++];  
    while (j < nR)
        a[k++] = R[j++];
}

这大约是您代码行数的一半,并且在广泛的范围内,要阅读的代码越少越好。有些人坚持在每个循环或条件之后使用大括号。我认为这没有必要(或特别有用),但如果这是你喜欢的风格,你可以使用它。

您的mergesort() 代码不那么松散,但可以更改为:

void mergesort(int a[],int n)    //n is the length of a[]
{
    if (n < 2)
        return;     //BASE CASE
    int mid = n / 2;
    int left[mid];
    int right[n-mid];

    for (int i = 0; i < mid; i++)
        left[i] = a[i];

    for (int i = mid; i < n; i++)
        right[i-mid] = a[i];

    mergesort(left, mid);
    mergesort(right, n - mid);
    merge(a, left, right, mid, n - mid); 
}

这包括对您的主要问题的修复 - 加载 right 数组的循环未复制最后一个元素。

带有调试功能如:

void dump_array(const char *tag, int n, int *a)
{
    printf("%s:%d:", tag, n);
    for (int i = 0; i < n; i++)
        printf(" %3d", a[i]);
    putchar('\n');
}

您可以通过以下方式进行很多有效的调试:

void mergesort(int a[],int n)
{
    if (n < 2)
        return;
    int mid = n / 2;
    int left[mid];
    int right[n-mid];

    dump_array("-->>mergesort()", n, a);

    for (int i = 0; i < mid; i++)
        left[i] = a[i];
    dump_array("left", mid, left);

    for (int i = mid; i < n; i++)
        right[i-mid] = a[i];
    dump_array("right", n - mid, right);

    mergesort(left, mid);
    dump_array("merged-L", mid, left);
    mergesort(right, n - mid);
    dump_array("merged-R", n - mid, right);
    merge(a, left, right, mid, n - mid);
    dump_array("<<--mergesort()", n, a);
}

在您的代码中,带有标签 right 的输出将显示最后一个元素的 0 或半随机数据,而不是您所期望的。这将暗示问题出在哪里。保留dump_array() 函数;拥有它是一种有用的生物。这是一个简单的版本;例如,您可以发明更复杂的版本,在长数组的中间位置输出换行符。

【讨论】:

  • +1 一个非常简洁易懂的merge()。令我印象深刻的是,OP 实际上使用了一种合并排序算法,该算法使用数组和元素计数作为输入,而不是大多数讲师似乎试图铲除它们的方法;数组和书挡索引。如果我可以再次为通用的“show-me-da-buffer”功能+1,我会的。
  • 抱歉,我花了一段时间才接受答案。非常感谢您对归并排序的所有见解以及如何更好地编写它。我一定会记下并使用该 dump_array 函数。
【解决方案2】:

问题出在以下代码中:

   for(int i = mid; i < n-1; i++)
   {
        right[i-mid] = a[i];
   }

应该是:

   for(int i = mid; i < n; i++) // right should range from mid to n - 1 *inclusive*
   {
        right[i-mid] = a[i];
   }

【讨论】:

    【解决方案3】:

    这是合并排序的简单实现,没有任何复杂性。只需传递数组指针和数组中的整数总数。

    void merge(int *a, int top)// Array pointer and max entries
    {
        int l1, k, l2, u1, u2, size = 1, i, j;
        int *sa;
        sa = (int *)calloc(top, sizeof(int));
    
        while (size < top)
        {
            l1 = 0;
            k = 0;
            while (l1 + size < top)
            {
                l2 = l1 + size;
                u1 = l2 - 1;
                u2 = ((l2 + size - 1) < top ? l2 + size - 1 : top - 1);
    
                for (i = l1, j = l2; i <= u1 && j <= u2; )// Merging
                {
                    sa[k++] = a[i] <= a[j] ? a[i++] : a[j++];
                }
                for ( ; i <= u1; )
                    sa[k++] = a[i++];
                for ( ; j <= u2; )
                    sa[k++] = a[j++];
                l1 = u2 + 1;
            }
    
            for (i = l1; i < top; i++) // For the left outs of the process
                sa[k++] = a[i];
    
            for (i = 0; i < top; i++)
                a[i] = sa[i];
    
            size *= 2;
        }
    }
    

    【讨论】:

    • 这段代码有一个严重的内存泄漏——sa 已分配但既没有返回也没有释放。
    • 另外,一个没有初始化或重新初始化子句的for循环(例如for ( ; i &lt;= u1; ))是一个while循环,应该写成while (i &lt;= u1)
    • 然而,修复了内存泄漏(free(sa); 就在返回之前),代码确实可以工作。 (即使没有该修复,它也会排序;它可以更频繁地与修复一起使用。)但是,它没有解释问题中原始代码的问题。它避免了递归,用迭代代替了它。它分配与递归版本一样多的空间,但使用单个calloc() 进行分配(尽管malloc() 会更好/足够;数组元素在使用之前被初始化)。它可以处理更大的数组,因为它不受堆栈大小的限制。
    猜你喜欢
    • 2015-09-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-05-03
    • 1970-01-01
    相关资源
    最近更新 更多