В приведённом фрагменте Python: def remove_dups(arr): for i in range(len(arr)): if arr[i] in arr[:i]: arr.pop(i) return arr объясните, что делает код, найдите ошибки (логические и по сложности), предложите корректную и эффективную реализацию удаления дубликатов с сохранением порядка
def remove_dupsarrarrarr: for i in rangelen(arr)len(arr)len(arr): if arriii in arr:i:i:i: arr.popiii
return arr
Автор пытается пройти по массиву по индексам и для каждого элемента удалить его, если он уже встречался слева вarr[:i]в arr[:i]вarr[:i].arr:i:i:i — срез, содержащий все элементы слева от позиции i, проверка arriii in arr:i:i:i — ищет дубликат слева.Если найден дубликат — выполняется arr.popiiiудалениепоиндексуудаление по индексуудалениепоиндексу.
Ошибки и недостатки
Логическая/ошибка исполнения:
Нельзя безопасно изменять длину списка poppoppop при итерации по rangelen(arr)len(arr)len(arr), потому что range вычислен заранее. После уменьшения длины списка обращение к arriii при дальнейшем i может вызвать IndexError. Пример: arr = 1,1,21,1,21,1,2
i=0: оставить i=1: удалить второй 1 → arr = 1,21,21,2
i=2: попытка обратиться к arr222 → IndexError.
Кроме того, удаление по индексу сдвигает оставшиеся элементы, что может привести к пропуску проверки некоторых элементов авторскийподходненадёжныйавторский подход ненадёжныйавторскийподходненадёжный.
Для каждого i создаётся срез arr:i:i:i — это Oiii по времени и дополнительная память; затем проверка in — ещё Oiii в худшем случае. Так что одна проверка ≈ Oiii, суммарно On2n^2n2.popiii удаление в середине списка — Onnn сдвиг элементов; в худшем случае суммарно ещё On2n^2n2.Итого алгоритм работает за квадратичное время ииспользуетдополнительнуюпамятьиз−засрезови использует дополнительную память из-за срезовииспользуетдополнительнуюпамятьиз−засрезов. Для больших списков это плохо.
Корректная и эффективная реализация ссохранениемпорядкас сохранением порядкассохранениемпорядка
1) Простая и понятная возвращаетновыйсписок,ожидается,чтоэлементыхешируемывозвращает новый список, ожидается, что элементы хешируемывозвращаетновыйсписок,ожидается,чтоэлементыхешируемы: def remove_dupsarrarrarr: seen = set
res =
for x in arr: if x not in seen: seen.addxxx
res.appendxxx
return res
Сложность: Onnn в среднем по времени, Onnn по дополнительной памяти.
2) Вариант, модифицирующий список in-place безсозданияновогосписка,носдополнительныммножествомдляseenбез создания нового списка, но с дополнительным множеством для seenбезсозданияновогосписка,носдополнительныммножествомдляseen: def remove_dups_inplacearrarrarr: seen = set
write = 0 for x in arr: if x not in seen: seen.addxxx
arrwritewritewrite = x write += 1 del arrwrite:write:write: # обрезаем хвост return arr
Этот вариант экономит место на новый список, но всё равно использует Onnn для множества seen.
3) Если элементы не хешируемы, можно использовать множество ключей keykeykey или fallback на проверку по списку медленнеемедленнеемедленнее: def remove_dups_keyarr,key=lambdax:xarr, key=lambda x: xarr,key=lambdax:x: seen = set
res =
for x in arr: k = keyxxx
if k not in seen: seen.addkkk
res.appendxxx
return res
Примеры и замечания
Все приведённые версии сохраняют порядок первых вхождений.Если элементы не хешируемы и у вас нет разумной функции key, придётся использовать квадратичный алгоритм проверкаinпоспискупроверка in по спискупроверкаinпосписку.Если нужна стабильность и допустима дополнительная память — вариант с seen+res наиболее прост и быстрый.
Если хотите, могу показать конкретные тесты, демонстрирующие падение исходного кода и корректность предложенных реализаций.
Кратко о том, что делает исходный код
def remove_dupsarrarrarr:
Автор пытается пройти по массиву по индексам и для каждого элемента удалить его, если он уже встречался слева вarr[:i]в arr[:i]вarr[:i].arr:i:i:i — срез, содержащий все элементы слева от позиции i, проверка arriii in arr:i:i:i — ищет дубликат слева.Если найден дубликат — выполняется arr.popiii удалениепоиндексуудаление по индексуудалениепоиндексу.for i in rangelen(arr)len(arr)len(arr):
if arriii in arr:i:i:i:
arr.popiii return arr
Ошибки и недостатки
Логическая/ошибка исполнения:
Нельзя безопасно изменять длину списка poppoppop при итерации по rangelen(arr)len(arr)len(arr), потому что range вычислен заранее. После уменьшения длины списка обращение к arriii при дальнейшем i может вызвать IndexError. Пример:
arr = 1,1,21,1,21,1,2 i=0: оставить
i=1: удалить второй 1 → arr = 1,21,21,2 i=2: попытка обратиться к arr222 → IndexError.
Кроме того, удаление по индексу сдвигает оставшиеся элементы, что может привести к пропуску проверки некоторых элементов авторскийподходненадёжныйавторский подход ненадёжныйавторскийподходненадёжный.
Сложность эффективностьэффективностьэффективность:
Для каждого i создаётся срез arr:i:i:i — это Oiii по времени и дополнительная память; затем проверка in — ещё Oiii в худшем случае. Так что одна проверка ≈ Oiii, суммарно On2n^2n2.popiii удаление в середине списка — Onnn сдвиг элементов; в худшем случае суммарно ещё On2n^2n2.Итого алгоритм работает за квадратичное время ииспользуетдополнительнуюпамятьиз−засрезови использует дополнительную память из-за срезовииспользуетдополнительнуюпамятьиз−засрезов. Для больших списков это плохо.Корректная и эффективная реализация ссохранениемпорядкас сохранением порядкассохранениемпорядка
1) Простая и понятная возвращаетновыйсписок,ожидается,чтоэлементыхешируемывозвращает новый список, ожидается, что элементы хешируемывозвращаетновыйсписок,ожидается,чтоэлементыхешируемы:
def remove_dupsarrarrarr:
seen = set res = for x in arr:
if x not in seen:
seen.addxxx res.appendxxx return res
Сложность: Onnn в среднем по времени, Onnn по дополнительной памяти.
2) Вариант, модифицирующий список in-place безсозданияновогосписка,носдополнительныммножествомдляseenбез создания нового списка, но с дополнительным множеством для seenбезсозданияновогосписка,носдополнительныммножествомдляseen:
def remove_dups_inplacearrarrarr:
seen = set write = 0
for x in arr:
if x not in seen:
seen.addxxx arrwritewritewrite = x
write += 1
del arrwrite:write:write: # обрезаем хвост
return arr
Этот вариант экономит место на новый список, но всё равно использует Onnn для множества seen.
3) Если элементы не хешируемы, можно использовать множество ключей keykeykey или fallback на проверку по списку медленнеемедленнеемедленнее:
def remove_dups_keyarr,key=lambdax:xarr, key=lambda x: xarr,key=lambdax:x:
seen = set res = for x in arr:
k = keyxxx if k not in seen:
seen.addkkk res.appendxxx return res
Примеры и замечания
Все приведённые версии сохраняют порядок первых вхождений.Если элементы не хешируемы и у вас нет разумной функции key, придётся использовать квадратичный алгоритм проверкаinпоспискупроверка in по спискупроверкаinпосписку.Если нужна стабильность и допустима дополнительная память — вариант с seen+res наиболее прост и быстрый.Если хотите, могу показать конкретные тесты, демонстрирующие падение исходного кода и корректность предложенных реализаций.