В коде на 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) — проанализируйте этот реализационный вариант по времени, пространству, стабильности и предложите улучшения для худшего случая и параллельного выполнения
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
простая функция: если массив мал — сортируем локально,# иначе рекурсивно параллелим левую и правую часть (ограничение 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 с общей памятью.
Код, который вы привели:
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):
рекурсивно сортируем меньшую часть первой (tail recursion elimination) if p - lo < hi - p: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)
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) Параллельное выполнение
CPython и GIL: многопоточный код не даст выигрыша в CPU‑bound задачах. Для реальной параллельной сортировки нужно multiprocessing (процессы) или использование расширений (numpy+numba, C‑реализация).Накладные расходы на сериализацию/передачу данных между процессами: отправлять/копировать большие подмассивы дорого. Поэтому параллелить стоит только верхние уровни рекурсии, где сегменты велики.Особенности в Python:
Варианты:
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 с общей памятью.