Реализация алгоритма быстрой сортировки в Java

Реализация алгоритма быстрой сортировки в Java

1. обзор

В этом руководстве мы подробно рассмотрим алгоритм QuickSort, сосредоточив внимание на его реализации на Java.

Мы также обсудим его преимущества и недостатки, а затем проанализируем его временную сложность.

2. Алгоритм быстрой сортировки

Quicksort is a sorting algorithm, which is leveraging the divide-and-conquer principle.  Он имеет среднюю сложностьO(n log n) и является одним из наиболее часто используемых алгоритмов сортировки, особенно для больших объемов данных.

It’s important to remember that Quicksort isn’t a stable algorithm. Стабильный алгоритм сортировки - это алгоритм, в котором элементы с одинаковыми значениями появляются в отсортированном выводе в том же порядке, что и во входном списке.

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

Наконец, все отсортированные подсписки объединяются, чтобы сформировать окончательный результат.

2.1. Шаги алгоритма

  1. Мы выбираем элемент из списка, называемый сводной. Мы будем использовать его, чтобы разделить список на два подсписка.

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

  3. Мы применяем вышеупомянутые шаги рекурсивно к обоим подспискам слева и справа от оси.

Как видим,quicksort is naturally a recursive algorithm, like every divide and conquer approach.

Давайте рассмотрим простой пример, чтобы лучше понять этот алгоритм.

Arr[] = {5, 9, 4, 6, 5, 3}
  1. Предположим, мы выбрали 5 в качестве точки поворота для простоты.

  2. Сначала мы поместим все элементы меньше 5 в первую позицию массива: \ {3, 4, 5, 6, 5, 9}

  3. Затем мы повторим это для левого подмассива \ {3,4}, взяв 3 в качестве точки поворота.

  4. Нет элементов меньше 3

  5. Мы применяем быструю сортировку в под-массиве справа от оси, т.е. {4}

  6. Этот подмассив состоит только из одного отсортированного элемента

  7. Мы продолжаем с правой части исходного массива, \ {6, 5, 9}, пока не получим окончательный упорядоченный массив

2.2. Выбор оптимального разворота

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

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

3. Реализация на Java

Первый метод -quickSort(), который принимает в качестве параметров сортируемый массив, первый и последний индекс. Во-первых, мы проверяем индексы и продолжаем работу, только если есть еще элементы для сортировки.

Мы получаем индекс отсортированной сводной таблицы и используем его для рекурсивного вызова методаpartition() с теми же параметрами, что и методquickSort(), но с другими индексами:

public void quickSort(int arr[], int begin, int end) {
    if (begin < end) {
        int partitionIndex = partition(arr, begin, end);

        quickSort(arr, begin, partitionIndex-1);
        quickSort(arr, partitionIndex+1, end);
    }
}

Продолжим методpartition(). Для простоты эта функция принимает последний элемент в качестве оси. Затем проверяет каждый элемент и меняет его перед точкой, если его значение меньше.

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

private int partition(int arr[], int begin, int end) {
    int pivot = arr[end];
    int i = (begin-1);

    for (int j = begin; j < end; j++) {
        if (arr[j] <= pivot) {
            i++;

            int swapTemp = arr[i];
            arr[i] = arr[j];
            arr[j] = swapTemp;
        }
    }

    int swapTemp = arr[i+1];
    arr[i+1] = arr[end];
    arr[end] = swapTemp;

    return i+1;
}

4. Алгоритм анализа

4.1. Сложность времени

В лучшем случае алгоритм разделит список на два подсписка одинакового размера. Итак, для первой итерации полного списка размеромn требуетсяO(n). Сортировка оставшихся двух подсписок с элементамиn/2 занимает2*O(n/2) каждый. В результате алгоритм QuickSort имеет сложностьO(n log n).

В худшем случае алгоритм будет выбирать только один элемент на каждой итерации, поэтомуO(n) + O(n-1) + … + O(1), что равноO(n2).

В среднем QuickSort имеет сложностьO(n log n), что делает его подходящим для больших объемов данных.

4.2. QuickSort против MergeSort

Давайте обсудим, в каких случаях следует выбирать QuickSort вместо MergeSort.

Хотя и Quicksort, и Mergesort имеют среднюю временную сложностьO(n log n), Quicksort является предпочтительным алгоритмом, так как он имеет пространственную сложностьO(log(n)). С другой стороны, сортировка слиянием требует дополнительного хранилищаO(n), что делает его довольно дорогим для массивов.

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

В этом случае увеличение объема служебной информации для быстрой сортировки и слияния обычно является предпочтительным.

5. Заключение

Quicksort - это элегантный алгоритм сортировки, который в большинстве случаев очень полезен.

Как правило, это алгоритм «на месте» со средней временной сложностьюO(n log n).с.

Еще один интересный момент, который следует упомянуть, заключается в том, что метод JavaArrays.sort() использует Quicksort для сортировки массивов примитивов. Реализация использует две точки поворота и работает намного лучше, чем наше простое решение, поэтому для производственного кода обычно лучше использовать библиотечные методы.

Как всегда, код для реализации этого алгоритма можно найти вon our GitHub repository.