【问题标题】:Why does my quicksort crash with large, reverse-sorted arrays?为什么我的快速排序会因大型反向排序数组而崩溃?
【发布时间】:2016-11-03 12:34:37
【问题描述】:

我正在学习 C,并尝试了递归快速排序算法。在较小的输入尺寸下,它按预期工作;对于随机生成的数组,所有测试大小(最多 100,000)都没有问题。对于降序数组,它以某种数组大小 (32,506) 以某种方式中断(Windows 给我一条消息,程序已停止工作)。我的代码中是否有任何错误(例如任何错误的内存分配 - 我不确定我是否正确)或者 C 在递归调用或其他方面是否有限制?

编辑: 我知道我的 Quicksort 实现是相当幼稚的,并且它在这种输入中表现得非常糟糕,但我没想到它会崩溃。

我在 Windows 10 的命令提示符下使用 GCC 和 MinGW。我不确定如何找出究竟发生了什么,因为尽管 Windows 告诉我我的程序已停止,但我并没有真正收到任何指定的错误消息工作。

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

int partition(int *a, int lo, int hi) {
    int i = lo; int j = hi+1; int v,t;
    v = a[lo]; //partition element
    while (1) {
        while (a[++i] < v) {if (i == hi) break;}
        while (v < a[--j]) {if (j == lo) break;}
        if (i >= j) break;
        t = a[j]; a[j] = a[i]; a[i]= t; //swap
    }
    t = a[lo]; a[lo] = a[j]; a[j]= t;//swap
    return j;
}

void quicksort(int a[], int lo, int hi) {
    int j;
    if (hi <= lo) return;
    j = partition(a, lo, hi);
    quicksort(a, lo, j-1);
    quicksort(a, j+1, hi);
}

int main()  {
    int len;
    for (len = 32000;len < 40000;len+=100) {
        printf("New Arr with len = %d\n",len);
        int *arr;
        arr = (int*) calloc(len,sizeof(int));
        int j;
        //create descending Array
        for (j = 0; j < len; ++j) {
            arr[j] = len-j;
        }
        printf("start sorting\n");
        quicksort(arr,0,len-1);
        free(arr);
    }
}

【问题讨论】:

  • 你用的是什么编译器?程序停止时错误是否提到堆栈溢出?之所以问,是因为递归程序的一个典型问题是堆栈溢出。
  • calloc 可能会失败。
  • 降序数组是带有第一个元素枢轴的“升序”快速排序的最坏情况输入;这是它具有二次复杂性的地方,你很可能递归太深。您需要一种不同的枢轴选择方法。
  • 如果@molbdnilo 确实认为递归深度是问题所在(这很合理),那么改变枢轴选择的替代方法是切换到半递归算法。也就是说,不是递归地对两个子分区进行排序,而是递归地对较小的子分区进行排序,而只是循环回对较大的子分区进行排序。在不利的情况下,它仍然会有二次运行时间,但在所有情况下都会有对数递归深度和开销。

标签: c recursion quicksort


【解决方案1】:

对我来说,您的代码在更大的尺寸(约 370,000 个元素)下会失败。您可能会遇到平台限制(可能由于堆栈溢出而限制递归深度)。如果没有确切的错误信息,当然很难确定。

您的输入集可能是您实施的病态案例 - 请参阅 What makes for a bad case for quick sort?

您可以通过更好地选择枢轴来减少递归深度 - 一种常见的技术是取第一个、中心和最后一个元素的中值。像这样的:

int v0 = a[lo], v1 = a[(lo+hi+1)/2], v2 = a[hi];
/* pivot: median of v0,v1,v2 */
int v = v0 < v1 ? v1 < v2 ? v1 : v0 < v2 ? v2 : v0 : v0 < v2 ? v0 : v1 < v2 ? v2 : v1;

您还可以通过仅对较小的分区进行递归并使用迭代来处理较大的分区来减少递归深度。您也许可以让编译器的尾调用消除器将递归转换为迭代,但如果这不起作用,您需要自己编写。比如:

void quicksort(int a[], int lo, int hi) {
    while (lo < hi) {
        int j = partition(a, lo, hi);
        if (j - lo < hi -j) {
            quicksort(a, lo, j-1);
            lo = j+1;
        } else {
            quicksort(a, j+1, hi);
            hi = j-1;
        }
    }
}

通过上述更改,我可以对超过十亿个元素的数组进行排序而不会崩溃(我必须进行一些性能改进 - 见下文 - 即便如此,也需要 17 秒)。

当您发现子数组已经排序时,您可能还想提前返回。我会把它留作练习。


附: main() 中的几个问题:

您不测试 calloc() 的结果 - 您可能应该改用 malloc(),因为无论如何您都会编写每个元素:

int *arr = malloc(len * sizeof *arr);
if (!arr) return fprintf(stderr, "allocation failed\n"), EXIT_FAILURE;

完整列表

这是我最终得到的代码:

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

int partition(int *a, int i, int j) {
    int v0 = a[i], v1 = a[(i+j+1)/2], v2 = a[j];
    /* pivot: median of v0,v1,v2 */
    int v = v0 < v1 ? v1 < v2 ? v1 : v0 < v2 ? v2 : v0 : v0 < v2 ? v0 : v1 < v2 ? v2 : v1;
    while (i < j) {
        while (a[i] < v && ++i < j)
            ;
        while (v < a[j] && i < --j)
            ;
        int t = a[j]; a[j] = a[i]; a[i]= t; //swap
    }
    /* i == j; that's where the pivot belongs */
    a[i] = v;
    return j;
}

void quicksort(int a[], int lo, int hi) {
    while (lo < hi) {
        int j = partition(a, lo, hi);
        if (j - lo < hi -j) {
            quicksort(a, lo, j-1);
            lo = j+1;
        } else {
            quicksort(a, j+1, hi);
            hi = j-1;
        }
    }
}

int main()  {
    int len = INT_MAX/2+1;
    printf("New Arr with len = %d\n",len);
    int *arr = malloc(len * sizeof *arr);
    if (!arr) return fprintf(stderr, "allocation failed\n"), EXIT_FAILURE;

    /* populate pessimal array */
    for (int j = 0; j < len; ++j) {
        arr[j] = len-j;
    }

    printf("start sorting\n");
    quicksort(arr, 0, len-1);

    /* test - is it sorted? */
    for (int i = 0;  i+1 < len;  ++i)
        if (arr[i] >= arr[i+1])
            return fprintf(stderr, "not sorted\n"), EXIT_FAILURE;
    free(arr);
}

【讨论】:

  • 我认为,问题在于递归深度(这可以解释为什么它不会在您的系统上与我的系统同时崩溃),因为只有您实现了 quicksort()功能,它没有崩溃(测试大小为 200'000)
【解决方案2】:

递归太深,无法将其存储在堆栈中。 它必须为每个级别存储int j = partition(..)。 有一些声明性技术可以最大限度地减少递归堆栈的使用。 例如携带结果作为参数。但是这个案例比我举的例子要复杂得多。

【讨论】:

    猜你喜欢
    • 2012-10-28
    • 2021-04-10
    • 1970-01-01
    • 1970-01-01
    • 2013-08-29
    • 2018-03-28
    • 2021-06-19
    • 2015-06-25
    • 2014-09-17
    相关资源
    最近更新 更多