04.06.160 Быстрая сортировка. Рандомизировання версия - justjune/AiSD2026 GitHub Wiki
Анализ алгоритма быстрой сортировки.
Быстрая сортировка работает следующим образом: У нас есть массив из n чисел.
1.) Мы выбираем случайное число из массива ( допустим это число p).
2.) Оставшиеся числа мы отсортировываем на две группы: левую (те что меньше p) и правую (и те что больше ).
-Благодаря этому шагу, элемент p теперь стоит там же, где он будет стоять в отсортированном массиве.
-К тому же мы получаем два массива, которые можно сортировать независимо друг от друга. ( поэтому дальше используем рекурсию)
3.) Рекурсивно отсортировываем эти 2 массива по тому же принципу.
Листинг 4.12. Код для алгоритма быстрой сортировки (стр. 161):
void quicksort(item_type s[], int l, int h) {
int p; /* Индекс элемента-разделителя */
if (l < h) { //если в нашем массиве больше одного элемента
p = partition(s, l, h); //берём наш элемент p и сортируем массив на две части
quicksort(s, l, p - 1); //дальше сортируем по этому принципу левую часть
quicksort(s, p + 1, h); //после правую
}
}
Массив можно разбить на две части относительного опорного элемента p за за один проход и линейное время O(n) с использованием трёх переменных:
firsthigh- справа от элемента с этим индексом находятся элементы равные или большие, чем опорный.p- индекс опорного элемента.i- счётчик.
Реализация процедуры разделения приведена ниже:
Листинг 4.13. Код для реализации процедуры разбиения массива на части (стр. 162):
int partition(item_type s[], int l, int h) {
int i; /* Счетчик */
int p; /* Индекс элемента-разделителя */
int firsthigh; /* Позиция для элемента разделителя */
p = h; /* Выбираем последний элемент в качестве разделителя */
firsthigh = l;
for (i = l; i < h; i++) {
if (s[i] < s[p]) {
swap(&s[i], &s[firsthigh]);
firsthigh++;
}
}
swap(&s[p], &s[firsthigh]);
return(firsthigh);
}
Опорный элемент p временно становится в конец массива (p = h), а переменная firsthigh в начало массива (firsthigh = l). Затем, запускается цикл по элементам от i до h-1. Если текущий элемент с индексом i будет меньше опорного, то он поменяется местами с элементом с индексом firsthigh, и затем мы увеличим firsthigh = firsthigh + 1. Таким образом элементы которые меньше опорного, будут сдвигаться влево, а граница отодвигаться вправо. В случае, когда проверяемый элемент больше или равен опорного, алгоритм проходит дальше, и элемент остаётся на своём месте (справа от элемента с индексом firsthigh). В конце опорный элемент поменяется местами с элементом на позиции firsthigh, вставая на своё место в отсортированном массиве.
Алгоритмы быстрой сортировки и сортировки слиянием строят рекурсивное дерево и на каждом уровне этого дерева выполняется работы за время O(n). Главная проблема быстрой сортировки - зависимость от выбора опорного элемента:
-
Если опорный элемент каждый раз делит массив пополам, то глубина рекурсии составит
logn, а итоговая сложностьO(nlogn). -
Если в качестве опорного элемента будет каждый раз выбираться наибольший или наименьший элемент массива, то массив будет делится на две части: 1-я пустая, 2-я размером
n-1. В итоге на обработку тратиться времяO(n), но задача уменьшатся всего на 1 элемент. Итоговая сложность составитO(n^2).
В итоге: в худшем случае быстрая сортировка работает хуже, чем пирамидальная сортировка или сортировка слиянием (обе гарантируют время работы O(nlogn)). Но в среднем, когда поступают случайные данные и сам опорный элемент выбирается случайно, она работает быстрее своих конкурентов.
Интуиция: ожидаемое время исполнения алгоритма быстрой сортировки.
Быстрая сортировка в среднем работает за время O(nlogn), потому что опорный элемент не обязан быть идеальной серединой, достаточно того, чтобы он находился возле центра сортируемого массива. Будем называть разделитель "хорошим", если он находится в центральной половине массива, то есть в диапазоне элементов для сортировки от n/4 до 3n/4. Вероятность выбрать такой элемент - 50%
Рис. 4.7. Демонстрация вероятности взять "хороший" элемент (стр. 163):
Даже если взять элемент на этой границе, самая большая часть после разделение будет размером не более 3n/4. Значит задача уменьшится не на 1 элемент, а сразу на 25%.
Чтобы дойти от n элементов до 1-го, потребуется k = $log_{4/3}n$ шагов (k - высота отсортированного дерева). Для идеального сбалансированного дерева, где разделителем является средний элемент, потребовалось бы k = $log_2n$ шагов. $log_{4/3}n$ больше $log_2n$ всего на 39%.
Поскольку на каждом уровне выполняется работа O(n), а глубина дерева в среднем O(logn), то итоговое время работы составит O(nlogn). И всё же, если постоянно нам не будет везти, и в качестве разделителя будет выбираться наибольший или наименьший элемент массива, то итоговое время работы составит O(n^2). Но вероятность выбрать такой элемент крайне мала.
Рандомизированные алгоритмы.
Comming soon