Big-O para loop aninhado

8

Estou lendo este post no Big-O
. Diz que o seguinte código é 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;
}

Mas eu não consigo entender o porquê.
O loop interno faz algo constante.
Portanto, é somatório de 1 .... N de uma constante. ou seja, número constante O (1).
O loop externo é somatório sobre o O (1).
Então, eu imagino que seja n * O (1).

Acho que estou entendendo algo errado aqui.
Eu não acho que todos os loops aninhados significam O (n ^ 2), certo?

user10326
fonte
3
De onde você tira a idéia de que o loop interno está "fazendo algo constante"? É um laço. Essa é sua primeira pista de que é preciso O (N).
Paul Tomblin
2
Isso pode ser reduzido para O (N ^ 2/2) fazendo com que o loop interno inicie em i+1vez de 0. Como está escrito, na pior das hipóteses (sem bobagens) 1/2, as comparações são redundantes.
Peter Rowell
1
O (N ^ 2/2) = O (N ^ 2) (em ambos os casos, quando N vai ao infinito, uma duplicação de N significa um quadruplicar do tempo de execução. Isso é tudo o que O (N ^ 2) significa.)
David Schwartz
5
Vale a pena notar que, em uma linguagem como C em que a comparação de strings é O(N), esse código seria O(N^2 * M)onde M é o comprimento das strings.
Isak Savo
2
A comparação (da cadeia de comprimento constante) é O (1). N comparações é O (N). As comparações de N ^ 2 são O (N ^ 2).
SF.

Respostas:

18

Seu erro é com o loop interno. Faz algo constante n vezes, por isso é O ( n ). O loop externo executa o loop interno n vezes, então é O ( n × n ) ou O ( n 2  ).

Em geral, se o número de iterações que um loop faz depende do tamanho da entrada, é O ( n ). E se k tais loops estão aninhados, é O ( n k  ).

KeithB
fonte
1
talvez uma notação mais fácil de entender seja dizer que o algoritmo é executado em O (i * j). Aqui ambas as espiras iterar ao longo do comprimento da corda f, assim = i j = n, onde n é strings.length
Newtopian
1
@ Newtopian: Você geralmente assume que as cordas têm comprimento constante; isso é realmente uma boa aproximação na prática.
Donal Fellows
@ Donal, de fato, podemos, com o conhecimento adequado do conjunto de dados, fazer tais suposições, mas como sempre há exceções, a análise de DNA vem à mente onde o comprimento da cadeia pode ter alguns caracteres para um códon com vários mega bytes para um cromossomo completo. Dito isso, neste caso, em particular, o comprimento da string não tem importância, pois iteramos as próprias strings, não os caracteres. Ou seja, a menos que o operador de igualdade tenha sido sobrecarregado para alguma lógica descolada, dependendo do comprimento dos operandos. Duvido, contudo, que o OP pretendesse ir tão longe em sua análise.
Newtopian
@ Newtopian: Para ser justo, a ContainsDuplicatesfunção / método da pergunta provavelmente não será usada com cromossomos completos.
Donal Fellows
@Donal: provavelmente não, não :-)
Newtopian
4

Se o comprimento da string for n, o teste if i == jserá executado n ^ 2 vezes. A ordem de um algoritmo é a ordem de qualquer parte dele que tenha a ordem mais alta. Como 'i' é comparado a 'j' n ^ 2 vezes, a ordem do algoritmo não pode ser menor que O(n^2).

Para um 'n' muito grande, dobrar 'n' quadruplicará o tempo de execução.

David Schwartz
fonte
Mas o teste não é i==jum O (1)?
user10326
5
O teste em si é O (1). O loop interno executa o teste 'n' vezes, então é O (n * 1). O loop externo executa o loop interno 'n' vezes, então é O (n * n * 1). Portanto, o código geral é O (n ^ 2). Se você executar uma etapa O (1) n ^ 2 vezes, é O (n ^ 2).
David Schwartz
1
Nit menor: na verdade, para qualquer valor de N dobrando ele irá quadruplicar o tempo de execução - é só que isso não importa muito para pequenas N.
Peter Rowell
@ Peter: Isso não é verdade. Por exemplo, passar de n = 2 para n = 4 pode não dobrar o tempo de execução porque uma máquina de 32 bits pode usar o mesmo número de buscas de memória para ler 2 bytes que 4. Da mesma forma, para n pequeno, a sobrecarga de entrada na função pode ser significativo, e isso é constante. N maior pode ter uma melhor previsão de ramificação, em média. E assim por diante.
David Schwartz
5
Nesse contexto, "bery large 'n'" significa que 'n' tende ao infinito, mas ignora quaisquer ineficiências que possam ocorrer devido a grandes 'n's (como não caber em um número inteiro de 32 ou 64 bits). O objetivo da notação big-O é analisar como o desempenho de um algoritmo muda à medida que o 'n' aumenta, desde que permaneça dentro dos recursos da máquina. (Este pode ser um trocadilho neste contexto, mas, em geral, entendendo o que a notação big-O inclui e ignora é importante para entender o que significa.)
David Schwartz
2

Você está interpretando mal o que significa uma operação constante.

Uma operação constante é uma operação que sempre executa em uma quantidade fixa de tempo, independentemente da entrada que recebe.

i == j é uma operação constante porque executa esta instrução em um período fixo de tempo. Vamos dizer que leva 1 unidade de tempo.

Mas essa operação constante é realizada (não dos valores de i) * (não dos valores de j) vezes. Digamos que iej sejam executados 10 vezes cada. Então, pelo cálculo, leva 100 unidades de tempo para a conclusão de i == j quando em um loop aninhado. Portanto, variará conforme os valores de iej.

Podemos ter certeza de que i == j será executado em 1 unidade de tempo, mas não podemos saber quantas vezes i == j será executado.

Se for realizado n vezes, levará n unidades de tempo. O loop externo executa o loop interno n vezes. Então, em essência, i == j operações são feitas n ^ 2 vezes.

Todos os loops aninhados significam O (n ^ (nenhum dos loops aninhados)). Aqui O significa o limite superior, o que significa que o código será executado menor ou igual ao valor de O (). Esta é a garantia de que não levará mais tempo do que isso, mas pode demorar menos e não ser maior.

codecool
fonte
0

Loops aninhados executam O ( i 1 * i 2 * ... * i n ), onde n é o número de loops aninhados e i x é o número de iterações no loop x . (Ou, em outras palavras, é o produto do número de iterações em cada um dos loops.)

Seu par de loops itera duas vezes na mesma matriz, que por coincidência é O (n * n) ou O (n 2 ).

O que acontece dentro do loop mais interno é tratado como constante, porque sempre acontece. A notação Big-O não é realmente medir o desempenho do código linear, é mais fazer comparações relativas de como os algoritmos iterativos se comportam quando recebem n itens de entrada para lidar. Certas operações que na verdade não fazem iterações (como push ou pop em uma pilha) são tingidas como O (1) porque elas manipulam um elemento dos dados.

Blrfl
fonte
1
não é verdade se i_k não for constante: pense nos algos de varredura no pior caso, eles são O (n ^ 2) no melhor caso, são O (n) (sem contar a classificação inicial), com alguns loops internos sendo sobre todos ou sobre nenhum, dependendo do problema. também quick implementado sem recursão é um loop aninhado no entanto, ainda é O (N log N *)
roquete aberração
Quicksort com seleção de pivô aleatório não é determinístico, o que significa que o número O (n log n) é uma média. Ele ainda se encaixa o que eu disse: i1 = n e i2 = log n .
Blrfl 26/09/11