【问题标题】:Quick Sort stackoverflow error for large arrays大型数组的快速排序堆栈溢出错误
【发布时间】:2015-11-24 01:46:20
【问题描述】:

因此,我被分配实施快速排序算法,并比较大小为 500、3500 和 80000 的数组的运行时间。这些数组填充了随机数:

(int)Math.random()*1000000

我的快速排序算法适用于大小为 500 和 3500 的数组,但是当我尝试对大小为 80000 的第三个数组进行排序时,我总是遇到 * 错误。我的其他排序算法可以很好地处理这些数组。

我的快速排序方法:

public static void quickSort(int[] a, int p, int r)
{
    if(p<r)
    {
        int q=partition(a,p,r);
        quickSort(a,p,q);
        quickSort(a,q+1,r);
    }
}

我的分区方法:

private static int partition(int[] a, int p, int r) {

    int x = a[p];
    int i = p;
    int j = r;

    while (true) {
        do {
            i++;
        } while (i < r && a[i] < x);
        do {
            j--;
        } while (j > p && a[j] > x);

        if (i < j) {
            int tmp = a[i];
            a[i++] = a[j];
            a[j--] = tmp;
        } else {
            return j;
        }
    }
}

我读到我可以简单地在 VM 选项中更改堆栈大小(不知道如何做到这一点),但这只是忽略了我的算法中的问题。是什么导致了错误?谢谢!

我的司机班:

public class Driver {

    public static void main(String[] args) {

        int[] array1 = new int[500];
        int[] array2 = new int[3500];
        int[] array3 = new int[80000];

        for(int i=0; i<array1.length; i++) {
            array1[i]=(int)(Math.random()*100000);
        }

        for(int i=0; i<array2.length; i++) {
            array2[i]=(int)(Math.random()*100000);
        }

        for(int i=0; i<array3.length; i++) {
            array3[i]=(int)(Math.random()*100000);
        }

        //~~~~~~~~~~~INSERTION~~~~~~~~~~~~~~~//

        System.out.println("INSERTION SORT:\n_______________");
        System.out.println("500 Elements: "+SortTimes.runTime(SortTimes.INSERTION,array1)+" ms");
        System.out.println("3500 Elements: "+SortTimes.runTime(SortTimes.INSERTION,array2)+" ms");
        System.out.println("80000 Elements: "+SortTimes.runTime(SortTimes.INSERTION,array3)+" ms");

        //~~~~~~~~~~~BUBBLE~~~~~~~~~~~~~~~//

        System.out.println("\n\nBUBBLE SORT:\n_______________");
        System.out.println("500 Elements: "+SortTimes.runTime(SortTimes.BUBBLE,array1)+" ms");
        System.out.println("3500 Elements: "+SortTimes.runTime(SortTimes.BUBBLE,array2)+" ms");
        System.out.println("80000 Elements: "+SortTimes.runTime(SortTimes.BUBBLE,array3)+" ms");

        //~~~~~~~~~~~MERGE~~~~~~~~~~~~~~~//

        System.out.println("\n\nMERGE SORT:\n_______________");
        System.out.println("500 Elements: "+SortTimes.runTime(SortTimes.MERGE,array1)+" ms");
        System.out.println("3500 Elements: "+SortTimes.runTime(SortTimes.MERGE,array2)+" ms");
        System.out.println("80000 Elements: "+SortTimes.runTime(SortTimes.MERGE,array3)+" ms");

        //~~~~~~~~~~~QUICK~~~~~~~~~~~~~~~//

        System.out.println("\n\nQUICK SORT:\n_______________");
        System.out.println("500 Elements: "+SortTimes.runTime(SortTimes.QUICK,array1)+" ms");
        System.out.println("3500 Elements: "+SortTimes.runTime(SortTimes.QUICK,array2)+" ms");
        System.out.println("80000 Elements: "+SortTimes.runTime(SortTimes.QUICK,array3)+" ms");
    }
}

这是我的 SortTimes 课程:

public class SortTimes {

    public final static int MERGE = 1;
    public final static int QUICK = 2;
    public final static int BUBBLE = 3;
    public final static int INSERTION = 4;

    public static double runTime(int sortMethod, int[] array) {

        double startTime;
        double endTime;

        switch(sortMethod) {
            case MERGE:
                startTime = System.currentTimeMillis();
                lab12.mergeSort(array);
                endTime = System.currentTimeMillis();
                break;

            case QUICK:
                startTime = System.currentTimeMillis();
                lab12.quickSort(array, 0, array.length-1);
                endTime = System.currentTimeMillis();
                break;

            case BUBBLE:
                startTime = System.currentTimeMillis();
                lab12.bubbleSort(array);
                endTime = System.currentTimeMillis();
                break;

            case INSERTION:
                startTime = System.currentTimeMillis();
                lab12.insertionSort(array);
                endTime = System.currentTimeMillis();
                break;

            default:
                startTime = -1;
                endTime = 0;
                break;
        }

        return endTime-startTime;
    }
}

