Big-O para bucle anidado

8

Estoy leyendo esta publicación en Big-O
Dice que el siguiente código es O (n ^ 2):

bool ContainsDuplicates(String[] strings)
{
    for(int i = 0; i < strings.Length; i++)
    {
        for(int j = 0; j < strings.Length; j++)
        {
            if(i == j) // Don't compare with self
            {
                continue;
            }

            if(strings[i] == strings[j])
            {
                return true;
            }
        }
    }
    return false;
}

Pero no puedo entender por qué.
El bucle interno hace algo constante.
Entonces es la suma de 1 .... N de una constante. es decir, número constante O (1).
El bucle externo es la suma sobre el O (1).
Entonces me imagino que es n * O (1).

Creo que estoy malentendido algo aquí.
No creo que todos los bucles anidados signifiquen O (n ^ 2), ¿verdad?

usuario10326
fuente
3
¿De dónde sacas la idea de que el circuito interno está "haciendo algo constante"? Es un bucle. Esa es su primera pista de que se necesita O (N).
Paul Tomblin
2
Esto se puede reducir a O (N ^ 2/2) haciendo que el bucle interno comience en i+1lugar de 0. Como está escrito, en el peor de los casos (sin engaños) 1/2 las comparaciones son redundantes.
Peter Rowell
1
O (N ^ 2/2) = O (N ^ 2) (En ambos casos, cuando N llega al infinito, duplicar N significa cuadruplicar el tiempo de ejecución. Eso es todo lo que O (N ^ 2) significa)
David Schwartz
55
Vale la pena señalar que en un lenguaje como C donde está la comparación de cadenas O(N), este código en realidad estaría O(N^2 * M)donde M es la longitud de las cadenas.
Isak Savo
2
La comparación (de cadena de longitud constante) es O (1). N comparaciones es O (N). N ^ 2 comparaciones es O (N ^ 2).
SF.

Respuestas:

18

Tu error es con el bucle interno. Hace algo constante n veces, por lo que es O ( n ). El bucle externo hace el bucle interno n veces, por lo que es O ( n × n ) u O ( n 2  ).

En general, si el número de iteraciones que realiza un bucle depende del tamaño de la entrada, es O ( n ). Y si k tales bucles están anidados, es O ( n k  ).

KeithB
fuente
1
quizás una notación más fácil de entender sería decir que el algoritmo se ejecuta en O (i * j). Aquí ambos bucles iterar sobre la longitud f String, por lo tanto i = j = n donde n es strings.length
Newtopian
1
@Newtopian: Usualmente asumes que las cadenas son de longitud constante; eso es realmente una muy buena aproximación en la práctica.
Donal Fellows
@Donal, de hecho, podemos, con el conocimiento adecuado del conjunto de datos, hacer tales suposiciones, pero como siempre hay excepciones, el análisis de ADN viene a la mente donde la longitud de la cadena puede ser de unos pocos caracteres para un codón a varios mega bytes para un cromosoma completo. Dicho esto, en este caso en particular, la longitud de la cadena no tiene ninguna consecuencia, ya que iteramos las cadenas en sí, no los caracteres. Es decir, a menos que el operador de igualdad se haya sobrecargado a una lógica funky dependiendo de la longitud de los operandos. Sin embargo, dudo que el OP tuviera la intención de llegar tan lejos en su análisis.
Newtopian
@Newtopian: Para ser justos, ContainsDuplicatesno es muy probable que la función / método de la pregunta se use con cromosomas completos.
Donal Fellows
@Donal: probablemente no no :-)
Newtopian
4

Si la longitud de la cadena es n, la prueba if i == jse ejecutará n ^ 2 veces. El orden de un algoritmo es el orden de cualquier parte que tenga el orden más alto. Como 'i' se compara con 'j' n ^ 2 veces, el orden del algoritmo no puede ser menor que O(n^2).

Para una 'n' muy grande, duplicar 'n' cuadruplicará el tiempo de ejecución.

