Estoy tratando de enseñarme a mí mismo cómo calcular la notación BigO para una función arbitraria. Encontré esta función en un libro de texto. El libro afirma que la función es O (n 2 ). Da una explicación de por qué esto es así, pero estoy luchando por seguirlo. Me pregunto si alguien podría mostrarme las matemáticas detrás de por qué esto es así. Básicamente, entiendo que es algo menor que O (n 3 ), pero no pude aterrizar independientemente en O (n 2 )
Supongamos que se nos dan tres secuencias de números, A, B y C. Asumiremos que ninguna secuencia individual contiene valores duplicados, pero que puede haber algunos números que están en dos o tres de las secuencias. El problema de la desunión del conjunto de tres vías es determinar si la intersección de las tres secuencias está vacía, es decir, que no hay un elemento x tal que x ∈ A, x ∈ B y x ∈ C.
Por cierto, este no es un problema de tarea para mí, ese barco ha navegado hace años :), solo yo tratando de ser más inteligente.
def disjoint(A, B, C):
"""Return True if there is no element common to all three lists."""
for a in A:
for b in B:
if a == b: # only check C if we found match from A and B
for c in C:
if a == c # (and thus a == b == c)
return False # we found a common value
return True # if we reach this, sets are disjoint
[Editar] De acuerdo con el libro de texto:
En la versión mejorada, no es simplemente que ahorremos tiempo si tenemos suerte. Afirmamos que el peor tiempo de ejecución para disjuntas es O (n 2 ).
La explicación del libro, que me cuesta seguir, es esta:
Para tener en cuenta el tiempo de ejecución general, examinamos el tiempo dedicado a ejecutar cada línea de código. La gestión del bucle for sobre A requiere tiempo O (n). La gestión del ciclo for sobre B representa un total de tiempo O (n 2 ), ya que ese ciclo se ejecuta en diferentes momentos. La prueba a == b se evalúa O (n 2 ) veces. El resto del tiempo dedicado depende de cuántos pares coincidentes (a, b) existen. Como hemos señalado, existen a lo sumo n de tales pares, por lo que la administración del bucle sobre C y los comandos dentro del cuerpo de ese bucle, se usan como máximo O (n 2 ). El tiempo total empleado es O (n 2 ).
(Y para dar el crédito adecuado ...) El libro es: Estructuras de datos y algoritmos en Python por Michael T. Goodrich et. todos, Wiley Publishing, pág. 135
[Editar] Una justificación; A continuación se muestra el código antes de la optimización:
def disjoint1(A, B, C):
"""Return True if there is no element common to all three lists."""
for a in A:
for b in B:
for c in C:
if a == b == c:
return False # we found a common value
return True # if we reach this, sets are disjoint
En lo anterior, puede ver claramente que esto es O (n 3 ), porque cada ciclo debe ejecutarse al máximo. El libro afirmaría que en el ejemplo simplificado (dado primero), el tercer bucle es solo una complejidad de O (n 2 ), por lo que la ecuación de complejidad es como k + O (n 2 ) + O (n 2 ) que finalmente produce O (n 2 ).
Si bien no puedo probar que este sea el caso (por lo tanto, la pregunta), el lector puede estar de acuerdo en que la complejidad del algoritmo simplificado es al menos menor que la original.
[Editar] Y para demostrar que la versión simplificada es cuadrática:
if __name__ == '__main__':
for c in [100, 200, 300, 400, 500]:
l1, l2, l3 = get_random(c), get_random(c), get_random(c)
start = time.time()
disjoint1(l1, l2, l3)
print(time.time() - start)
start = time.time()
disjoint2(l1, l2, l3)
print(time.time() - start)
Rendimientos:
0.02684807777404785
0.00019478797912597656
0.19134306907653809
0.0007600784301757812
0.6405444145202637
0.0018095970153808594
1.4873297214508057
0.003167390823364258
2.953308343887329
0.004908084869384766
Como la segunda diferencia es igual, la función simplificada es de hecho cuadrática:
[Editar] Y aún más pruebas:
Si asumo el peor de los casos (A = B! = C),
if __name__ == '__main__':
for c in [10, 20, 30, 40, 50]:
l1, l2, l3 = range(0, c), range(0,c), range(5*c, 6*c)
its1 = disjoint1(l1, l2, l3)
its2 = disjoint2(l1, l2, l3)
print(f"iterations1 = {its1}")
print(f"iterations2 = {its2}")
disjoint2(l1, l2, l3)
rendimientos:
iterations1 = 1000
iterations2 = 100
iterations1 = 8000
iterations2 = 400
iterations1 = 27000
iterations2 = 900
iterations1 = 64000
iterations2 = 1600
iterations1 = 125000
iterations2 = 2500
Usando la segunda prueba de diferencia, el resultado del peor de los casos es exactamente cuadrático.