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.
Respuestas:
El libro es correcto, y proporciona un buen argumento. Tenga en cuenta que los tiempos no son un indicador confiable de la complejidad algorítmica. Los tiempos solo podrían considerar una distribución de datos especial, o los casos de prueba podrían ser demasiado pequeños: la complejidad algorítmica solo describe cómo el uso de recursos o el tiempo de ejecución se extiende más allá de un tamaño de entrada adecuadamente grande.
El libro argumenta que la complejidad es O (n²) porque la
if a == b
rama se ingresa como máximo n veces. Esto no es obvio porque los bucles todavía se escriben como anidados. Es más obvio si lo extraemos:Esta variante utiliza generadores para representar resultados intermedios.
AB
, tendremos a lo sumo n elementos (debido a la garantía de que las listas de entrada no contendrán duplicados), y producir el generador requiere complejidad O (n²).ABC
implica un bucle sobre el generadorAB
de longitud n yC
de longitud n , de modo que su complejidad algorítmica es también O (n²).Debido a que los pares de listas de entrada se pueden verificar secuencialmente, se deduce que determinar si cualquier número de listas es disjunto puede hacerse en tiempo O (n²).
Este análisis es impreciso porque supone que todas las listas tienen la misma longitud. Podemos decir con mayor precisión que
AB
tiene como máximo min (| A |, | B |) y su producción tiene complejidad O (| A | • | B |). ProducirABC
tiene complejidad O (min (| A |, | B |) • | C |). La complejidad total depende de cómo se ordenan las listas de entrada. Con | A | ≤ | B | ≤ | C | obtenemos la complejidad total en el peor de los casos de O (| A | • | C |).Tenga en cuenta que las ganancias de eficiencia son posibles si los contenedores de entrada permiten pruebas rápidas de membresía en lugar de tener que repetir todos los elementos. Este podría ser el caso cuando se ordenan para que se pueda realizar una búsqueda binaria, o cuando son conjuntos hash. Sin bucles anidados explícitos, esto se vería así:
o en la versión basada en generador:
fuente
n
variable mágica y hablamos de las variables reales en juego.len(a) == len(b) == len(c)
, aunque es cierto en el contexto del análisis de la complejidad del tiempo, tiende a confundir la conversación.Tenga en cuenta que si todos los elementos son diferentes en cada una de las listas que se supone, puede iterar C solo una vez para cada elemento en A (si hay un elemento en B que es igual). Entonces el bucle interno es O (n ^ 2) total
fuente
Es una información muy importante.
De lo contrario, el peor de los casos de la versión optimizada sería O (n³), cuando A y B son iguales y contienen un elemento duplicado n veces:
que salidas:
Básicamente, los autores suponen que el peor caso de O (n³) no debería suceder (¿por qué?), Y "prueban" que el peor de los casos es ahora O (n²).
La optimización real sería utilizar conjuntos o dictados para probar la inclusión en O (1). En ese caso,
disjoint
sería O (n) para cada entrada.fuente
key in dict
, tal como lo hicieron los autores. : - / En mi defensa, creo que es mucho más difícil encontrar un dict conn
claves yn
colisiones hash que simplemente crear una lista conn
valores duplicados. Y con un conjunto o dict, tampoco puede haber ningún valor duplicado. Por lo tanto, el peor de los casos es O (n²). Actualizaré mi respuesta.Para poner las cosas en los términos que usa su libro:
Creo que no tiene ningún problema para entender que la verificación para
a == b
O es el peor de los casos (n 2 ).Ahora, en el peor de los casos para el tercer bucle, cada
a
enA
cuenta en un partidoB
, por lo que el tercer bucle se llama cada vez. En el caso dea
que no existaC
, se ejecutará por todo elC
conjunto.En otras palabras, es 1 vez por cada
a
y 1 vez por cadac
, o n * n. O (n 2 )Entonces, está el O (n 2 ) + O (n 2 ) que su libro señala.
fuente
El truco del método optimizado es cortar esquinas. Solo si a y b coinciden, se dará un vistazo a c. Ahora puede darse cuenta de que, en el peor de los casos, aún tendría que evaluar cada c. Esto no es verdad.
Probablemente piense que el peor de los casos es que cada verificación para a == b resulta en una ejecución sobre C porque cada verificación para a == b devuelve una coincidencia. Pero esto no es posible porque las condiciones para esto son contradictorias. Para que esto funcione necesitaría una A y una B que contengan los mismos valores. Se pueden ordenar de manera diferente, pero cada valor en A debería tener un valor coincidente en B.
Ahora aquí está el pateador. No hay forma de organizar estos valores para que para cada a tenga que evaluar todas las b antes de encontrar su coincidencia.
Esto se haría instantáneamente porque los 1 coincidentes son el primer elemento de ambas series. Qué pasa
Eso funcionaría para la primera ejecución sobre A: solo el último elemento en B produciría un golpe. Pero la siguiente iteración sobre A ya tendría que ser más rápida porque el último lugar en B ya está ocupado por 1. Y, de hecho, esta vez solo tomaría cuatro iteraciones. Y esto mejora un poco con cada próxima iteración.
Ahora que no soy matemático, no puedo probar que esto terminará en O (n2), pero puedo sentirlo en mis obstrucciones.
fuente
O(n^2)
bucles separados ; que da en generalO(n^2)
(las constantes se ignoran).Al principio estaba desconcertado, pero la respuesta de Amon es realmente útil. Quiero ver si puedo hacer una versión realmente concisa:
Para un valor dado de
a
inA
, la función se comparaa
con cada posibleb
inB
, y lo hace solo una vez. Entonces, para un hecho,a
se realizaa == b
exactamenten
veces.B
no contiene ningún duplicado (ninguna de las listas lo hace), por lo que para un determinadoa
habrá como máximo una coincidencia. (Esa es la clave). Cuando haya una coincidencia,a
se comparará con todas las posiblesc
, lo que significa quea == c
se lleva a cabo exactamente n veces. Donde no hay coincidencia,a == c
no sucede en absoluto.Entonces, para un hecho
a
, hayn
comparaciones o2n
comparaciones. Esto sucede para todosa
, por lo que el mejor caso posible es (n²) y el peor es (2n²).TLDR: cada valor de
a
se compara con cada valor deb
y con cada valor dec
, pero no con cada combinación deb
yc
. Los dos problemas se suman, pero no se multiplican.fuente
Piénselo de esta manera, algunos números pueden estar en dos o tres de las secuencias, pero el caso promedio de esto es que para cada elemento en el conjunto A, se realiza una búsqueda exhaustiva en b. Se garantiza que cada elemento del conjunto A se repetirá, pero implica que menos de la mitad de los elementos del conjunto b se repetirán.
Cuando los elementos del conjunto b se repiten, se produce una iteración si hay una coincidencia. Esto significa que el caso promedio de esta función disjunta es O (n2), pero el peor caso absoluto podría ser O (n3). Si el libro no entrara en detalles, probablemente le daría un caso promedio como respuesta.
fuente