Дан фрагмент на Python: def quicksort(a): if len(a)<=1: return a; pivot=a[len(a)//2]; left=[x for x in a if xpivot]; return quicksort(left)+middle+quicksort(right) — проанализируйте сложность по времени и памяти в среднем и в худшем случаях, обсудите стабильность и предложите практические улучшения (включая in-place варианты и выбор опорного элемента)
Код, который вы привели, — классическая «питоновская» реализация quicksort в функциональном стиле:
def quicksortaaa: if lenaaa <= 1: return a pivot = alen(a)//2len(a)//2len(a)//2
left = [x for x in a if x < pivot] middle = xforxinaifx==pivotx for x in a if x == pivotxforxinaifx==pivot
right = [x for x in a if x > pivot] return quicksortleftleftleft + middle + quicksortrightrightright
1) Временная сложность
В среднем: Onlognn log nnlogn. При «хороших» разбиениях обычноглубинарекурсии≈O(logn)обычно глубина рекурсии ≈ O(log n)обычноглубинарекурсии≈O(logn) на каждом уровне делаем Onnn сравнений, уровней ≈ Olognlog nlogn.В худшем случае: On2n^2n2. Если опорный элемент постоянно даёт сильно несбалансированные разбиения например,одинподмассивразмерn−1,другой0например, один подмассив размер n−1, другой 0например,одинподмассивразмерn−1,другой0, глубина ≈ n и суммарно ≈ 1+2+...+n сравнений → On2n^2n2.
2) Память auxiliary/peakauxiliary / peakauxiliary/peak
Текущая реализация создаёт новые списки left/middle/right на каждом вызове: В среднем при«хороших»разбиенияхпри «хороших» разбиенияхпри«хороших»разбиениях: пик занимаемой дополнительной памяти — Onnn. Более точный подсчёт: на уровнях рекурсии суммарно живые элементы дают геометрическую сумму n + n/2 + n/4 + ... = Onnn.В худшем случае сильныйперекоссильный перекоссильныйперекос: пик может быть On2n^2n2вызовысоздаютспискиразмеромn,n−1,n−2,...иониостаютсяживыминастекевызовы создают списки размером n, n−1, n−2,... и они остаются живыми на стекевызовысоздаютспискиразмеромn,n−1,n−2,...иониостаютсяживыминастеке, то есть очень плохо по памяти.Рекурсивная глубина: в среднем Olognlog nlogn, в худшем Onnn — это дополнительно стек вызовов.
3) Стабильность
Данная реализация стабильна. Разделение выполнено фильтрами, которые сохраняют порядок элементов, и при конкатенации относительный порядок равных элементов сохраняется. Тоестьэлементысравнымизначенияминеменяютпорядокотносительнодругдруга.То есть элементы с равными значениями не меняют порядок относительно друг друга.Тоестьэлементысравнымизначенияминеменяютпорядокотносительнодругдруга.
4) Практические улучшения и варианты
a) Выбор опорного элемента
Случайный pivot random.choiceилиswapсrandomindexrandom.choice или swap с random indexrandom.choiceилиswapсrandomindex сильно уменьшает шанс худшего случая на «враждебных» входах → часто достаточное эмпирическое решение.median-of-three например,median(a[low],a[mid],a[high])например, median(a[low], a[mid], a[high])например,median(a[low],a[mid],a[high]) уменьшает шанс плохих разбиений на почти-отсортированных данных.Для жёстких гарантий — медиана медиан median−of−mediansmedian-of-mediansmedian−of−medians даёт детерминированный Onnn выбор опоры и гарантирует Onlognn log nnlogn времени, но сложнее и в практике редко используется.
b) In-place варианты меньшедополнительнойпамятименьше дополнительной памятименьшедополнительнойпамяти
Классический in-place quicksort с разделением Ломуто или Хоара использует Olognlog nlogn рекурсивной памяти в среднем и O111 дополнительной памяти для данных безсозданияновыхсписковбез создания новых списковбезсозданияновыхсписков. Минус: неустойчив.Пример Hoare−партитция,итеративный/рекурсивныйвариантHoare-партитция, итеративный/рекурсивный вариантHoare−партитция,итеративный/рекурсивныйвариант:
def quicksort_inplacea,lo=0,hi=Nonea, lo=0, hi=Nonea,lo=0,hi=None: if hi is None: hi = lenaaa-1 while lo < hi:
median-of-three можно сюда вставить для alololo, a(lo+hi)//2(lo+hi)//2(lo+hi)//2, ahihihi p = partitiona,lo,hia, lo, hia,lo,hi # реализуйте Hoare/Lomuto # рикурсивно сортируем меньшую часть, итеративно — большую tailrecursioneliminationtail recursion eliminationtailrecursionelimination
if p - lo < hi - p: quicksort_inplacea,lo,p−1a, lo, p-1a,lo,p−1
lo = p+1 else: quicksort_inplacea,p+1,hia, p+1, hia,p+1,hi
hi = p-1
реализуйтеpartitionвстилеHoare/Lomuto;вHoare−партитцииp—индексразбиения.реализуйте partition в стиле Hoare/Lomuto; в Hoare-партитции p — индекс разбиения.реализуйтеpartitionвстилеHoare/Lomuto;вHoare−партитцииp—индексразбиения.
c) Трёх-путевая DutchNationalFlagDutch National FlagDutchNationalFlag партиция
При большом количестве равных ключей обычный quicksort деградирует. 3-way разбиение меньше=,равноpivot,больше=меньше =, равно pivot, больше =меньше=,равноpivot,больше= даёт Onnn на массиве с большим числом дубликатов и в целом улучшает константы:
def quicksort_3waya,lo=0,hi=Nonea, lo=0, hi=Nonea,lo=0,hi=None: if hi is None: hi = lenaaa-1 if lo >= hi: return lt, i, gt = lo, lo+1, hi pivot = alololo
while i <= gt: if aiii < pivot: altltlt, aiii = aiii, altltlt; lt += 1; i += 1 elif aiii > pivot: aiii, agtgtgt = agtgtgt, aiii; gt -= 1 else: i += 1 quicksort_3waya,lo,lt−1a, lo, lt-1a,lo,lt−1
quicksort_3waya,gt+1,hia, gt+1, hia,gt+1,hi
d) Комбинация с insertion sort
Для маленьких подмассивов обычнопорог10–40обычно порог 10–40обычнопорог10–40 быстрее использовать insertion sort вместо рекурсии — уменьшает константы.
e) Избежать глубокой рекурсии
Применять tail recursion elimination sortsmallerhalfрекурсивно,абольшуювциклеsort smaller half рекурсивно, а большую в циклеsortsmallerhalfрекурсивно,абольшуювцикле, или использовать собственный стек, чтобы избежать переполнения стека при больших n.В Python глубокая рекурсия часто запрещена лимит 1000лимит ~1000лимит1000, поэтому для больших n лучше in-place итеративный вариант или увеличить recursionlimit нолучшенеполагатьсянаэтоно лучше не полагаться на этонолучшенеполагатьсянаэто.
f) Использовать встроенный сортировщик Python
Для практических задач в Python предпочтительнее использовать list.sort или sorted — это Timsort: стабильный, Onlognn log nnlogn в худшем случае, хорош для частично отсортированных данных и обычно быстрее самописного quicksort.
5) Итог краткократкократко
Ваш код: время среднее Onlognn log nnlogn, худшее On2n^2n2; память средняя Onnn, худшая On2n^2n2; стабильный.In-place quicksort: время те же, память средняя Olognlog nlognрекурс.стекрекурс. стекрекурс.стек, худшая Onnn; обычно нестабилен.Практические приёмы: случайный/median-of-three pivot, 3-way partition для дубликатов, использовать insertion sort для маленьких подмассивов, избегать глубокой рекурсии, или просто применять встроенный sorted/list.sort.
Если нужно, могу:
привести полный рабочий код in-place quicksort с Hoare-партитцией и median-of-three,или показать более подробный анализ памяти peakliveallocationspeak live allocationspeakliveallocations для разных сценариев.
Код, который вы привели, — классическая «питоновская» реализация quicksort в функциональном стиле:
def quicksortaaa:
if lenaaa <= 1:
return a
pivot = alen(a)//2len(a)//2len(a)//2 left = [x for x in a if x < pivot]
middle = xforxinaifx==pivotx for x in a if x == pivotxforxinaifx==pivot right = [x for x in a if x > pivot]
return quicksortleftleftleft + middle + quicksortrightrightright
1) Временная сложность
В среднем: Onlognn log nnlogn. При «хороших» разбиениях обычноглубинарекурсии≈O(logn)обычно глубина рекурсии ≈ O(log n)обычноглубинарекурсии≈O(logn) на каждом уровне делаем Onnn сравнений, уровней ≈ Olognlog nlogn.В худшем случае: On2n^2n2. Если опорный элемент постоянно даёт сильно несбалансированные разбиения например,одинподмассивразмерn−1,другой0например, один подмассив размер n−1, другой 0например,одинподмассивразмерn−1,другой0, глубина ≈ n и суммарно ≈ 1+2+...+n сравнений → On2n^2n2.2) Память auxiliary/peakauxiliary / peakauxiliary/peak
Текущая реализация создаёт новые списки left/middle/right на каждом вызове:В среднем при«хороших»разбиенияхпри «хороших» разбиенияхпри«хороших»разбиениях: пик занимаемой дополнительной памяти — Onnn. Более точный подсчёт: на уровнях рекурсии суммарно живые элементы дают геометрическую сумму n + n/2 + n/4 + ... = Onnn.В худшем случае сильныйперекоссильный перекоссильныйперекос: пик может быть On2n^2n2 вызовысоздаютспискиразмеромn,n−1,n−2,...иониостаютсяживыминастекевызовы создают списки размером n, n−1, n−2,... и они остаются живыми на стекевызовысоздаютспискиразмеромn,n−1,n−2,...иониостаютсяживыминастеке, то есть очень плохо по памяти.Рекурсивная глубина: в среднем Olognlog nlogn, в худшем Onnn — это дополнительно стек вызовов.
3) Стабильность
Данная реализация стабильна. Разделение выполнено фильтрами, которые сохраняют порядок элементов, и при конкатенации относительный порядок равных элементов сохраняется. Тоестьэлементысравнымизначенияминеменяютпорядокотносительнодругдруга.То есть элементы с равными значениями не меняют порядок относительно друг друга.Тоестьэлементысравнымизначенияминеменяютпорядокотносительнодругдруга.4) Практические улучшения и варианты
a) Выбор опорного элемента
Случайный pivot random.choiceилиswapсrandomindexrandom.choice или swap с random indexrandom.choiceилиswapсrandomindex сильно уменьшает шанс худшего случая на «враждебных» входах → часто достаточное эмпирическое решение.median-of-three например,median(a[low],a[mid],a[high])например, median(a[low], a[mid], a[high])например,median(a[low],a[mid],a[high]) уменьшает шанс плохих разбиений на почти-отсортированных данных.Для жёстких гарантий — медиана медиан median−of−mediansmedian-of-mediansmedian−of−medians даёт детерминированный Onnn выбор опоры и гарантирует Onlognn log nnlogn времени, но сложнее и в практике редко используется.b) In-place варианты меньшедополнительнойпамятименьше дополнительной памятименьшедополнительнойпамяти
Классический in-place quicksort с разделением Ломуто или Хоара использует Olognlog nlogn рекурсивной памяти в среднем и O111 дополнительной памяти для данных безсозданияновыхсписковбез создания новых списковбезсозданияновыхсписков. Минус: неустойчив.Пример Hoare−партитция,итеративный/рекурсивныйвариантHoare-партитция, итеративный/рекурсивный вариантHoare−партитция,итеративный/рекурсивныйвариант:def quicksort_inplacea,lo=0,hi=Nonea, lo=0, hi=Nonea,lo=0,hi=None:
median-of-three можно сюда вставить для alololo, a(lo+hi)//2(lo+hi)//2(lo+hi)//2, ahihihi p = partitiona,lo,hia, lo, hia,lo,hi # реализуйте Hoare/Lomutoif hi is None: hi = lenaaa-1
while lo < hi:
# рикурсивно сортируем меньшую часть, итеративно — большую tailrecursioneliminationtail recursion eliminationtailrecursionelimination if p - lo < hi - p:
quicksort_inplacea,lo,p−1a, lo, p-1a,lo,p−1 lo = p+1
else:
quicksort_inplacea,p+1,hia, p+1, hia,p+1,hi hi = p-1
реализуйтеpartitionвстилеHoare/Lomuto;вHoare−партитцииp—индексразбиения.реализуйте partition в стиле Hoare/Lomuto; в Hoare-партитции p — индекс разбиения.реализуйтеpartitionвстилеHoare/Lomuto;вHoare−партитцииp—индексразбиения.
c) Трёх-путевая DutchNationalFlagDutch National FlagDutchNationalFlag партиция
При большом количестве равных ключей обычный quicksort деградирует. 3-way разбиение меньше=,равноpivot,больше=меньше =, равно pivot, больше =меньше=,равноpivot,больше= даёт Onnn на массиве с большим числом дубликатов и в целом улучшает константы:def quicksort_3waya,lo=0,hi=Nonea, lo=0, hi=Nonea,lo=0,hi=None:
if hi is None: hi = lenaaa-1
if lo >= hi: return
lt, i, gt = lo, lo+1, hi
pivot = alololo while i <= gt:
if aiii < pivot:
altltlt, aiii = aiii, altltlt; lt += 1; i += 1
elif aiii > pivot:
aiii, agtgtgt = agtgtgt, aiii; gt -= 1
else:
i += 1
quicksort_3waya,lo,lt−1a, lo, lt-1a,lo,lt−1 quicksort_3waya,gt+1,hia, gt+1, hia,gt+1,hi
d) Комбинация с insertion sort
Для маленьких подмассивов обычнопорог10–40обычно порог 10–40обычнопорог10–40 быстрее использовать insertion sort вместо рекурсии — уменьшает константы.e) Избежать глубокой рекурсии
Применять tail recursion elimination sortsmallerhalfрекурсивно,абольшуювциклеsort smaller half рекурсивно, а большую в циклеsortsmallerhalfрекурсивно,абольшуювцикле, или использовать собственный стек, чтобы избежать переполнения стека при больших n.В Python глубокая рекурсия часто запрещена лимит 1000лимит ~1000лимит 1000, поэтому для больших n лучше in-place итеративный вариант или увеличить recursionlimit нолучшенеполагатьсянаэтоно лучше не полагаться на этонолучшенеполагатьсянаэто.f) Использовать встроенный сортировщик Python
Для практических задач в Python предпочтительнее использовать list.sort или sorted — это Timsort: стабильный, Onlognn log nnlogn в худшем случае, хорош для частично отсортированных данных и обычно быстрее самописного quicksort.5) Итог краткократкократко
Ваш код: время среднее Onlognn log nnlogn, худшее On2n^2n2; память средняя Onnn, худшая On2n^2n2; стабильный.In-place quicksort: время те же, память средняя Olognlog nlogn рекурс.стекрекурс. стекрекурс.стек, худшая Onnn; обычно нестабилен.Практические приёмы: случайный/median-of-three pivot, 3-way partition для дубликатов, использовать insertion sort для маленьких подмассивов, избегать глубокой рекурсии, или просто применять встроенный sorted/list.sort.Если нужно, могу:
привести полный рабочий код in-place quicksort с Hoare-партитцией и median-of-three,или показать более подробный анализ памяти peakliveallocationspeak live allocationspeakliveallocations для разных сценариев.