在最佳情况下,由于堆栈溢出错误,快速排序算法失败 - C++

Posted

技术标签:

【中文标题】在最佳情况下,由于堆栈溢出错误,快速排序算法失败 - C++【英文标题】:QuickSort algorithm fails due to stack overflow error in best case - C++ 【发布时间】:2014-03-22 18:21:08 【问题描述】:

我尝试实现的快速排序算法有问题。

我参加了基础算法课程,我们为实验室作业提供了用于实现各种算法的伪代码。这些算法取自 Cormen 并被同化为 C++ 语言,我们应该验证效率并为其中的分配和比较的数量生成图表。

现在的问题:

下面的代码应该对一个包含 10000 个数字的数组进行快速排序,并在最佳情况下使用它(数组的轴始终位于中间):

int partition(int *a, int p, int r) 
    int x = a[r];
    countOpQS++;
    int index = p - 1;
    for (int count = p; count <= (r - 1); count++) 
        if (a[count] <= x) 
            index += 1;
            swap(a[index], a[count]);
            countOpQS += 3;
        
        countOpQS++;
    
    swap(a[index + 1], a[r]);
    countOpQS += 3;
    return (index + 1);


int select(int *a, int p, int r, int index) 
    if (p == r) 
        return a[p];
    
    int q;
    q = partition(a, p, r);
    //countOpQS++;
    int k = q - p + 1;
    if (index <= k) 
        return select(a, p, q - 1, index);
     else 
        return select(a, q + 1, r, index - k);
    



void bestQuickSort(int *a, int p, int r) 
    if (p < r) 
        select(a, p, r, (r - p + 1) / 2);
        bestQuickSort(a, p, (r - p + 1) / 2);
        bestQuickSort(a, ((r - p + 1) / 2) + 1, r);
    

main函数中的调用是由:

for (index = 100; index <= 10000; index += 100) 
        countOpQS = 0;
        for (int k = 0; k < index; k++) 
            a[k] = rand();
        
        bestQuickSort(a, 1, index);
        out3 << index << ", " << countOpQS << "\n";
    

使用这些方法应该是可行的,但它在运行时会很快跳入堆栈溢出。我什至在 Visual Studio 中提出了保留堆栈,因为它在进入可能的最坏情况时是必要的(已经有序的数组,随机枢轴)。

你们知道为什么它不起作用吗?

【问题讨论】:

您没有显示数组(或向量)a 的定义位置,但由于您使用 0..index-1 范围内的索引对其进行了初始化,这表明它包含 index 元素。 .. 请注意,您将index 传递给bestQuickSort,后者将其作为r 传递给select,然后传递给partition,后者随后引用了a[r],它是a[index],它是末尾的一个你的数组/向量。我会先解决这个问题。 @amdn 数组 a 在主函数中声明。 for 循环只是生成数字并将它们输出到 .csv 文件中。现在有必要吗?可以这样想:我正在获取一个包含 100 个元素(从 0 到 99)的数组。调用中的数字将只是要排序的数组中第一个和最后一个元素的索引... Best Case scenario (taking the pivot of the array always at the middle) 请注意,这只是输入已经排序(或反向排序)的最佳情况 是的,这是我的一个错误。上面的代码是单独保存的。内部 for 语句看起来像' a[k] = k ',使其已经排序。所以还是一样。 它的深度不应超过 14 层,并且每向下一层应将阵列削减一半左右。在调试器中单步执行。 【参考方案1】:

首先,你应该知道你的函数select() 重新排列了[p, r] 范围内的元素,使得第一个索引处的元素(注意索引是从一开始的! ) 位置是在排序序列中处于该位置的元素,就像std::nth_element 所做的那样。 因此,当您通过select(a, p, r, (r - p + 1) / 2); 选择子数组的中值元素时,中值的索引基于p。 例如:p = 3, r = 5,所以(r - p + 1) / 2为1,中位数会放在a[4],也就是说你应该这样调用函数:select(a, 3, 5, 2)。之后,您只需像这样调用bestQuickSort()

    bestQuickSort(a, p, (r - p + 1) / 2); // (r - p + 1) / 2 is 1 now!
    bestQuickSort(a, ((r - p + 1) / 2) + 1, r);

当然不行!整个代码是:

int select(int *a, int p, int r, int index) 
    if (p == r) 
        return a[p];
    
    int q;
    q = partition(a, p, r);
    //countOpQS++;
    int k = q - p + 1;
    if(k== index)
        return a[q];
    else if (index <= k) 
        return select(a, p, q - 1, index);
     else 
        return select(a, q + 1, r, index - k);
    


void bestQuickSort(int *a, int p, int r) 
    if (p < r) 
        select(a, p, r, (r - p + 1) / 2 + 1); // the index passed to select is one-based!

        int midpoint = p + (r - p + 1) / 2; 
        bestQuickSort(a, p, midpoint - 1);
        bestQuickSort(a, midpoint + 1, r);
    

顺便说一句,您的快速排序版本并不总是在最佳情况下运行,尽管每次您选择(子)数组的确切中位数,但 select 的时间复杂度并不总是 O(n),因为您只需选择a[r] 作为支点,select 的最坏情况性能是二次的:O(n*n)。

【讨论】:

现在我并不是说你在上面所说的对我来说是某种逻辑,但是你的代码让我遇到了比最坏情况更糟糕的情况......你看到的那个计数器是计算函数内完成的分配和操作。对于已经排序的数组,它给我的值比最坏情况高 4-5 倍。 你如何计算最坏情况下的作业?我没有修改你程序的逻辑,只是修复了一些错误。您应该在主函数中将基于 0 的索引传递给 bestQuickSort(),所以它是 bestQuickSort(a, 0, index-1) 有些人喜欢在将数组传递给 QuickSort 之前(随机)打乱他们的数组,以避免“已经排序”或“几乎排序”的情况。此外,qsort 的标准实现根据数组大小使用不同的中值选择启发式算法,对于较小的数组,它们甚至会切换到冒泡排序。现在关于堆栈溢出,一个优秀的编译器将通过将递归转换为迭代来消除实际的递归堆栈。例如:***.com/questions/310974/… @v.oddou 你说的与OP的问题无关,堆栈溢出是由于错误的代码导致无限递归。 @jfly:完全正确。就像 Mike Dunlavey 所说的那样,递归深度应该在 log(N) 周围浮动,所以非常小。

以上是关于在最佳情况下,由于堆栈溢出错误,快速排序算法失败 - C++的主要内容,如果未能解决你的问题,请参考以下文章

由于递归方法调用导致 Java 堆栈溢出

在某些情况下,我的快速排序实现失败

堆栈溢出相关问题算法[关闭]

快速排序的 C++ 实现中的运行时错误

“就地”MSD 基数排序、堆栈空间和堆栈溢出

人生算法之“快速排序”