Итак, проблема в том
Найдите повторяющиеся записи (2-е вхождение и далее) в заданном массиве numpy и пометьте их как True. Первое появление должно быть ложным.
И мое решение:
import numpy as np
def duplicates(a):
uniques = np.unique(a)
result = []
for i in a:
if i not in uniques:
result.append(True)
else:
result.append(False)
uniques = np.delete(uniques, np.where(uniques == i))
return result
np.random.seed(100)
a = np.random.randint(0, 5, 10)
print('Array: ', a)
print(duplicates(a))
Какие улучшения я могу сделать в этой программе?
3 ответа
Я бы сказал, что ваш код — хороший код. В этом нет ничего плохого. Единственное, что вы можете написать более или менее один и тот же алгоритм намного короче, и это, по сути, означает, что вам нужно реорганизовать весь свой код до одной строки.
Итак, прежде чем взглянуть на упрощенную версию того, что вы написали, позвольте мне просто указать, что имена переменных должны быть описательными, и если вы используете сокращения или однобуквенные имена, вам следует придерживаться наиболее использованные условные обозначения. Почему я это говорю? Хорошо, i
обычно называют неважным индекс переменная, но в вашем цикле у вас есть for i in a
, а также i
является элементом массива … Так что, хотите верьте, хотите нет, люди, просматривающие остальную часть вашего кода, могут быть сбиты с толку тем фактом, что i
не является индексом.
Промежуточный шаг
Итак, прежде чем переходить к однострочному, на самом деле удобочитаемый (т.е. это хороший код Python) давайте посмотрим на более понятный промежуточный шаг. Вместо того, чтобы вычислять все уникальные значения и затем удалять оттуда (удаление из середины списка обычно «медленное»), вы можете пойти другим путем и получить список, в котором сохраняются все найденные вами уникальные значения:
def duplicates(arr):
result, uniques = [], []
for elem in arr:
if elem in uniques:
result.append(True)
else:
result.append(False)
uniques.append(elem)
return result
Следующий промежуточный шаг
А теперь еще раз взгляните на то, что я написал. Обратите внимание, что оба в if
и else
Я добавляю к result
Переменная. Единственное, что действительно «особено», — это добавление к uniques
, что происходит только на else
, чтобы мы могли написать .append
безоговорочно и добавить к uniques
только при необходимости:
def duplicates(arr):
result, uniques = [], []
for elem in arr:
result.append(elem in uniques)
if elem in not uniques:
uniques.append(elem)
return result
Окончательное решение (после переосмысления алгоритма)
Дайте мне знать, если вы понимаете предложенное мной решение, и я расширю его, если вам нужно 🙂
def duplicates(arr):
return [elem in arr[:i] for i, elem in enumerate(arr)]
Обратите внимание, что это понимание списка читается как «вернуть список, в котором указано, может ли i-й элемент быть найден в первых элементах i-1».
Главное здесь — правильно сформулировать результат. Если вы правильно рассмотрите код Python, английский язык и код станут одним и тем же.
Когда у вас относительно простой for
цикл, который строит список с последовательными .append
, это знак, чтобы попробовать и посмотреть, сможете ли вы использовать понимание списка.
Я также использую enumerate
встроенный (узнайте об этом здесь) для обхода массива, одновременно просматривая значения и индексы.
РЕДАКТИРОВАТЬ:
Как отмечали другие ответы (а также см. Комментарии к этому ответу), этот однострочный алгоритм работает с временной сложностью O (n ^ 2), что не лучшее, что вы можете сделать. Тем не менее, эта единственная строка Python показывает вам, как далеко вы можете зайти с простой идеей / наивным алгоритмом и оптимизированным использованием основных функций Python 🙂
Как упоминалось в комментариях к другому ответу, вы должны использовать set
s для проверки членства, когда это возможно. Совместите это с наращиванием uniques
вместо того, чтобы урезать его, мы получаем очень простую реализацию:
def duplicates(numbers):
uniques = set()
result = []
for num in numbers:
result.append(num in uniques)
uniques.add(num)
return result
Эта реализация duplicates
краток, легко читается и запускается в O (n) вместо O (n ^ 2), что является основным отличием. Однострочники — это забавное упражнение, которое может научить вас некоторым интересным аспектам Python, и я понимаю их привлекательность, но часто это не лучший вариант. Не стоит жертвовать удобочитаемостью и производительностью ради написания меньшего количества строк кода.
РЕДАКТИРОВАТЬ: тестирование производительности
Как и просили в комментариях, я протестировал производительность трех разных подходов:
def duplicates_set(numbers):
uniques = set()
result = []
for num in numbers:
result.append(num in uniques)
uniques.add(num)
return result
def duplicates_list(numbers):
result, uniques = [], []
for elem in numbers:
if elem in uniques:
result.append(True)
else:
result.append(False)
uniques.append(elem)
return result
def duplicates_oneliner(numbers):
return [elem in numbers[:i] for i, elem in enumerate(numbers)]
Не то чтобы следующий подход для duplicates_list
в любом случае следует избегать, так как он проверяет elem
членство в uniques
дважды.
def duplicates_list(numbers):
result, uniques = [], []
for elem in numbers:
result.append(elem in uniques)
if elem not in uniques:
uniques.append(elem)
return result
Вот мой код для тестирования производительности с использованием timeit
модуль. Все предоставленные выходные данные были произведены этим кодом.
num_range: (int, int)
random_nums: int
executions: int
np.random.seed(100)
a = np.random.randint(*num_range, random_nums)
from timeit import timeit
print(f"{executions} executions with {random_nums} random numbers in range {num_range}")
print("duplicates_set:", "t" * 2, timeit(stmt=lambda: duplicates_set(a), number=executions))
print("duplicates_list:", "t" * 2, timeit(stmt=lambda: duplicates_list(a), number=executions))
print("duplicates_oneliner:t", timeit(stmt=lambda: duplicates_oneliner(a), number=executions))
Начиная с 100_000 выполнений с параметрами из исходного вопроса, мы получаем следующий результат:
100000 executions with 10 random numbers in range (0, 5)
duplicates_set: 0.22261620000000001
duplicates_list: 0.18974730000000006
duplicates_oneliner: 2.9783912
Мы ясно видим, что list
реализация лучше всего работает для небольшого количества чисел и небольшого диапазона случайных чисел. Однострочник уже находится в очень невыгодном положении.
Увеличивая длину массива случайных чисел до 1_000, мы получаем такой результат:
1000 executions with 1000 random numbers in range (0, 5)
duplicates_set: 0.1603748
duplicates_list: 0.13001250000000003
duplicates_oneliner: 3.1837349
Кажется, что длина массива сама по себе не влияет на относительную производительность duplicates_set
а также duplicates_list
. Пока диапазон чисел невелик, list
выходит на первое место. Относительная производительность оставалась неизменной до 1_000_000 элементов, что имеет смысл, поскольку размер uniques
ограничен диапазоном чисел.
Как только мы начнем увеличивать num_range
, то set
реализация действительно начинает сиять, так как она просто не заботится о размере uniques
когда дело доходит до времени выполнения:
1000 executions with 1000 random numbers in range (0, 10)
duplicates_set: 0.15972010000000003
duplicates_list: 0.15932980000000002
duplicates_oneliner: 3.1950098
Для ряда (0, 10)
, set
а также list
реализация примерно такая же.
В диапазоне (0, 1_000)
в list
реализация уже превосходит однострочный:
1000 executions with 1000 random numbers in range (0, 1000)
duplicates_set: 0.15644170000000002
duplicates_list: 3.4406095
duplicates_oneliner: 3.1854200999999995
Однако однострочник сразу выходит из конкурса, как только ввод становится больше, даже при небольших диапазонах чисел:
10 executions with 100000 random numbers in range (0, 10)
duplicates_set: 0.1576303
duplicates_list: 0.24761889999999998
duplicates_oneliner: 30.1282213
Конечный результат именно такой, как и следовало ожидать:
duplicates_list
действительно начинает бороться, когда количество уникальных чисел увеличивается, посколькуif elem in uniques
становится все дороже.duplicates_oneliner
полностью не работает с длинными списками ввода, так какelem in numbers[:i]
становится очень быстро.duplicates_set
уступает только при небольшом количестве уникальных чисел, но в масштабе он превосходит обе другие реализации с огромным отрывом, посколькуif elem in uniques
можно проверить в постоянное время.
Я согласен с тем, что ваше решение краткое и разборчивое, и мне нравится ваше безоговорочное
uniques.add
, отлично сработано! +1 от меня 🙂— РГО
Однако вы, похоже, намекаете, что мое решение не читается и не работает. Хотя это факт, что его сложность составляет O (n ^ 2) и, следовательно, менее производительна, я думаю, мы можем согласиться с тем, что одна строка по-прежнему очень разборчива и хорошо демонстрирует, как далеко вы можете зайти с довольно наивным алгоритмом. Наконец, мне интересно, если решение, использующее
set
действительно O (n), если в массиве слишком много уникальных элементов. Я бы ожидал либоset
использовать путь к большой памяти быть O (1) при вставке и проверке членства или быть чем-то вроде O (log n) для проверки членства, нет? У вас есть ссылки для меня, чтобы узнать? 🙂— РГО
- 1
@RGS В этом ты прав
set
s используют больше памяти, чем списки, поскольку значения хеш-значений должны храниться в дополнение к самим элементам. Сравнение памяти на SO. Однако ваше предположение о том, что проверка членства не является O (1), неверно.set
песокdict
s) реализованы с использованием хэш-таблиц, обеспечивающих поиск и вставки O (1). Глава 4 в Высокопроизводительный Python: Словари и наборы— рискованный пингвин
- 2
@RGS Это безусловное
add
Кстати, это то, что я имел в виду, говоря «Плюс, тогда вы можете снять лишнюю проверку». После этого я подозреваю, что однострочный и набор-решение примерно одинаково читаемы в целом, а однострочный более читабельный для людей, привыкших к ним 🙂— Мануэль
- 1
Ну что ж, теперь к моему ответу добавлены тесты (те же случаи, что и у вас).
— Мануэль
Проверить unique
с документация опять же, он предлагает дать вам индексы:
def duplicates(a):
uniques = np.unique(a, True)[1]
result = a == a
result[uniques] = False
return result
В тестах (повторное использование рискованного пингвина) он средний для крошечного оригинального случая, но, безусловно, самый быстрый для всех более крупных случаев:
100000 executions with 10 random numbers in range (0, 5)
duplicates_original 17.807 17.395 18.284 seconds
duplicates_set 0.726 0.686 0.721 seconds
duplicates_list 0.631 0.548 0.587 seconds
duplicates_oneliner 7.087 6.946 7.822 seconds
duplicates_numpy 2.017 2.177 2.155 seconds
1000 executions with 1000 random numbers in range (0, 5)
duplicates_original 6.904 6.862 6.617 seconds
duplicates_set 0.520 0.535 0.584 seconds
duplicates_list 0.385 0.363 0.341 seconds
duplicates_oneliner 7.241 7.927 7.244 seconds
duplicates_numpy 0.062 0.065 0.063 seconds
1000 executions with 1000 random numbers in range (0, 10)
duplicates_original 6.720 6.885 6.801 seconds
duplicates_set 0.656 0.542 0.576 seconds
duplicates_list 0.459 0.511 0.509 seconds
duplicates_oneliner 7.556 7.064 7.637 seconds
duplicates_numpy 0.080 0.082 0.073 seconds
1000 executions with 1000 random numbers in range (0, 1000)
duplicates_original 25.222 26.108 24.767 seconds
duplicates_set 0.682 0.757 0.759 seconds
duplicates_list 11.608 11.651 11.196 seconds
duplicates_oneliner 8.320 7.949 7.608 seconds
duplicates_numpy 0.101 0.110 0.102 seconds
10 executions with 100000 random numbers in range (0, 10)
duplicates_original 6.577 6.658 6.637 seconds
duplicates_set 0.609 0.585 0.603 seconds
duplicates_list 0.514 0.524 0.508 seconds
duplicates_oneliner 36.337 36.677 35.764 seconds
duplicates_numpy 0.082 0.083 0.083 seconds
Код эталонного теста:
from timeit import timeit
import numpy as np
def duplicates_original(a):
uniques = np.unique(a)
result = []
for i in a:
if i not in uniques:
result.append(True)
else:
result.append(False)
uniques = np.delete(uniques, np.where(uniques == i))
return result
def duplicates_set(numbers):
uniques = set()
result = []
for num in numbers:
result.append(num in uniques)
uniques.add(num)
return result
def duplicates_list(numbers):
result, uniques = [], []
for elem in numbers:
if elem in uniques:
result.append(True)
else:
result.append(False)
uniques.append(elem)
return result
def duplicates_oneliner(numbers):
return [elem in numbers[:i] for i, elem in enumerate(numbers)]
def duplicates_numpy(a):
uniques = np.unique(a, True)[1]
result = a == a
result[uniques] = False
return result
def benchmark(num_range: (int, int), random_nums: int, executions: int):
np.random.seed(100)
a = np.random.randint(*num_range, random_nums)
print(f"{executions} executions with {random_nums} random numbers in range {num_range}")
solutions = [
duplicates_original,
duplicates_set,
duplicates_list,
duplicates_oneliner,
duplicates_numpy,
]
# Correctness
expect = list(solutions[0](a))
for solution in solutions:
assert list(solution(a)) == expect
# Speed
for solution in solutions:
ts = ['%6.3f' % timeit(stmt=lambda: solution(a), number=executions)
for _ in range(3)]
print(solution.__name__.ljust(20), *ts, 'seconds')
print()
benchmark((0, 5), 10, 100000)
benchmark((0, 5), 1000, 1000)
benchmark((0, 10), 1000, 1000)
benchmark((0, 1000), 1000, 1000)
benchmark((0, 10), 100000, 10)
Это тоже хорошее решение! Важно знать функции вашей библиотеки 🙂 +1
— РГО
В этом случае это должно быть False, True, True. Значение False присваивается только при первом появлении значения в массиве. Спасибо за ваши объяснения и советы, я очень ценю участие в этом сообществе 🙂
— Левон Аветисян
@LevonAvetisyan ага, я более спокойно посмотрел на ваш код и, конечно же, это понял!
— РГО
Лучше сделай
uniques
аset
.— Мануэль
@Manuel лучшая производительность при проверке наличия элемента? (Разве спектакль не будет Добавлять новый элемент будет хуже?) Или почему вы это предлагаете?
— РГО
Да, все это делается за O (n) вместо O (n ^ 2). Кроме того, тогда вы можете снять дополнительную проверку.
— Мануэль
Показывать 1 больше комментариев