簡體   English   中英

為什么這種快速排序會導致幾乎排序的列表和排序列表上的堆棧溢出?

[英]Why does this quick sort cause stack overflow on nearly sorted lists and sorted lists?

我目前正在用Java編寫快速排序算法來對隨機的整數數組進行排序,然后使用System.nanoTime()對它們進行計時。 這些數組的大小是10的冪,從10 ^ 3開始到10 ^ 7結束。 此外,隨機列表具有不同的屬性。 我正在整理純粹的隨機列表,列表中包含一些相同的值(很少),反向排序列表,排序列表和幾乎排序的列表。

排序有效。 它在數組上遞歸執行快速排序,直到它需要排序30個元素或更少的數組,在這種情況下,它執行插入排序。

一切都很好10 ^ 3和10 ^ 4但是一旦我達到10 ^ 5的值,它只會對隨機,很少的唯一和隨機列表進行排序,但在排序幾乎排序和排序的列表時會產生堆棧溢出錯誤。

我不相信問題在於生成列表的方式,因為堆棧溢出發生在排序算法中(編譯器引用的行是findPivot()方法中的第一行)。 此外,它通常會在崩潰之前對1到6個列表進行排序。 因此,必須以某種方式使算法本身與幾乎排序和排序的列表進行交互。 此外,反向列表的生成涉及調用用於創建排序列表的代碼(然后將其反轉)。

此外,我發現該問題不太可能只是由於某種原因,算法必須通過在幾乎排序和排序的列表中遞歸而不是其他列表類型來調用數組的部分分區,如它可以對10 ^ 7值的隨機列表進行排序,這將需要比具有10 ^ 5值的近似排序列表更多的分區。

我意識到它必須與這些列表類型如何與我的快速排序的遞歸相互作用有關,但如果有人可以闡明它會很棒。 我把代碼放到了完整的快速排序算法和下面的隨機列表生成器中。

快速排序算法

/**
 * Performs a quick sort given the indexes of the bounds of an integer array
 * @param arr The array to be sorted
 * @param highE The index of the upper element
 * @param lowE The index of the lower element
 */
public static void quickSort(int[] arr, int highE, int lowE)
{       
    //Only perform an action if arr.length > 30, otherwise insertion sort [recursive base case])
    if (lowE + 29 < highE)
    {
        //Get the element and then value of the pivot
        int pivotE = findPivot(arr, highE, lowE);
        int pivotVal = arr[pivotE], storeE = lowE;

        //Swap the pivot and the last value.
        swapElements(arr, pivotE, highE);

        //For each element in the selection that is not the pivot, check to see if it is lower than the pivot and if so, move it to the leftmost untouched element.
        for (int i = lowE; i < highE; i++)
        {
            if (arr[i] < pivotVal)
            {
                swapElements(arr, storeE, i);

                //Increment storeE so that the element that is being switched moves to the right location
                storeE++;
            }
        }

        //Finally swap the pivot into its proper position and recrusively call quickSort on the lesser and greater portions of the array
        swapElements(arr, storeE, highE);                   
        //Lesser
        quickSort(arr, storeE - 1, lowE);
        //Greater
        quickSort(arr, highE, storeE + 1);
    }
    else
    {
        insertSort(arr, highE, lowE);
    }
}




/**
 * Finds the pivot element
 * @param arr The array to be sorted
 * @param highE The index of the top element
 * @param lowE The index of the bottom element
 * @return The index of the pivot.
 */
public static int findPivot(int[] arr, int highE, int lowE)
{
    //Finds the middle element
    int mid = (int) Math.floor(lowE + (highE - lowE) / 2);

    //Returns the value of the median of the first, middle and last elements in the array.
    if ((arr[lowE] >= arr[mid]) && (arr[lowE] >= arr[highE])) 
    {
        if (arr[mid] > arr[highE]) {return mid;}
        else {return highE;}
    }
    else if ((arr[mid] >= arr[lowE]) && (arr[mid] >= arr[highE])) 
    {
        if (arr[lowE] > arr[highE]) {return lowE;}
        else {return highE;}
    }
    else 
    {
        if (arr[lowE] > arr[mid]) {return lowE;}
    }

    return mid;
}




