Один из самых популярных алгоритмов сортировки – QuickSort. Он основан на подходе «разделяй и властвуй», где на каждом шаге выбирается опорный элемент (pivot) из массива. Затем массив разбивается на три части – элементы меньше опорного, равные опорному и больше опорного. Этот процесс повторяется, пока весь массив не окажется отсортирован.
По сути, на каждой итерации алгоритм делает следующее:
Выбирает опорный элемент из массива.
Перемещает все элементы, которые меньше опорного, в левую часть.
Перемещает все элементы, которые больше опорного, в правую часть.
Собирает элементы, равные опорному, в середине.
Рекурсивно повторяет те же действия для левого и правого фрагментов массива.
def quicksort(a):
if len(a) <= 1:
return a
pivot = a[len(a) // 2]
left = [x for x in a if x < pivot]
middle = [x for x in a if x == pivot]
right = [x for x in a if x > pivot]
return quicksort(left) + middle + quicksort(right)
Эта реализация QuickSort использует дополнительные массивы для частей left, middle и right. Чтобы сделать сортировку на месте (in place), можно немного изменить код, используя индексы и переставляя элементы прямо в массиве.
def quicksort(a, start, end):
if start >= end: # Нет элементов для сортировки
return
pivot = a[start] # Выбираем опорный элемент
l, r = start + 1, end # Сортируем a[start+1 ... end]
while l <= r: # Пока в диапазоне остаются элементы
if a[l] < pivot: # если a[l] < pivot => не трогаем a[l]
l += 1 # Увеличиваем левый указатель
else: # иначе перемещаем a[l] в конец
a[l], a[r] = a[r], a[l] # Меняем a[l] и a[r] местами -> перенос в конец
r -= 1 # Уменьшаем правый указатель
a[start], a[r] = a[r], a[start] # Меняем местами опорный элемент и a[r]
quicksort(a, start, r - 1) # Сортируем a[start ... r-1]
quicksort(a, r + 1, end) # Сортируем a[r+1 ... end]
Обратите внимание, что выбор pivot в этих двух примерах был разным. На самом деле у QuickSort нет строгих ограничений на выбор опорного элемента. Существует несколько распространённых подходов:
Первый элемент: a[start] (как в показанном варианте).
Последний элемент: a[end].
Средний элемент: a[(start + end + 1) // 2].
Случайный элемент: выбрать любой элемент из a[start ... end].
Медианный элемент: взять медиану текущего отрезка (обеспечивает равные левые и правые части, но требует более сложной реализации).
Давайте наглядно проследим работу алгоритма на нескольких примерах:
Среднее время работы QuickSort составляет , однако в худшем случае, в зависимости от выбора опорного элемента, алгоритм может работать за .
🤔 Можете ли вы представить такой пример массива, при котором выбор опорного элемента как start на каждом шаге приведёт к операциям?
Challenge
Пусть у нас есть список из n целых чисел и q опорных элементов (pivots). Требуется расположить в массиве все элементы, которые меньше данного опорного элемента, слева от него, а все элементы, которые больше – справа, оставив элементы, равные опорному, вместе (рядом друг с другом, сразу после меньших и сразу перед большими).
Входные данные
Первая строка входных данных содержит одно целое число n (1 ≤ n ≤ 100 000).
Следующая строка содержит n целых чисел (через пробел), где каждое удовлетворяет условию .
Третья строка содержит одно целое число q (1 ≤ q ≤ 10).
Следующая строка содержит q целых чисел (1 ≤ ≤ n), которые обозначают индекс опорного элемента.
Выходные данные
Для каждого из q опорных элементов программа должна вывести итоговый массив после соответствующего перераспределения. Если существует несколько корректных вариантов расположения, можно вывести любой из них.