В коде на Python функция быстрого сортирования написана так: def quicksort(a): if len(a)<=1: return a; pivot=a[0]; left=[x for x in a[1:] if xpivot]; return quicksort(left)+[pivot]+quicksort(right) — проанализируйте этот реализационный вариант по времени, пространству, стабильности и предложите улучшения для худшего случая и параллельного выполнения

2 Окт в 13:42
12 +1
0
Ответы
1

Код, который вы привели:

def quicksort(a):
if len(a) <= 1:
return a
pivot = a[0]
left = [x for x in a[1:] if x <= pivot]
right = [x for x in a[1:] if x > pivot]
return quicksort(left) + [pivot] + quicksort(right)

классический «функциональный» вариант quicksort на Python. Проанализирую по пунктам и предложу улучшения.

1) Время

Среднее: O(n log n). При удачном (случайном/балансном) разбиении рекурсивные уровни ≈ log n, суммарная работа на уровне ≈ n.Худший случай: O(n^2). Происходит при очень неудачном выборе опорного (например, первый элемент в уже отсортированном массиве) — каждый вызов делает разбиение 1 и n-1.Константы: из‑за создания новых списков на каждом вызове и конкатенации списков константы большие по сравнению с in‑place реализацией.

2) Память

Этот вариант не in‑place: на каждом вызове создаются два новых списка (left и right) + результаты рекурсий и конкатенации. Поэтому используются дополнительные объёмы памяти.В среднем: auxiliary O(n) — на верхнем уровне создаются списки суммарно ~n; рекурсивная глубина ≈ log n. Но из‑за того, что родительские локальные списки живут пока выполняется рекурсия на потомках, пиковое потребление может быть значительно выше.Худший случай: глубина рекурсии O(n). В плохо сбалансированных случаях одновременно могут существовать списки размеров n-1, n-2, … ⇒ потенциально крупные пиковые затраты (на практике это приводит либо к сильному росту аллокаций, либо к O(n^2) суммарно выделенной памяти за время работы).Рекурсивный стек: глубина до n ⇒ стек O(n) в худшем случае (может привести к переполнению рекурсивного стека в Python).

3) Стабильность

Реализация нестабильна. Хотя list comprehension сохраняет относительный порядок внутри left и right, опорный элемент вынесен отдельно. Все элементы, равные pivot, из a[1:] попадут в left (<=), и в результирующем списке окажутся перед pivot — это меняет относительный порядок равных элементов относительно опорного. Следовательно алгоритм не гарантирует стабильность.

4) Улучшения для худшего случая

Рандомизация:
Перемешать массив заранее (random.shuffle(a)) — делает худший (предсказуемый) вход крайне маловероятным.Или выбирать случайный элемент в качестве pivot на каждом шаге: pivot = a[random.randrange(len(a))].Лучший выбор опорного:
median-of-three (выбрать медиану из a[lo], a[mid], a[hi]) — уменьшает шанс последовательных плохих разбиений.Для жёстких гарантий можно использовать «median of medians» (selection) — гарантирует O(n log n), но с большим накладным расходом и редко применимо на практике.Introsort:
Запустить quicksort, и если глубина рекурсии превысила порог (например, 2 * log2(n)), переключиться на heapsort для гарантии O(n log n) в худшем случае.Малые подмассивы:
Для подмассивов небольшого размера (например, <= 16) не рекурсировать и использовать insertion sort — это снижает константы.В‑place partition:
Использовать Hoare или Lomuto partition, чтобы не копировать подсписки — уменьшает использование памяти и накладные расходы на аллокацию/конкатенацию.

Пример in‑place quicksort с рандомным pivot и cutoff на insertion sort:

def insertion_sort(a, lo, hi):
for i in range(lo + 1, hi + 1):
key = a[i]
j = i - 1
while j >= lo and a[j] > key:
a[j + 1] = a[j]
j -= 1
a[j + 1] = key

def partition(a, lo, hi):

Lomuto partition with random pivotpivot_index = random.randrange(lo, hi + 1)
a[pivot_index], a[hi] = a[hi], a[pivot_index]
pivot = a[hi]
i = lo
for j in range(lo, hi):
if a[j] <= pivot:
a[i], a[j] = a[j], a[i]
i += 1
a[i], a[hi] = a[hi], a[i]
return i