/**
 *Performs an insertion sort on part of an array
 * @param arr The array to be sorted.
 * @param highE The index of the top element.
 * @param lowE The index of the low element.
 */
public static void insertSort(int[] arr, int highE, int lowE)
{
    //Sorts elements lowE to i in array, with i being gradually incremented.
    for (int i = lowE + 1; i <= highE; i++)
    {
        for (int j = i; j > lowE; j--)
        {
            if (arr[j] < arr[j - 1])
            {
                swapElements(arr, j, j-1);
            }
            else {break;}
        }
    }
}

隨機列表發電機

/**
 * Creates a random list
 * @param arr The array to place the list inside of
 */
public static void randomList(int[] arr)
{
    //Places a random number at each element of the array

    for (int i = 0; i < arr.length; i++)
    {
        arr[i] = (int) Math.floor(Math.random() * RAND_MAX);
    }
}




/**
 * Creates a nearly sorted list of random numbers
 * @param arr the array to place the list inside of
 */
public static void nearSortList(int[] arr)
{
    //Creates a sorted list in arr
    sortList(arr);



    int swaps = (int) (Math.ceil(Math.pow((Math.log(arr.length)), 2.0)));

    //The two values to be switched each time
    int a, b;

    //Performs a number of swaps equal to swaps [log(N) ^ 2] rounded up, with numbers switched no more than ln(N) places away
    for (int i = 0; i < swaps; i++)
    {
        a = (int) Math.floor(Math.random() * arr.length);

        b = (int) (a + Math.random() * 2 * Math.log(arr.length) - Math.log(arr.length));

        //Accounts for cases in which b is either greater or smaller than the array bounds
        if (b < 0)
        {
            b = -b;
        }
        else if (b >= arr.length)
        {
            b = -1 * (arr.length - b);
        }

        swapElements(arr, a, b);
    }
}




/**
 * Creates a random list with many unique values in
 * @param arr the array to place the list inside of
 */
public static void fewUniqueList(int[] arr)
{
    int[] smallArr = new int[(int) Math.floor(Math.pow(arr.length, 9.0 / 10.0))];


    //Creates a smaller array of random values
    randomList(smallArr);



    //Fills the larger list up with values from the smaller list, ensuring aproximately N / N ^ (9/10) instances of each number in the array and ensuring, at most, there are N ^ (9/10) (rounded down) unique values in the large array
    for (int i = 0; i < arr.length; i++)
    {
        arr[i] = smallArr[(int) Math.floor(Math.random() * smallArr.length)];
    }
}




/**
 * Creates a reversed list of random numbers
 * @param arr the array to place the list inside of
 */
public static void reversedList(int[] arr)
{
    //Creates a sorted list in arr
    sortList(arr);




    //Switches each ith elements with its mirror on the other end of the array until the value of i reaches the middle of the array
    for (int i = 0; i < (int) (arr.length / 2.0); i++)
    {
        swapElements(arr, i, arr.length - 1 - i);
    }
}




/**
 * Creates a sorted list of random numbers using a merge sort
 * @param arr the array to place the list inside of
 */
public static void sortList(int[] arr)
{
    //Creates a random list in arr
    randomList(arr);

    Arrays.sort(arr);
}

編輯: [已解散]

編輯2:

我已經使用以下代碼替換了基本的遞歸調用,以便僅在EJP建議時調用兩個分區中的最小分區,但仍然沒有解決問題。

if (storeE - 1 - lowE < highE - storeE + 1)
{
    //Lesser
    quickSort(arr, storeE - 1, lowE);
    //Greater
    quickSort(arr, highE, storeE + 1);
}
else
{
    //Greater
    quickSort(arr, highE, storeE + 1);
    //Lesser
    quickSort(arr, storeE - 1, lowE);
}

編輯3:

好吧,現在顯而易見的是,遞歸深度遠遠大於我對於近乎排序和排序的列表所給予的信任。 但現在我需要弄清楚為什么會出現這種情況,以及為什么隨機列表的深度僅為10 ^ 7的值,但幾乎排序和排序的列表的深度超過3000

