Реализация алгоритма быстрой сортировки в 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. Шаги алгоритма
-
Мы выбираем элемент из списка, называемый сводной. Мы будем использовать его, чтобы разделить список на два подсписка.
-
Мы переупорядочиваем все элементы вокруг стержня - перед ним располагаются элементы с меньшим значением, а все элементы больше, чем стержень после него. После этого шага стержень находится в своем окончательном положении. Это важный шаг раздела.
-
Мы применяем вышеупомянутые шаги рекурсивно к обоим подспискам слева и справа от оси.
Как видим,quicksort is naturally a recursive algorithm, like every divide and conquer approach.
Давайте рассмотрим простой пример, чтобы лучше понять этот алгоритм.
Arr[] = {5, 9, 4, 6, 5, 3}
-
Предположим, мы выбрали 5 в качестве точки поворота для простоты.
-
Сначала мы поместим все элементы меньше 5 в первую позицию массива: \ {3, 4, 5, 6, 5, 9}
-
Затем мы повторим это для левого подмассива \ {3,4}, взяв 3 в качестве точки поворота.
-
Нет элементов меньше 3
-
Мы применяем быструю сортировку в под-массиве справа от оси, т.е. {4}
-
Этот подмассив состоит только из одного отсортированного элемента
-
Мы продолжаем с правой части исходного массива, \ {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.