David Schwartz
fuente
¿Pero no es la prueba i==jun O (1)?
user10326
55
La prueba en sí es O (1). El bucle interno ejecuta la prueba 'n' veces, por lo que es O (n * 1). El bucle externo ejecuta el bucle interno 'n' veces, por lo que es O (n * n * 1). Entonces, el código general es O (n ^ 2). Si ejecuta un paso O (1) n ^ 2 veces, eso es O (n ^ 2).
David Schwartz
1
Minor nit: en realidad, para cualquier valor de N que se duplique, se cuadruplicará el tiempo de ejecución; es solo que no importa mucho para la pequeña N.
Peter Rowell
@ Peter: Eso no es cierto. Por ejemplo, pasar de n = 2 a n = 4 puede no duplicar el tiempo de ejecución porque una máquina de 32 bits puede usar el mismo número de recuperaciones de memoria para leer 2 bytes que 4. De manera similar, para n pequeña, la sobrecarga de ingresar a la función puede ser significativo, y eso es constante. Mayor n puede tener una mejor predicción de rama en promedio. Y así.
David Schwartz
55
En este contexto, "bery large 'n'" significa que 'n' tiende hacia el infinito, pero ignora cualquier ineficiencia que pueda ocurrir debido a grandes 'n's (como no encajar en un entero de 32 o 64 bits). El punto de la notación big-O es analizar cómo cambia el rendimiento de un algoritmo a medida que 'n' aumenta, siempre que permanezca dentro de las capacidades de la máquina. (Esto puede ser una objeción en este contexto, pero en general, comprender lo que la notación big-O incluye e ignora es importante para entender lo que significa).
David Schwartz,
2

Estás malinterpretando lo que significa una operación constante.

Una operación constante es una operación que siempre se ejecuta en una cantidad fija de tiempo independiente de la entrada que recibe.

i == j es una operación constante porque ejecuta esta declaración en una cantidad de tiempo fija. Digamos que toma 1 unidad de tiempo.

Pero esta operación constante se realiza (no de valores de i) * (no de valores de j) veces. Digamos que i y j se ejecutan 10 veces cada uno. Luego, por cálculo, se requieren 100 unidades de tiempo para completar i == j cuando se encuentra en un bucle anidado. Por lo tanto, variará a medida que varían los valores de i y j.

Podemos estar seguros de que i == j se realizará en 1 unidad de tiempo, pero no podemos saber cuántas veces se realizará i == j.

Si se realiza n veces, tardará n unidades de tiempo. El bucle externo ejecuta el bucle interno n veces. Entonces, en esencia, i == j las operaciones se realizan n ^ 2 veces.

Todos los bucles anidados significan O (n ^ (no de bucles anidados)). Aquí O significa el límite superior, lo que significa que el código se ejecutará en menos o igual que el valor de O (). Esta es la garantía de que no tomará más tiempo que eso, pero puede tomar menos tiempo, no más.

codecool
fuente
0

Los bucles anidados ejecutan O ( i 1 * i 2 * ... * i n ), donde n es el número de bucles anidados e i x es el número de iteraciones en el bucle x . (O, para decirlo de otra manera, es el producto del número de iteraciones en cada uno de los bucles).

Su par de bucles itera sobre la misma matriz dos veces, que por coincidencia es O (n * n) u O (n 2 ).

Lo que sucede dentro del bucle más interno se trata como constante porque va a suceder siempre. La notación Big-O no se trata realmente de medir el rendimiento del código lineal, se trata más bien de hacer comparaciones relativas de cómo se comportan los algoritmos iterativos cuando se les da n elementos de entrada para tratar. Ciertas operaciones que en realidad no hacen ninguna iteración (como un push o pop en una pila) se clasifican como O (1) porque manejan un elemento de los datos.

Blrfl
fuente
1
no es cierto si i_k no es constante: piense en la línea de barrido de los algos en el peor de los casos, son O (n ^ 2), el mejor de los casos son O (n) (sin contar la clasificación inicial) con algunos bucles internos que están sobre todo o sobre ninguno dependiendo de problema. también implementado quicksort sin recursión es un bucle anidado sin embargo, todavía es O (n * log n)
monstruo de trinquete
La clasificación rápida con selección de pivote aleatorio no es determinista, lo que significa que la cifra O (n log n) es un promedio. Todavía se ajusta lo que dije: i1 = n e i2 = log n .
Blrfl