[ACP] [1]中的Don Knuth建議始終推動兩個分區中的較大分區並立即對較小分區進行排序,以限制堆棧增長。 在您的代碼中,對應於先遞歸排序兩個分區中較小的一個,然后是另一個。

[1]: 計算機程序設計的藝術,第三卷,#5.2.2第114頁。

對於隨機數組,您可以對大量數據進行分區。
但是對於一個(幾乎)排序的數組,你一次主要是分割1個元素。

因此,對於排序數組,您的堆棧大小最終會與數組的大小相同,而對於隨機數組,它更可能是該大小的對數。

因此,即使隨機數組比近似排序的數組大得多,因此較小的一個引發異常也就不足為奇了,但較大的一個則不然。

修改你的代碼

就修復方面而言,正如EJP指出的那樣 ,您應首先執行較小的分區以限制堆棧增長。 但這本身不會解決問題,因為Java不支持尾調用優化 (嗯,它是可選的實現,因為我理解這個問題)。

這里一個相當簡單的解決方法是將函數拋入while循環,本質上是對尾調用優化進行硬編碼。

為了更好地了解我的意思:

public static void quickSort(int[] arr, int highE, int lowE)
{
    while (true)
    {
        if (lowE + 29 < highE)
        {
            ...
            quickSort(arr, storeE - 1, lowE);

            // not doing this any more
            //quickSort(arr, highE, storeE + 1);

            // instead, simply set the parameters to their new values
            // highE = highE;
            lowE = storeE + 1;
        }
        else
        {
            insertSort(arr, highE, lowE);
            return;
        }
    }
}

好吧,既然你已經有了基本的想法,那么看起來會更好(功能上與上面相同,只是更簡潔):

public static void quickSort(int[] arr, int highE, int lowE)
{
    while (lowE + 29 < highE)
    {
        ...
        quickSort(arr, storeE - 1, lowE);
        lowE = storeE + 1;
    }
    insertSort(arr, highE, lowE);
}

這當然實際上並不首先做較小的一個,但我會留給你弄清楚(似乎你已經對如何做到這一點有了一個很好的想法)。

這是如何工作的

對於一些彌補價值......

您當前的代碼執行此操作:(縮進表示該函數調用內發生的事情 - 因此增加縮進意味着遞歸)

quickSort(arr, 100, 0)
   quickSort(arr, 49, 0)
      quickSort(arr, 24, 0)
         insertion sort
      quickSort(arr, 49, 26)
         insertion sort
   quickSort(arr, 100, 51)
      quickSort(arr, 76, 0)
         insertion sort
      quickSort(arr, 100, 74)
         insertion sort

修改后的代碼執行此操作:

quickSort(arr, 100, 0)
   quickSort(arr, 49, 0)
      quickSort(arr, 24, 0)
         break out of the while loop
         insertion sort
   lowE = 26
   break out of the while loop
      insertion sort
lowE = 51
run another iteration of the while-loop
    quickSort(arr, 76, 0)
      break out of the while loop
      insertion sort
lowE = 74
break out of the while loop
   insertion sort

增加堆棧大小

不確定您是否考慮過這一點,或者它是否適用於您的參數,但您始終可以考慮使用-Xss命令行參數增加堆棧大小

StackOverflowError很可能與遞歸過深有關。 使用更多元素對快速排序進行排序必須在進入插入排序部分之前對quicksort()進行更多遞歸調用。 在某些時候,這種遞歸太深,並且堆棧上的方法調用太多。

可能是已經排序的列表上的遞歸導致更深的遞歸,因此比排序未排序的列表更少的元素崩潰。 這取決於實施。

對於非學術和非學習目的,總是優選使用命令式樣式而不是使用遞歸來實現這些algs。

檢查您是否有相同元素的長期運行。 分區部分:

for (int i = lowE; i < highE; i++)
{
    if (arr[i] < pivotVal)
    {
        swapElements(arr, storeE, i);
        storeE++;
    }
}

以最糟糕的方式划分包含相同元素的列表。

對於排序或接近排序的數據集,QuickSort表示O(n ^ 2)的最壞情況運行時間。 對於N的大值,遞歸樹變得如此之深以致系統堆棧耗盡以產生進一步的遞歸。 通常,這種算法應該用迭代方法而不是遞歸方法來實現。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM