Почему эта быстрая сортировка вызывает переполнение стека в почти отсортированных списках и отсортированных списках?

В настоящее время я пишу алгоритм быстрой сортировки на Java для сортировки случайных массивов целых чисел, а затем синхронизации их с помощью системы.nanoTime(). Размер этих массивов составляет десять степеней, начиная с 10^3 и заканчивая 10^7. Кроме того, случайные списки имеют различные свойства. Я сортирую чисто случайные списки, списки с некоторыми идентичными значениями (fewUnique), перевернутые отсортированные списки, отсортированные списки и почти отсортированные списки.

вроде работает. Он выполняет быструю сортировку рекурсивно на массив, пока ему не нужно отсортировать 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);
}

EDIT: [несуществующей]

EDIT 2:

Я заменил основные рекурсивные вызовы следующим кодом, чтобы вызвать только самый маленький из двух разделов по рекомендации EJPs, и он все еще не фиксирует вопрос.

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);
}

EDIT 3:

хорошо, теперь очевидно, что глубина рекурсии намного больше, чем я дал ей кредит для почти отсортированных и отсортированных списков. Но теперь мне нужно понять, почему это так, и почему случайные списки имели только глубину 28 для значений 10^7, но почти отсортированные и отсортированные списки имеют глубину более 3000

5 ответов


для случайного массива вы можете разделить массивные куски данных.
Но для (почти) отсортированного массива вы в основном будете разбивать по 1 элементу за раз.

таким образом, для отсортированного массива размер вашего стека будет таким же, как размер массива, в то время как для случайного массива это гораздо более вероятно, будет логарифм такого размера.

Итак, даже если случайный массив намного больше, чем почти отсортированный, неудивительно, что меньший один выбрасывает исключение, но больший-нет.

изменение кода

С точки зрения исправления, как данные указал, вы должны сначала сделать меньший раздел, чтобы ограничить рост стека. Но это само по себе не исправит проблему как 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;
        }
    }
}
.

Дон кнут в [ACP][1] предлагает всегда нажимать большую из двух секций и немедленно сортировать меньшую, чтобы ограничить рост стека. В вашем коде это соответствует рекурсивной сортировке сначала меньшего из двух разделов, затем другого.

[1]: Искусство программирования, vol III, #5.2.2 p.114.


StackOverflowError, скорее всего, связано со слишком глубокой рекурсией. С большим количеством элементов для сортировки quicksort должен выполнять больше рекурсивных вызовов quicksort () перед вводом части сортировки вставки. В какой-то момент эта рекурсия слишком глубока, и в стеке слишком много вызовов методов.

возможно, рекурсия в уже отсортированных списках приводит к более глубокой рекурсии и, следовательно, сбою раньше с меньшим количеством элементов, чем сортировка несортированного списка. Это зависит от реализация.

для неакадемических и неученых целей всегда предпочтительнее реализовать эти algs с императивным стилем вместо использования рекурсии.


проверьте, есть ли у вас длинные прогоны одинаковых элементов. Часть раздела:

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

разбивает список, содержащий те же элементы, наихудшим образом.


с отсортированным или почти отсортированным набором данных QuickSort exibits худший случай время работы O (n^2). При большом значении N дерево рекурсии уходит так глубоко, что системный стек истощается, чтобы породить дальнейшие рекурсии. Как правило, такие алгоритмы должны реализовываться с итеративным подходом вместо рекурсивного подхода.