【问题讨论】:

标签: java arrays algorithm sorting stack-overflow


【解决方案1】:

这是您的快速排序:

public static void quickSort(int[] a, int p, int r)
{
    if(p<r)
    {
        int q=partition(a,p,r);
        quickSort(a,p,q);
        quickSort(a,q+1,r);
    }
}

它可以工作,但在最坏的情况下它使用 O(r-p) 堆栈空间。这对于实际实现来说太过分了。不过,修复很简单——你在 smaller 分区上递归,然后循环到更大的分区。在较小的分区上递归意味着无论如何您都只使用 O(log(r-p)) 堆栈空间:

public static void quickSort(int[] a, int p, int r)
{
    while(p<r)
    {
        int q=partition(a,p,r);
        if (q-p <= r-(q+1))
        {
            quickSort(a,p,q);
            p=q+1;
        }
        else
        {
            quickSort(a,q+1,r);
            r=q;
        }
    }
}

编辑:所以,这是真正的快速排序实现确保在最坏情况下没有堆栈溢出的方式......

但是当你用随机数初始化一个数组时,最坏的情况永远不会发生。

你说你用(int)Math.random()*1000000初始化了数组。检查优先表!强制转换发生在乘法之前,所以它总是 0,这就是为什么你会得到最坏的情况。你想要(int)(Math.random()*1000000)

编辑: 你的分区功能也坏了。它总是将 a[p] 留在位置 p,即使它是数组中的最大元素

【讨论】:

  • 这只会导致无限循环?
  • 不,每次迭代都会增加 p 或减少 r,因此它会终止。注意:下一次迭代中的 p 和 r 正是您将传递给另一个递归调用的值。
  • 奇怪,对我来说它陷入了无限循环。在 math.random() 中添加了括号,结果仍然相同:/。到目前为止,我感谢您的帮助!
  • 我认为你的 partition() 坏了。我现在没有时间检查它,但我稍后会回来。
  • 这是一个很棒的回应;做得非常好,无论是优化还是发现他们每次都会遇到最坏的情况。
【解决方案2】:

您正在报告一个包含 80000 个元素的数组的堆栈溢出。您的代码在我的笔记本电脑上对一个 80 百万 元素数组进行排序,大约 10 秒内没有任何问题。我没有看到任何堆栈溢出错误...

如果您有一个随机输入,您应该期望最大递归深度在 log2(n) 的范围内,对于 n=80,000,000,它小于 30。 quicksort wikipedia article有更详细的分析。基本上,除非你遇到一个非常病态的情况(你所有的枢轴都很糟糕),否则你不应该期望看到如此多的递归以至于堆栈溢出。

但是,我确实必须修复代码中的几个逻辑错误才能真正获得有效的排序(我没有得到完全排序的结果)。


修复随机数生成

(int)Math.random()*1000000总是返回零。您需要在乘法之后添加另一组括号以截断(int)(Math.random()*1000000)

修复你的分区逻辑

您的分区方法几乎Hoare partitioning scheme 的逻辑相匹配。但是,您似乎有一些错误。如果您将您的代码与*的代码进行比较,您会发现一些差异。

  1. 你设置了i=pj=r,但应该是i=p-1j=r+1
  2. 您应该删除交换逻辑中的递增和递减,所以a[i++] 应该只是a[i],而a[j--] 应该只是a[j]

这是我用来测试的代码:

public class QSort {

    private static int partition(int[] a, int p, int r) {

        int x = a[p];
        int i = p-1;
        int j = r+1;

        while (true) {
            while (++i < r && a[i] < x);
            while (--j > p && a[j] > x);

            if (i < j) {
                int tmp = a[i];
                a[i] = a[j];
                a[j] = tmp;
            } else {
                return j;
            }
        }
    }

    public static void quickSort(int[] a, int p, int r) {
        if(p<r) {
            int q=partition(a,p,r);
            quickSort(a,p,q);
            quickSort(a,q+1,r);
        }
    }

    public static void main(String args[]) {
        int n = Integer.valueOf(args[0]);
        int[] xs = new int[n];
        for (int i=0; i<n; i++) {
            xs[i] = (int)(Math.random()*1000000);
        }
        quickSort(xs, 0, xs.length-1);
        for (int i=0; i<n-1; i++) {
            if (xs[i] > xs[i+1]) {
                System.out.println("ERROR");
                System.exit(-1);
            }
        }
        System.out.println("SORTED");
    }

}

