Big O - Bubble сорт

Я написал две разные версии алгоритма Bubble Sort- bubbleSort, традиционная версия алгоритма, который вы увидите в учебниках, и sortIntArray, что очень похоже на bubbleSort но рекурсивно

Возможно, это мое недоразумение, но последний вызывающий алгоритм заставляет меня думать, что эффективность алгоритма отличается от пузырьковой сортировки. Может кто-нибудь объяснить мне разницу между этими двумя, если таковые имеются?

private static int[] sortIntArray(int[] arr) {

    for(int i=0; i<arr.length-1; i++) {
        if(arr[i+1]<arr[i]) { // [i-2], [i-1], [i], [i+1], [i+2]
            printArr(arr);
            int temp = arr[i];
            arr[i] = arr[i+1];
            arr[i+1] = temp;
            sortIntArray(arr);
        }
    }
    return arr;
}

private static int[] bubbleSort(int[] arr) {

    boolean swap = false;
    while(!swap) {
        swap = true;
        for(int i = 0; i<arr.length-1; i++) {
            if(arr[i+1]< arr[i]) {
                printArr(arr);
                swap = false;
                int temp = arr[i];
                arr[i] = arr[i+1];
                arr[i+1] = temp;
            }
        }
    }
    return arr;
}

/**
 * Returns an unordered integer array.
 * @return
 */
private static int[] getIntArray() {
    return new int[]{54, 899, 213, 2, 43, 8, 12, 11, 111, 43, 6, 44, 83, 3458};
}

2 ответа

Решение

Я добавил статическую переменную-счетчик, которую я увеличивал с каждым проходом самого внутреннего цикла в каждом из ваших методов. Так что для sortIntArray выдержка со счетчиком будет такой:

for(int i=0; i<arr.length-1; i++) {
        o++;
        if(arr[i+1]<arr[i]) { // [i-2], [i-1], [i], [i+1], [i+2]
            ...

Я также реализовал другой метод, который по сути представлял собой Bubble, предоставленный в Википедии, и я изменил ваш генератор случайных массивов для создания массивов разных размеров:

private static int[] getIntArray() {
    int[] rand;
    if (MAX_SIZE - MIN_SIZE > 0) {
        rand = new int[new Random().nextInt(MAX_SIZE - MIN_SIZE) + MIN_SIZE + 1];
    } else {
        rand = new int[MIN_SIZE];
    }
    for (int i = 0; i < rand.length; i++) {
        rand[i] = new Random().nextInt(rand.length * 2);
    }
    return rand;
}

Вот пример результата, когда список имеет размер 50. Если вы создадите тестовый код самостоятельно, вы увидите, что реализация Wikipedia не только гарантированно сортирует, но и обеспечивает хороший тест:

Unsorted: [52, 48, 62, 47, 42, ...]
n: 50
Bubble sort: [3, 4, 6, 6, 11, ...]
O(n): 1960
Wikipedia sort: [3, 4, 6, 6, 11, ...]
O(n): 2450
Recursive sort: [3, 4, 6, 6, 11, ...]
O(n): 27391

Интересно, что ясно видно, что рекурсивная сортировка выполняет больше итераций, чем другие алгоритмы сортировки. На основании данных из 10000 сортов, которые я сбросил в CSV, также может показаться, что при максимальном размере 30 коэффициент n^2-члена равен apx. 11.3 для полинома строгого порядка 2, тогда как для максимального размера 50 он увеличился примерно до 19, что указывает на то, что не существует такого m, что m * n^2 является верхней границей сложности вашего алгоритма во время выполнения.

tl; dr: эмпирически рекурсивный алгоритм хуже, чем O(n^2) во времени.

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

Ваши функции эмулируют следующую итеративную версию (которая фактически является сортировкой вставки):

for (int i = 1; i < a.size(); ++i) {
    for (int j = i; j >= 1; --j) {
        if (a[j] < a[j - 1]) {
            int tmp = a[j];
            a[j] = a[j - 1];
            a[j - 1] = tmp;
        }
        else {
            break;
        }
    }
}

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

Подумайте об этом так: для каждой инверсии вы сталкиваетесь с рекурсивным вызовом. Если в какой-либо точке цикла у вас есть начальная отсортированная часть массива, например, пусть этот вызов функции будет f_1
2 3 7. 1 4 5 6

Где . указывает, что массив отсортирован до этой точки. Теперь обратите внимание, что когда цикл достигает 7 и 1, он меняет местами и рекурсивно вызывает функцию, и это повторяется, так что 1 в конце концов оказывается впереди (скажем, вызов, в котором это происходит, f_2), и массив становится:

1 2 3 7. 4 5 6

Обратите внимание, что f_2 находится на большей глубине, чем f_1, С этой точки зрения, f_2 не возвращается, но цикл продолжается от 1, что эффективно эмулирует f_1, но что большая часть отсортирована, что означает, что при вызове f_2 возвращает, весь массив будет отсортирован. И, следовательно, дополнительные рекурсивные вызовы перед f_2 больше ничего не делайте, но занимайте место в стеке.

Пространственная сложность вашего алгоритма наихудшая O(n^2)и сложность по времени O(n^3), которая менее эффективна, чем оригинал (Для сложности по времени рассмотрим обратно отсортированный массив размера n у которого есть n^2 инверсии, таким образом n^2 рекурсивные вызовы, каждый из которых пересекает весь массив, таким образом O(n^3)).

Другие вопросы по тегам