В следующем фрагменте Python-функции намереваются найти k‑й наименьший элемент массива, но она работает неверно для некоторых входов: def kth_smallest(arr, k): pivot = arr[0] left = [x for x in arr if x pivot] if k
Ниже — разбор проблем в исходном фрагменте, исправленный вариант и оценка сложности.
Исходный код: def kth_smallestarr,karr, karr,k: pivot = arr000
left = [x for x in arr if x < pivot] right = [x for x in arr if x > pivot] if k <= lenleftleftleft: return kth_smallestleft,kleft, kleft,k
elif k == lenleftleftleft + 1: return pivot else: return kth_smallestright,k−len(left)−1right, k - len(left) - 1right,k−len(left)−1
Основные логические ошибки и ситуации, приводящие к неправильному поведению или падению
Дубликаты равные pivot теряются. Компрехеншены left и right исключают элементы == pivot кромесамогоpivotкроме самого pivotкромесамогоpivot, поэтому элементы, равные pivot, никуда не попадают. Пример: arr = 2,2,22,2,22,2,2, k = 2 — функция рекурсивно вызовет себя на пустом right и упадёт или зациклится.Неправильная обработка случаев, когда k попадает в диапазон равных pivot еслиихбольшеодногоесли их больше одногоеслиихбольшеодного. Код считает только один pivot, поэтому для k внутри блока равных вернёт неверный ответ.Нет проверки границ k (k < 1 или k > len(arr)) — возможны неверные вызовы или бесконечная рекурсия.Нет явного базового случая для пустого массива — вызов на пустом списке приведёт к arr000 и падению.Выбор pivot = arr000 делает алгоритм чувствительным к порядку входа например,ужеотсортированныймассивдастхудшуюпроизводительностьO(n2)например, уже отсортированный массив даст худшую производительность O(n^2)например,ужеотсортированныймассивдастхудшуюпроизводительностьO(n2). Для среднего времени лучше выбирать случайный pivot или применять медиану из трёх.
Исправленный простой вариант разделениенатричасти—меньше/равно/больше,случайныйpivotразделение на три части — меньше / равно / больше, случайный pivotразделениенатричасти—меньше/равно/больше,случайныйpivot: import random
def kth_smallestarr,karr, karr,k: if not 1 <= k <= lenarrarrarr: raise IndexError("k is out of bounds") if lenarrarrarr == 1: return arr000
pivot = random.choicearrarrarr
lows = [x for x in arr if x < pivot] pivots = xforxinarrifx==pivotx for x in arr if x == pivotxforxinarrifx==pivot
highs = [x for x in arr if x > pivot]
if k <= lenlowslowslows: return kth_smallestlows,klows, klows,k
elif k <= lenlowslowslows + lenpivotspivotspivots: return pivot else: return kth_smallesthighs,k−len(lows)−len(pivots)highs, k - len(lows) - len(pivots)highs,k−len(lows)−len(pivots)
Почему это исправляет ошибки
Все элементы распределяются в три группы: <pivot, ==pivot, >pivot — ничего не теряется.Если k попадает в диапазон группы равных — возвращаем pivot.Проверяем границы k и добавлен базовый случай lenarrarrarr == 1.Случайный pivot даёт ожидаемое линейное среднее время и предотвращает злостные паттерны входа.
Альтернатива для экономии памяти: in-place quickselect Lomuto/HoareLomuto/HoareLomuto/Hoare. Она использует обмены в массиве и работает с индексами O(1)дополнительнойпамяти,рекурсивнаяглубина—O(logn)всреднемO(1) дополнительной памяти, рекурсивная глубина — O(log n) в среднемO(1)дополнительнойпамяти,рекурсивнаяглубина—O(logn)всреднем. Вот простой in-place LomutoLomutoLomuto вариант:
import random
def partitiona,lo,hia, lo, hia,lo,hi: pivot = ahihihi
i = lo for j in rangelo,hilo, hilo,hi: if ajjj < pivot: aiii, ajjj = ajjj, aiii
i += 1 aiii, ahihihi = ahihihi, aiii
return i
def quickselect_inplacea,ka, ka,k: if not 1 <= k <= lenaaa: raise IndexError("k is out of bounds") lo, hi = 0, lenaaa - 1 k_index = k - 1 while lo <= hi:
случайный выбор опорного элемента pivot_index = random.randintlo,hilo, hilo,hi
apivotindexpivot_indexpivotindex, ahihihi = ahihihi, apivotindexpivot_indexpivotindex
p = partitiona,lo,hia, lo, hia,lo,hi
if p == k_index: return appp
elif p > k_index: hi = p - 1 else: lo = p + 1 raise RuntimeError"unreachable""unreachable""unreachable"
Оценка сложности 1) Исправленная версия с созданием новых списков первыйвариантпервый вариантпервыйвариант:
Среднее ожидаемоеожидаемоеожидаемое время: Onnn — при случайном выборе pivot ожидается линейная сложность.Худшее время: On2n^2n2 — если pivot постоянно очень плох например,всегдаэкстремальныйэлементнапример, всегда экстремальный элементнапример,всегдаэкстремальныйэлемент.Доп. память пиковаяпиковаяпиковая: в среднем OnnnприсбалансированныхразделенияхсуммарныеразмерысписковпоуровнямрекурсииограниченыO(n)при сбалансированных разделениях суммарные размеры списков по уровням рекурсии ограничены O(n)присбалансированныхразделенияхсуммарныеразмерысписковпоуровнямрекурсииограниченыO(n), но в худшем случае сильнонеудачныеразбиения,рекурсияглубинойO(n)сильно неудачные разбиения, рекурсия глубиной O(n)сильнонеудачныеразбиения,рекурсияглубинойO(n) — On2n^2n2 из‑за накопления копий списков на стеке вызовов.
2) In-place quickselect:
Среднее время: OnnnХудшее время: On2n^2n2зависитотвыбораpivot;можноуменьшитьвероятностьплохогослучаярандомизациейилимедианойизтрёхзависит от выбора pivot; можно уменьшить вероятность плохого случая рандомизацией или медианой из трёхзависитотвыбораpivot;можноуменьшитьвероятностьплохогослучаярандомизациейилимедианойизтрёхДоп. память: O111 дополнительной памяти изменяетсямассивнаместеизменяется массив на местеизменяетсямассивнаместе плюс стек вызовов — Olognlog nlogn в среднем, Onnn в худшем случае можнопереписатьитеративно,тогдастекO(1)можно переписать итеративно, тогда стек O(1)можнопереписатьитеративно,тогдастекO(1).
Рекомендации
Если важна простота и читабельность — использовать первый исправленный вариант с разделением на три списка и random.choice.Если важна экономия памяти и производительность в худших практических случаях — использовать in-place quickselect с рандомизированным опорным элементом.Всегда проверяйте границы k и обрабатывайте пустые массивы/базовые случаи.
Ниже — разбор проблем в исходном фрагменте, исправленный вариант и оценка сложности.
Исходный код:
def kth_smallestarr,karr, karr,k:
pivot = arr000 left = [x for x in arr if x < pivot]
right = [x for x in arr if x > pivot]
if k <= lenleftleftleft:
return kth_smallestleft,kleft, kleft,k elif k == lenleftleftleft + 1:
return pivot
else:
return kth_smallestright,k−len(left)−1right, k - len(left) - 1right,k−len(left)−1
Основные логические ошибки и ситуации, приводящие к неправильному поведению или падению
Дубликаты равные pivot теряются. Компрехеншены left и right исключают элементы == pivot кромесамогоpivotкроме самого pivotкромесамогоpivot, поэтому элементы, равные pivot, никуда не попадают. Пример: arr = 2,2,22,2,22,2,2, k = 2 — функция рекурсивно вызовет себя на пустом right и упадёт или зациклится.Неправильная обработка случаев, когда k попадает в диапазон равных pivot еслиихбольшеодногоесли их больше одногоеслиихбольшеодного. Код считает только один pivot, поэтому для k внутри блока равных вернёт неверный ответ.Нет проверки границ k (k < 1 или k > len(arr)) — возможны неверные вызовы или бесконечная рекурсия.Нет явного базового случая для пустого массива — вызов на пустом списке приведёт к arr000 и падению.Выбор pivot = arr000 делает алгоритм чувствительным к порядку входа например,ужеотсортированныймассивдастхудшуюпроизводительностьO(n2)например, уже отсортированный массив даст худшую производительность O(n^2)например,ужеотсортированныймассивдастхудшуюпроизводительностьO(n2). Для среднего времени лучше выбирать случайный pivot или применять медиану из трёх.Исправленный простой вариант разделениенатричасти—меньше/равно/больше,случайныйpivotразделение на три части — меньше / равно / больше, случайный pivotразделениенатричасти—меньше/равно/больше,случайныйpivot:
import random
def kth_smallestarr,karr, karr,k:
if k <= lenlowslowslows:if not 1 <= k <= lenarrarrarr:
raise IndexError("k is out of bounds")
if lenarrarrarr == 1:
return arr000 pivot = random.choicearrarrarr lows = [x for x in arr if x < pivot]
pivots = xforxinarrifx==pivotx for x in arr if x == pivotxforxinarrifx==pivot highs = [x for x in arr if x > pivot]
return kth_smallestlows,klows, klows,k elif k <= lenlowslowslows + lenpivotspivotspivots:
return pivot
else:
return kth_smallesthighs,k−len(lows)−len(pivots)highs, k - len(lows) - len(pivots)highs,k−len(lows)−len(pivots)
Почему это исправляет ошибки
Все элементы распределяются в три группы: <pivot, ==pivot, >pivot — ничего не теряется.Если k попадает в диапазон группы равных — возвращаем pivot.Проверяем границы k и добавлен базовый случай lenarrarrarr == 1.Случайный pivot даёт ожидаемое линейное среднее время и предотвращает злостные паттерны входа.Альтернатива для экономии памяти: in-place quickselect Lomuto/HoareLomuto/HoareLomuto/Hoare. Она использует обмены в массиве и работает с индексами O(1)дополнительнойпамяти,рекурсивнаяглубина—O(logn)всреднемO(1) дополнительной памяти, рекурсивная глубина — O(log n) в среднемO(1)дополнительнойпамяти,рекурсивнаяглубина—O(logn)всреднем. Вот простой in-place LomutoLomutoLomuto вариант:
import random
def partitiona,lo,hia, lo, hia,lo,hi:
pivot = ahihihi i = lo
for j in rangelo,hilo, hilo,hi:
if ajjj < pivot:
aiii, ajjj = ajjj, aiii i += 1
aiii, ahihihi = ahihihi, aiii return i
def quickselect_inplacea,ka, ka,k:
случайный выбор опорного элемента pivot_index = random.randintlo,hilo, hilo,hi apivotindexpivot_indexpivoti ndex, ahihihi = ahihihi, apivotindexpivot_indexpivoti ndex p = partitiona,lo,hia, lo, hia,lo,hi if p == k_index:if not 1 <= k <= lenaaa:
raise IndexError("k is out of bounds")
lo, hi = 0, lenaaa - 1
k_index = k - 1
while lo <= hi:
return appp elif p > k_index:
hi = p - 1
else:
lo = p + 1
raise RuntimeError"unreachable""unreachable""unreachable"
Оценка сложности
Среднее ожидаемоеожидаемоеожидаемое время: Onnn — при случайном выборе pivot ожидается линейная сложность.Худшее время: On2n^2n2 — если pivot постоянно очень плох например,всегдаэкстремальныйэлементнапример, всегда экстремальный элементнапример,всегдаэкстремальныйэлемент.Доп. память пиковаяпиковаяпиковая: в среднем Onnn присбалансированныхразделенияхсуммарныеразмерысписковпоуровнямрекурсииограниченыO(n)при сбалансированных разделениях суммарные размеры списков по уровням рекурсии ограничены O(n)присбалансированныхразделенияхсуммарныеразмерысписковпоуровнямрекурсииограниченыO(n), но в худшем случае сильнонеудачныеразбиения,рекурсияглубинойO(n)сильно неудачные разбиения, рекурсия глубиной O(n)сильнонеудачныеразбиения,рекурсияглубинойO(n) — On2n^2n2 из‑за накопления копий списков на стеке вызовов.1) Исправленная версия с созданием новых списков первыйвариантпервый вариантпервыйвариант:
2) In-place quickselect:
Среднее время: OnnnХудшее время: On2n^2n2 зависитотвыбораpivot;можноуменьшитьвероятностьплохогослучаярандомизациейилимедианойизтрёхзависит от выбора pivot; можно уменьшить вероятность плохого случая рандомизацией или медианой из трёхзависитотвыбораpivot;можноуменьшитьвероятностьплохогослучаярандомизациейилимедианойизтрёхДоп. память: O111 дополнительной памяти изменяетсямассивнаместеизменяется массив на местеизменяетсямассивнаместе плюс стек вызовов — Olognlog nlogn в среднем, Onnn в худшем случае можнопереписатьитеративно,тогдастекO(1)можно переписать итеративно, тогда стек O(1)можнопереписатьитеративно,тогдастекO(1).Рекомендации
Если важна простота и читабельность — использовать первый исправленный вариант с разделением на три списка и random.choice.Если важна экономия памяти и производительность в худших практических случаях — использовать in-place quickselect с рандомизированным опорным элементом.Всегда проверяйте границы k и обрабатывайте пустые массивы/базовые случаи.