【讨论】:

  • 如果你使用 | x = a[(p+r)/2]; |那么这两个时间可以是:|而 (a[++i] x); | .
  • 所以我改变了你所说的,并且该方法本身运行良好,但是当我使用我的驱动程序测试它时,它会不断出现错误并向我抛出 * 错误,该驱动程序通过对数组进行排序来工作我创建的一个计时器类,它以毫秒为单位报告运行时间。我将在我的主帖中编辑计时器类和驱动程序类。非常感谢您的宝贵时间,无论如何!
  • @SamNayerman - 我运行了您发布的代码,它对我来说仍然可以正常工作(即使使用驱动程序代码)。但是,我注释掉了所有其他排序类型(因为我只有快速排序代码)。您确定错误来自快速排序吗?即使手动将 JVM 堆栈大小设置为 160k(这是它在我的机器上可以接受的最小值),我仍然可以对 8000 万个元素的数组进行排序而不会出错。
  • @DaoWen 这真的很奇怪,因为当我注释掉所有其他排序类型时它也适用于我,并且只在与其他排序类型一起运行时抛出错误。我真的很难过
【解决方案3】:

如果 quickSort() 仍然使用 pivot x = a[p],而不是 x = a[(p+r)/2],那么如果数据已经排序,quickSort() 可能会出现堆栈溢出。您是否有可能对已按先前排序排序的数据运行 quickSort()?

【讨论】:

  • 我很惭愧地承认是这种情况......我没有意识到我的驱动程序在初始排序后从未对数组进行随机化。非常感谢!
【解决方案4】:

由于这个程序是递归的,并且 java 在递归中的内存效率不高,所以请尝试增加 IDE 为您的程序分配的内存量。

由于您使用的是 Intellij,请尝试按以下方式增加内存。

更多信息:https://www.jetbrains.com/idea/help/run-debug-configuration-application.html

【讨论】:

  • 增加堆大小不会阻止堆栈溢出——它是内存的不同部分。另外,我认为建议增加堆限制以避免学习如何编写适当的快速排序并不是一个好建议。
  • @MattTimmermans:我同意你的观点,但每个优化算法都有最低内存要求。我们应该满足内存要求以运行该算法而不会失败。根据我在使用大型数据集测试算法时的经验,默认的 IDE 内存参数不是正确的值。
【解决方案5】:

大型数组上的递归快速排序算法会导致 * 错误。

尝试this link 中的非递归方法。以下Java代码由原始c代码转换而来,希望对您有所帮助。

static final int MAX_LEVELS = 1000;
public static boolean quickSort(int[] arr, int elements) {
    int i=0,L,R,pivot;
    int[] beg = new int[MAX_LEVELS], end = new int[MAX_LEVELS];
    beg[0]=0;
    end[0]=elements;
    while(i>=0) {
        L=beg[i];
        R=end[i]-1;
        if(L<R) {
            pivot=arr[L]; if(i==MAX_LEVELS-1) return false;
            while(L<R) {
                while(arr[R]>=pivot&&L<R) R--; if(L<R) arr[L++]=arr[R];
                while(arr[L]<=pivot&&L<R) L++; if(L<R) arr[R--]=arr[L];
            }
            arr[L]=pivot;
            beg[i+1]=L+1;
            end[i+1]=end[i];
            end[i++]=L;
        } else {
            i--;
        }
    }
    return true;
}
// an example
public static void main(String[] args) {
    // construct the integer array
    int[] arr = new int[80000];
    for(int i=0;i<arr.length;i++) {
        arr[i]=(int)Math.random()*100000;
    }

    // sort the array
    quickSort(arr, arr.length);
}

它既省时又不受 * 影响。

【讨论】:

  • 这个赋值必须是递归的。
【解决方案6】:

您输入的数组是否已排序?您是否将排序后的数组传递给快速排序?

public class quickSortTest {
public static void main(String[] args) {
    int max = 800000;
    int[] array = new int[max];
    for (int i = 0; i < max; ++i) {
        array[i] = (int) Math.random() * 1000000;
    }
    long start = System.currentTimeMillis();
    quickSort(array, 0, max - 1);
    System.out.println("time:"+(System.currentTimeMillis()-start));
    System.out.println(testSortResult(array));
}

public static boolean testSortResult(int[] array){
    for(int i=1;i<array.length;++i){
        if(array[i]<array[i-1]){
            return false;
        }
    }
    return true;
}

public static void quickSort(int[] a, int p, int r) {
    if (p < r) {
        int q = partition(a, p, r);
        quickSort(a, p, q);
        quickSort(a, q + 1, r);
    }
}

private static int partition(int[] a, int p, int r) {

    int x = a[p];
    int i = p;
    int j = r;

    while (true) {
        do {
            i++;
        } while (i < r && a[i] < x);
        do {
            j--;
        } while (j > p && a[j] > x);

        if (i < j) {
            int tmp = a[i];
            a[i++] = a[j];
            a[j--] = tmp;
        } else {
            return j;
        }
    }
}
}

我测试了你的代码,即使数组长度为 800000 也可以。

【讨论】:

  • 它们未排序。只要元素的数量不太大,该方法就可以正常工作。
  • 我测试了你的代码,没关系,你也可以粘贴完整的代码。
  • 不行。原始代码的问题只发生在最坏的情况下,随机数组不太可能发生这种情况。但是,如果您发布这样的代码,那么它在某天使用真实数据发生在某些客户的计算机上。