def quicksort_inplace(a, lo=0, hi=None):
if hi is None:
hi = len(a) - 1
while lo < hi:
if hi - lo < 16: # cutoff
insertion_sort(a, lo, hi)
break
p = partition(a, lo, hi)

рекурсивно сортируем меньшую часть первой (tail recursion elimination) if p - lo < hi - p:
quicksort_inplace(a, lo, p - 1)
lo = p + 1
else:
quicksort_inplace(a, p + 1, hi)
hi = p - 1

Этот вариант:

in‑place (не создаёт новых больших списков);меньшие накладные расходы;уменьшен риск переполнения стека за счёт итеравизации меньшей части (tail elimination);cutoff на insertion sort — быстрее для маленьких участков.

5) Параллельное выполнение
Особенности в Python:

CPython и GIL: многопоточный код не даст выигрыша в CPU‑bound задачах. Для реальной параллельной сортировки нужно multiprocessing (процессы) или использование расширений (numpy+numba, C‑реализация).Накладные расходы на сериализацию/передачу данных между процессами: отправлять/копировать большие подмассивы дорого. Поэтому параллелить стоит только верхние уровни рекурсии, где сегменты велики.

Варианты:
a) Параллелизация с ProcessPoolExecutor (верхние уровни только):

from concurrent.futures import ProcessPoolExecutor
import math

def quicksort_parallel(a, max_workers=None, min_size=1_000_00, depth=0):

простая функция: если массив мал — сортируем локально,# иначе рекурсивно параллелим левую и правую часть (ограничение depth)
if len(a) <= 1:
return a
if len(a) < min_size or depth > math.log2(len(a)) + 4:
a.sort()
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]
with ProcessPoolExecutor(max_workers=max_workers) as ex:
left_f = ex.submit(quicksort_parallel, left, max_workers, min_size, depth+1)
right_f = ex.submit(quicksort_parallel, right, max_workers, min_size, depth+1)
left_sorted = left_f.result()
right_sorted = right_f.result()
return left_sorted + middle + right_sorted

Недостатки: копирование left/right в процессы, накладные расходы сериализации.

b) Использовать общую память — numpy + numba / C:

Если данные числовые, лучше использовать numpy‑массив и разделять массив по индексам, передавая только индексы в процессы или запускать параллельную сортировку на уровне C/numba, чтобы не копировать данные между процессами.Примеры: multiprocessing.Array + manual in‑place partition и рекурсивная распараллеливание только на верхних уровнях (forked процессы могут разделять память в ОС, но нужно осторожно).

c) Альтернатива: использовать многопроцессную реализацию merge/parallel sort в библиотеках (joblib, dask) или воспользоваться C/C++ реализациями (встроенный sorted()/list.sort() — очень оптимизированный Timsort; для параллельной сортировки использовать numpy.sort с режимом kind).

Рекомендации практические

Если вам нужна стабильность и хорошая производительность — используйте встроенный sort (list.sort() / sorted()), это Timsort: стабильный, O(n log n) в среднем, хорошая кэш/профилировка на уже частично отсортированных данных.Если нужна in‑place быстрая сортировка с малыми накладными расходами — используйте in‑place partition (Hoare/Lomuto), рандомный pivot / median‑of‑three, cutoff на insertion sort, и introsort для гарантий по худшему случаю.Для реальной параллелизации в Python — либо распараллеливайте только верхние уровни и используйте multiprocessing с порогом по размеру подмассива, либо используйте числовые структуры и инструменты (numpy/numba) позволяющие работать с общим блоком памяти.

Если хотите, могу:

дать полный рабочий пример in‑place quicksort с median‑of‑three + introsort переключением на heapsort;или показать пример параллельной реализации для числового массива на numpy/numba или через multiprocessing с общей памятью.
2 Окт в 14:08
Не можешь разобраться в этой теме?
Обратись за помощью к экспертам
Гарантированные бесплатные доработки в течение 1 года
Быстрое выполнение от 2 часов
Проверка работы на плагиат
Поможем написать учебную работу
Прямой эфир