Сложность связана с несколькими случайными вызовами: вы вызываете его для каждого гена.
Ваш второй пример делает то же самое, но на этот раз они все вызываются в одной и той же функции.
Способ радикально улучшить производительность - уменьшить количество случайных вызовов. Для этого вы можете использовать некоторую математику, чтобы заранее узнать, сколько мутаций получит геном, формула следующая
P(L, n, p) # probability of modifying n-times a genome of size L with a succes p (here p is 1/L)
P(L, n, p) = binomial(n, L) * p**n * (1-p)**(L-n)
Если вы не слишком знакомы с математикой, вот функция python, которая сделает это за вас:
def binomial(n, k):
if 0 <= k <= n:
ntok = ktok = 1
for t in range(1, min(k, n - k) + 1):
ntok *= n; ktok *= t; n -= 1
return ntok // ktok
else: return 0
def P(L, n, p): return binomial(L, n) * p**n * (1-p)**(L-n)
И теперь вы можете предварительно рассчитать это и сохранить его в списке:
proba = [P(L, i, 1/L) for i in range(0, L+1)]
Также я рекомендую частично суммировать его для более легкого использования случайных чисел
probaS = [sum(proba[:k]) for k in range(0, L+1)] + [1]
Теперь вы можете генерировать только одно случайное число, и вы будете напрямую знать, сколько мутаций вам нужно для этого генома:
r = random()
i = 0
while r > probaS[i]: i += 1
В конце цикла i-1 сообщит вам, сколько требуется мутаций.
Теперь вам просто нужно случайным образом выбрать другую часть генома, и это сделано! Вы перешли от L случайных звонков в среднем до 2 или 3.
В базовом тесте, который я провел, сложность времени для L = 50 и 100 000 геномов возросла с 5,74 до 196 мс, так что примерно в 30 раз быстрее.
Мой ответ немного технический, поэтому не стесняйтесь спрашивать, если он не был ясен.