Big O, como você calcula / aproximá-la?

votos
772

A maioria das pessoas com uma licenciatura em CS, certamente sabe o Big O significa . Ela nos ajuda a medir como (in) eficiente um algoritmo realmente é e se você sabe em qual categoria o problema que você está tentando resolver estabelece em que você pode descobrir se ainda é possível para espremer aquela pequena performance extra. 1

Mas estou curioso, como você calcular ou aproximar a complexidade de seus algoritmos?

1 , mas como se costuma dizer, não exagere, otimização prematura é a raiz de todo o mal , e otimização sem causa justificada deve merecer esse nome também.

Publicado 06/08/2008 em 11:18
fonte usuário
Em outras línguas...                            


23 respostas

votos
1k

Eu sou um professor assistente na minha universidade local sobre as Estruturas de Dados e Algoritmos curso. Vou fazer o meu melhor para explicá-lo aqui em termos simples, mas seja advertido que este tema tem meus alunos um par de meses para finalmente entender. Você pode encontrar mais informações sobre o capítulo 2 das Estruturas de Dados e Algoritmos em Java livro.


Não há procedimento mecânico que pode ser usado para obter o BigOh.

Como um "livro de receitas", para obter o BigOh de um pedaço de código que você primeiro precisa entender que você está criando uma fórmula matemática para contar quantos passos de computações são executadas dada uma entrada de algum tamanho.

O objetivo é simples: para comparar algoritmos de um ponto de vista teórico, sem a necessidade de executar o código. Quanto menor o número de passos, mais rápido o algoritmo.

Por exemplo, digamos que você tem este pedaço de código:

int sum(int* data, int N) {
    int result = 0;               // 1

    for (int i = 0; i < N; i++) { // 2
        result += data[i];        // 3
    }

    return result;                // 4
}

Esta função retorna a soma de todos os elementos do array, e queremos criar uma fórmula para contar o complexidade computacional dessa função:

Number_Of_Steps = f(N)

Portanto, temos f(N), uma função para contar o número de passos computacionais. A entrada da função é o tamanho da estrutura a processar. Isso significa que esta função é chamada, tais como:

Number_Of_Steps = f(data.length)

O parâmetro Ntoma o data.lengthvalor. Agora precisamos da definição real da função f(). Isso é feito a partir do código-fonte, no qual cada linha interessante é numerado de 1 a 4.

Há muitas formas de calcular o BigOh. Deste ponto em diante vamos supor que cada frase que não depende do tamanho dos dados de entrada leva uma constante Cnúmero de passos computacionais.

Nós vamos adicionar o número individual de etapas da função, e nem a declaração de variável local nem a instrução de retorno depende do tamanho da datamatriz.

Isso significa que as linhas 1 e 4 leva montante C de passos cada, e a função é um pouco como este:

f(N) = C + ??? + C

A próxima parte é para definir o valor da fordeclaração. Lembre-se que estamos a contar o número de passos computacionais, o que significa que o corpo da fordeclaração é executado Nvezes. Isso é o mesmo que adicionar C, Nvezes:

f(N) = C + (C + C + ... + C) + C = C + N * C + C

Não existe uma regra mecânica para contar quantas vezes o corpo do foré executado, é preciso contá-lo, olhando para o que o código faz. Para simplificar os cálculos, estamos ignorando a variável de inicialização, condição e incremento partes do forcomunicado.

Para obter o BigOh real precisamos da análise assintótica da função. Isto é mais ou menos feito assim:

  1. Tire todas as constantes C.
  2. De f()tirar o polynomium em sua standard form.
  3. Divida os termos do polynomium e classificá-los pela taxa de crescimento.
  4. Mantenha a que cresce mais quando Nabordagens infinity.

O nosso f()tem dois termos:

f(N) = 2 * C * N ^ 0 + 1 * C * N ^ 1

Tirando todas as Cconstantes e peças redundantes:

f(N) = 1 + N ^ 1

Desde o último termo é a que cresce mais quando f()se aproxima do infinito (pensar em limites ), este é o argumento BigOh, ea sum()função tem um BigOh de:

O(N)

Existem alguns truques para resolver alguns dos mais difíceis: usar somatórios sempre que puder.

Como exemplo, este código pode ser facilmente resolvido usando somatórios:

for (i = 0; i < 2*n; i += 2) {  // 1
    for (j=n; j > i; j--) {     // 2
        foo();                  // 3
    }
}

A primeira coisa que precisava ser feita é a ordem de execução de foo(). Enquanto o usual é para ser O(1), você precisa perguntar a seus professores sobre o assunto. O(1)meios (quase, principalmente) constantes C, independente do tamanho N.

A fordeclaração sobre o número frase é complicado. Enquanto que o índice termina no 2 * N, o incremento é feito por dois. Isso significa que o primeiro foré executado apenas Npassos, e precisamos dividir a contagem por dois.

f(N) = Summation(i from 1 to 2 * N / 2)( ... ) = 
     = Summation(i from 1 to N)( ... )

O número sentença dois é ainda mais complicado, uma vez que depende do valor de i. Dê uma olhada: o índice i assume os valores: 0, 2, 4, 6, 8, ..., 2 * N, eo segundo forsão executadas: N vezes a primeira, N - 2 O segundo, N - 4 o terceiro ... até à fase N / 2, em que o segundo fornunca é executado.

Na fórmula, que significa:

f(N) = Summation(i from 1 to N)( Summation(j = ???)(  ) )

Mais uma vez, estamos contando o número de passos . E, por definição, cada somatório deve sempre começar em um, e terminam em um número maior-ou-igual do que um.

f(N) = Summation(i from 1 to N)( Summation(j = 1 to (N - (i - 1) * 2)( C ) )

(Estamos supondo que foo()é O(1)e toma Cetapas.)

Temos um problema aqui: quando iassume o valor N / 2 + 1para cima, o somatório interior termina em um número negativo! Isso é impossível e errado. Precisamos dividir o somatório em dois, sendo o ponto crucial do momento ipreciso N / 2 + 1.

f(N) = Summation(i from 1 to N / 2)( Summation(j = 1 to (N - (i - 1) * 2)) * ( C ) ) + Summation(i from 1 to N / 2) * ( C )

Desde o momento crucial i > N / 2, o interior fornão serão executados, e estamos assumindo uma complexidade de execução C constante em seu corpo.

Agora, os somatórios pode ser simplificada usando algumas regras de identidade:

  1. Somatório (w de 1 a N) (C) = N * C
  2. Somatório (w de 1 a N) (A (+/-) B) = somatório (w de 1 a N) (A) (+/-) soma (w de 1 a N) (B)
  3. Somatório (w de 1 a N) (w * C) = C * somatório (w de 1 a N) (w) (C é uma constante, independente de w)
  4. Somatório (w de 1 a N) (w) = (N * (N + 1)) / 2

Aplicando alguns álgebra:

f(N) = Summation(i from 1 to N / 2)( (N - (i - 1) * 2) * ( C ) ) + (N / 2)( C )

f(N) = C * Summation(i from 1 to N / 2)( (N - (i - 1) * 2)) + (N / 2)( C )

f(N) = C * (Summation(i from 1 to N / 2)( N ) - Summation(i from 1 to N / 2)( (i - 1) * 2)) + (N / 2)( C )

f(N) = C * (( N ^ 2 / 2 ) - 2 * Summation(i from 1 to N / 2)( i - 1 )) + (N / 2)( C )

=> Summation(i from 1 to N / 2)( i - 1 ) = Summation(i from 1 to N / 2 - 1)( i )

f(N) = C * (( N ^ 2 / 2 ) - 2 * Summation(i from 1 to N / 2 - 1)( i )) + (N / 2)( C )

f(N) = C * (( N ^ 2 / 2 ) - 2 * ( (N / 2 - 1) * (N / 2 - 1 + 1) / 2) ) + (N / 2)( C )

=> (N / 2 - 1) * (N / 2 - 1 + 1) / 2 = 

   (N / 2 - 1) * (N / 2) / 2 = 

   ((N ^ 2 / 4) - (N / 2)) / 2 = 

   (N ^ 2 / 8) - (N / 4)

f(N) = C * (( N ^ 2 / 2 ) - 2 * ( (N ^ 2 / 8) - (N / 4) )) + (N / 2)( C )

f(N) = C * (( N ^ 2 / 2 ) - ( (N ^ 2 / 4) - (N / 2) )) + (N / 2)( C )

f(N) = C * (( N ^ 2 / 2 ) - (N ^ 2 / 4) + (N / 2)) + (N / 2)( C )

f(N) = C * ( N ^ 2 / 4 ) + C * (N / 2) + C * (N / 2)

f(N) = C * ( N ^ 2 / 4 ) + 2 * C * (N / 2)

f(N) = C * ( N ^ 2 / 4 ) + C * N

f(N) = C * 1/4 * N ^ 2 + C * N

E o BigOh é:

O(N²)
Respondeu 31/01/2011 em 16:33
fonte usuário

votos
190

O grande dá o limite superior para a complexidade de tempo de um algoritmo. É normalmente utilizado em conjunto com o processamento de conjuntos de dados (lista), mas pode ser utilizado noutro local.

Alguns exemplos de como ele é usado em código C.

Digamos que tem uma matriz de elementos n

int array[n];

Se quiséssemos para acessar o primeiro elemento da matriz isso seria O (1), uma vez que não importa quão grande a matriz é, ele sempre leva o mesmo tempo constante para obter o primeiro item.

x = array[0];

Se quisermos encontrar um número na lista:

for(int i = 0; i < n; i++){
    if(array[i] == numToFind){ return i; }
}

Isso seria O (n), já que, no máximo, teríamos de olhar através da lista inteira para encontrar o nosso número. O Big-O ainda é O (n) embora possamos encontrar nosso número na primeira tentativa e executar através do loop uma vez porque Big-O descreve o limite superior de um algoritmo (omega é para limite inferior e theta é para apertado ligado) .

Quando chegarmos ao loops aninhados:

for(int i = 0; i < n; i++){
    for(int j = i; j < n; j++){
        array[j] += 2;
    }
}

Este é O (n ^ 2) uma vez que para cada passagem do laço externo (O (n)), temos que passar por toda a lista novamente para multiplicar o n nos deixando com n ao quadrado.

Este é apenas arranhando a superfície, mas quando você começa a analisar mais complexa matemática algoritmos complexo que envolve provas entra em jogo. Espero que este familiariza-lo com o básico, pelo menos, no entanto.

Respondeu 06/08/2008 em 14:34
fonte usuário

votos
87

Embora sabendo como descobrir o tempo Big O para o seu problema particular é útil, sabendo alguns casos gerais podem percorrer um longo caminho para ajudar você a tomar decisões em seu algoritmo.

Aqui estão alguns dos casos mais comuns, levantada a partir http://en.wikipedia.org/wiki/Big_O_notation#Orders_of_common_functions :

O (1) - Determinar se um número é par ou ímpar; usando uma tabela de pesquisa de tamanho constante ou tabela hash

O (log n) - Encontrar um item em uma matriz classificada com uma pesquisa binária

O (n) - Encontrar um item em uma lista não ordenada; a adição de dois números de n dígitos

O (n 2 ) - Multiplicando dois números de n dígitos por um algoritmo simples; adicionando dois nxn matrizes; bolha espécie ou tipo de inserção

O (n 3 ) - Multiplicar dois n × n matrizes por algoritmo simples

O (c n ) - Encontrar a solução (exato) para o problema do caixeiro viajante usando programação dinâmica; determinar se duas afirmações lógicas são equivalentes usando força bruta

O (n!) - Resolver o problema do caixeiro viajante através de pesquisa de força bruta

O (n n ) - Geralmente usado em vez de O (! N) para derivar fórmulas mais simples para a complexidade assintótica

Respondeu 05/09/2008 em 20:09
fonte usuário

votos
40

Lembrete de pequeno porte: a big Onotação é usada para designar assintótica complexidade (isto é, quando o tamanho do problema cresce até ao infinito), e esconde-se uma constante.

Isto significa que entre um algoritmo em O (n) e uma em O (n 2 ), o mais rápido não é sempre o primeiro um (embora não sempre existe um valor de n tal que para problemas de tamanho> N, o primeiro algoritmo é o mais rápido).

Note-se que a constante escondido muito depende da implementação!

Além disso, em alguns casos, o tempo de execução não é uma função determinista do tamanho n da entrada. Tome triagem usando tipo rápido, por exemplo: o tempo necessário para resolver uma matriz de n elementos não é uma constante, mas depende da configuração a partir da matriz.

Existem diferentes complexidades de tempo:

  • Pior caso (geralmente o mais simples de descobrir, embora nem sempre muito significativo)
  • caso médio (geralmente muito mais difícil de descobrir ...)

  • ...

Uma boa introdução é Uma Introdução à Análise de Algoritmos por R. Sedgewick e P. Flajolet.

Como você disse, premature optimisation is the root of all evile (se possível) profiling realmente deve ser sempre usado quando otimização de código. Ele pode até mesmo ajudar a determinar a complexidade de seus algoritmos.

Respondeu 23/08/2008 em 21:43
fonte usuário

votos
26

Vendo as respostas aqui, acho que podemos concluir que a maioria de nós, de fato, aproximar o fim do algoritmo por olhando para ele e usar o bom senso em vez de calcular isso com, por exemplo, o método mestre como nós foram pensados na universidade. Com isso dito, devo acrescentar que, mesmo o professor nos encorajou (mais tarde) para realmente pensar sobre isso em vez de apenas cálculo.

Também eu gostaria de adicionar como é feito para funções recursivas :

suponha que temos uma função como ( código de esquema ):

(define (fac n)
    (if (= n 0)
        1
            (* n (fac (- n 1)))))

que recursivamente calcula o fatorial do número dado.

O primeiro passo é tentar determinar a característica de desempenho para o corpo da função somente neste caso, nada de especial é feito no corpo, apenas uma multiplicação (ou a devolução do valor 1).

Assim, o desempenho para o corpo é: O (1) (constante).

Em seguida tentar determinar isso para o número de chamadas recursivas . Neste caso, temos n-1 chamadas recursivas.

Assim, o desempenho para as chamadas recursivas é: O (n-1) (ordem é n, como jogamos fora as partes insignificantes).

Em seguida, colocar os dois juntos e então você tem o desempenho para toda a função recursiva:

1 * (n-1) = O (n)


Peter , para responder suas questões levantadas; o método que eu descrevo aqui realmente lida com isso muito bem. Mas tenha em mente que esta é ainda uma aproximação e não uma resposta matematicamente correta completo. O método descrito aqui é também um dos métodos que foram ensinadas na universidade, e se bem me lembro foi usado para muito mais algoritmos avançados do que o fatorial eu usei neste exemplo.
Claro que tudo depende de quão bem você pode estimar o tempo de execução do corpo da função e do número de chamadas recursivas, mas isso é tão verdadeiro para os outros métodos.

Respondeu 07/08/2008 em 09:10
fonte usuário

votos
25

Se o seu custo é um polinômio, basta manter o termo de mais alta ordem, sem o seu multiplicador. Por exemplo:

O ((N / 2 + 1) * (n / 2)) = O (n 2 /4 + n / 2) = O (n 2 /4) = O (n 2 )

Isso não funciona para séries infinitas, você mente. Não existe uma receita única para o caso geral, embora para alguns casos comuns, as seguintes desigualdades se por:

O (log N ) <S ( N ) <O ( N log N ) <S ( N 2 ) <O ( N k ) <O (E n ) <O ( N !)

Respondeu 31/01/2011 em 14:30
fonte usuário

votos
19

Eu penso sobre isso em termos de informação. Qualquer problema consiste em aprender um certo número de bits.

Sua ferramenta básica é o conceito de pontos de decisão e sua entropia. A entropia de um ponto de decisão é a informação média ele vai te dar. Por exemplo, se um programa contém um ponto de decisão com dois ramos, é entropia é a soma da probabilidade de cada ramo vezes o log 2 da probabilidade inversa desse ramo. Isso é o quanto você aprende através da execução de tal decisão.

Por exemplo, uma ifinstrução tendo dois ramos, ambos igualmente provável, tem uma entropia de 1/2 * log (2/1) + 1/2 * log (2/1) = 1/2 * 1 + 1/2 * 1 = 1. Portanto, a sua entropia é um pouco.

Suponha que você está procurando uma mesa de N itens, como N = 1024. Isso é um problema de 10 bits porque o log (1024) = 10 bits. Então, se você pode pesquisá-lo com instruções IF que têm resultados igualmente prováveis, deve tomar 10 decisões.

Isso é o que você ganha com busca binária.

Suponha que você está fazendo pesquisa linear. Você olha para o primeiro elemento e perguntar se é o que deseja. As probabilidades são 1/1024 que é, e 1023/1024 que não é. A entropia de decisão que é 1/1024 * log (1024/1) + 1023/1024 * log (1024/1023) = 1/1024 * 10 + 1023/1024 * cerca de 0 = cerca de 0,01 bits. Você aprendeu muito pouco! A segunda decisão não é muito melhor. É por isso que busca linear é tão lento. Na verdade, é exponencial do número de bits que você precisa aprender.

Suponha que você está fazendo indexação. Suponha que a tabela é pré-ordenada em um monte de caixas, e você usar algum de todos os bits na chave para indexar diretamente para a entrada da tabela. Se há 1024 caixas, a entropia é 1/1024 * log (1024) + 1/1024 * log (1024) + ... para todos os 1024 resultados possíveis. Este é 1/1024 * 10 vezes 1024 resultados, ou 10 bits de entropia para que uma operação de indexação. É por isso que a indexação de pesquisa é rápido.

Agora pense sobre a classificação. Você tem itens N, e você tem uma lista. Para cada item, você tem que procurar onde o item vai na lista e, em seguida, adicioná-lo à lista. Assim, a classificação leva cerca de N vezes o número de etapas da pesquisa subjacente.

Assim, os tipos com base em decisões binárias que têm cerca de resultados igualmente prováveis ​​tudo levar cerca de O (n log n) passos. Um algoritmo tipo O (N) é possível se for baseada na indexação de pesquisa.

Descobri que problemas de desempenho quase todos algorítmica pode ser encarado desta forma.

Respondeu 10/03/2009 em 14:24
fonte usuário

votos
17

Vamos começar do começo.

Primeiro de tudo, aceitar o princípio de que certas operações simples sobre os dados pode ser feito em O(1)tempo, isto é, no tempo que é independente do tamanho da entrada. Estas operações primitivas em C consistem

  1. As operações aritméticas (por exemplo + ou%).
  2. operações lógicas (por exemplo, &&).
  3. As operações de comparação (por exemplo, <=).
  4. operações Estrutura acesso (por exemplo, matriz de indexação como a [i], ou ponteiro abaixamento se- com o operador ->).
  5. atribuição simples como copiar um valor para uma variável.
  6. As chamadas para as funções de biblioteca (por exemplo, scanf, printf).

A justificativa para este princípio requer um estudo detalhado das instruções de máquina (etapas primitivas) de um computador típico. Cada uma das operações descritas pode ser feito com um pequeno número de instruções de máquina; muitas vezes são necessários apenas uma ou duas instruções. Como consequência, vários tipos de declarações em C pode ser executado no O(1)tempo, isto é, de alguma quantidade constante de tempo independente da entrada. Estes simples incluem

  1. instruções de atribuição que não envolvem chamadas de função em suas expressões.
  2. Leia as declarações.
  3. Escrever declarações que não exigem chamadas de função para avaliar argumentos.
  4. As demonstrações salto quebrar, continue, Goto, e retornar expressão, onde a expressão não contém uma chamada de função.

Em C, muitos para-loops são formados por inicializar uma variável de índice para algum valor e incrementando que variável por um de cada vez em volta do circuito. A for-loop termina quando o índice atinge um limite. Por exemplo, a for-loop

for (i = 0; i < n-1; i++) 
{
    small = i;
    for (j = i+1; j < n; j++)
        if (A[j] < A[small])
            small = j;
    temp = A[small];
    A[small] = A[i];
    A[i] = temp;
}

utiliza variável índice i. Incrementa-se i em 1 de cada vez em volta do circuito, e as iterações parar quando atinge i n - 1.

No entanto, para o momento, o foco na forma simples de para-loop, onde a diferença entre os valores final e inicial, dividido pelo valor pelo qual a variável índice é incrementado nos diz quantas vezes vamos ao redor do circuito . Essa contagem é exata, a menos que há maneiras de sair do loop através de uma declaração salto; é um limite superior do número de iterações em qualquer caso.

Por exemplo, os itera para-circuito ((n − 1) − 0)/1 = n − 1 times, uma vez que 0 é o valor inicial de i, n - 1 é o valor mais alto alcançado por i (ou seja, quando atinge i n-1, os batentes de ansa e não ocorre com iteração i = n- 1), e 1 é adicionado a i em cada iteração do circuito.

No caso mais simples, onde o tempo gasto no corpo do laço é o mesmo para cada iteração, podemos multiplicar o big-oh limite superior para o corpo pelo número de vezes em todo o circuito . Estritamente falando, devemos, em seguida, adicione O (1) tempo para inicializar o índice de loop e O (1) tempo para a primeira comparação do índice de loop com o limite , porque estamos a testar mais uma vez do que ir ao redor do loop. No entanto, a menos que seja possível executar o loop zero vezes, o tempo para inicializar o loop e testar o limite de uma vez é um termo de baixa ordem que pode ser descartado pela regra somatório.


Agora, considere este exemplo:

(1) for (j = 0; j < n; j++)
(2)   A[i][j] = 0;

Sabemos que linha (1) leva O(1)tempo. Claramente, vamos em torno do circuito n vezes, como se pode determinar por subtracção do limite inferior do limite superior encontrado em linha (1) e, em seguida, adicionando 1. Uma vez que o corpo, a linha (2), leva O (1) o tempo, podemos negligenciar o tempo para incrementar j e o tempo para comparar j com n, ambos os quais são também o (1). Assim, o tempo de execução de linhas (1) e (2) é o produto de n e O (1) , o qual é O(n).

Da mesma forma, podemos ligado o tempo de execução do laço externo consistindo de linhas (2) a (4), que é

(2) for (i = 0; i < n; i++)
(3)     for (j = 0; j < n; j++)
(4)         A[i][j] = 0;

Nós já estabelecido de que o circuito de linhas (3) e (4) converte o (n) de tempo. Assim, podemos negligenciar a O (1) tempo para incrementar i e para testar se i <n em cada iteração, concluindo-se que cada iteração do circuito externo leva O (n).

A inicialização i = 0 da espira externa e a (n + 1) de teste de r a condição i <n igualmente ter ó (1) e tempo pode ser negligenciada. Finalmente, observa-se que ir em torno do laço exterior n vezes, tendo o (n) de tempo para cada iteração, dando um total de O(n^2)tempo de funcionamento.


Um exemplo mais prático.

digite descrição da imagem aqui

Respondeu 02/02/2014 em 16:30
fonte usuário

votos
11

Se você quiser estimar a ordem de seu código empiricamente em vez de analisar o código, você pode ficar em uma série de valores de n e tempo de seu código aumentando. Traçar os intervalos em uma escala logarítmica. Se o código é O (x ^ n), os valores devem cair em uma linha de inclinação n.

Isto tem várias vantagens sobre apenas estudando o código. Por um lado, você pode ver se você está na faixa onde o tempo de execução se aproxima de seu fim assintótica. Além disso, você pode achar que algum código que você pensou que era ordem O (x) é realmente encomendar O (x ^ 2), por exemplo, por causa do tempo gasto em chamadas de biblioteca.

Respondeu 11/12/2008 em 21:49
fonte usuário

votos
9

Basicamente, a coisa que surge 90% do tempo é apenas analisando loops. Você tem, duplos, loops aninhados com três? O que você tem O (n), O (n ^ 2), O (n ^ 3) tempo de execução.

Muito raramente (a menos que você está escrevendo uma plataforma com uma extensa biblioteca de base (como por exemplo, o .NET BCL, ou STL do C ++), você vai encontrar tudo o que é mais difícil do que apenas olhando para seus loops (para declarações, enquanto, Goto, etc ...)

Respondeu 14/08/2008 em 16:35
fonte usuário

votos
7

Menos útil em geral, eu acho, mas por uma questão de exaustividade, há também uma grande Omega Ω , que define um limite inferior da complexidade de um algoritmo, e um Big Theta Θ , que define tanto um limite superior e inferior.

Respondeu 23/09/2008 em 08:26
fonte usuário

votos
7

notação O Big é útil porque é fácil de trabalhar e esconde complicações e detalhes desnecessários (por alguma definição de desnecessário). Uma boa maneira de trabalhar fora a complexidade da divisão e conquista é o método de árvore. Vamos dizer que você tem uma versão do quicksort com o procedimento mediana, para que dividir a matriz em subarrays perfeitamente equilibrado o tempo todo.

Agora construir uma árvore correspondente a todas as matrizes que trabalham com você. Na raiz você tem a matriz original, a raiz tem dois filhos que são os subarrays. Repita este procedimento até que você tem matrizes de um único elemento na parte inferior.

Uma vez que podemos encontrar a mediana em O (n) o tempo e dividir a matriz em duas partes em O (n) o tempo, o trabalho feito em cada nó é O (k), onde k é o tamanho da matriz. Cada nível da árvore contém (no máximo) toda a matriz de modo que o trabalho por nível é O (n) (os tamanhos dos subarrays adicionar até n, e uma vez que temos O (k) por nível, podemos adicionar este up) . Há registo de apenas (n) níveis na árvore, pois cada vez que reduzir pela metade a entrada.

Portanto, podemos limite superior da quantidade de trabalho por O (N * log (n)).

No entanto, Big O esconde alguns detalhes que às vezes não pode ignorar. Considere computar a sequência de Fibonacci com

a=0;
b=1;
for (i = 0; i <n; i++) {
    tmp = b;
    b = a + b;
    a = tmp;
}

e permite que apenas assumir a a e b são BigIntegers em Java ou algo que pode lidar arbitrariamente grandes números. A maioria das pessoas diria que este é um (n) algoritmo O sem vacilar. O raciocínio é que tem n iterações do loop e O (1) funcionam em lado da laçada.

Mas os números de Fibonacci são grandes, o número Fibonacci n-th é exponencial em n então basta armazená-la assumirá a ordem de n bytes. Realizando disso com grandes números inteiros levará O (n) quantidade de trabalho. Assim, a quantidade total de trabalho feito neste procedimento é

1 + 2 + 3 + ... + n = n (n-1) / 2 = O (n ^ 2)

Portanto, este algoritmo roda em tempo quadradic!

Respondeu 08/08/2008 em 14:53
fonte usuário

votos
7

Quebrar o algoritmo em pedaços você sabe a notação grande O de, e combinam através de operadores S grandes. Essa é a única maneira que eu conheço.

Para mais informações, consulte a página da Wikipedia sobre o assunto.

Respondeu 06/08/2008 em 12:34
fonte usuário

votos
7

Familiaridade com os algoritmos / estruturas de dados eu utilização e / ou análise de olhar rápido de assentamento iteração. A dificuldade é quando você chamar uma função de biblioteca, possivelmente várias vezes - muitas vezes você pode não ter certeza se você está chamando a função desnecessariamente às vezes ou o que aplicação que está usando. Talvez funções de biblioteca deve ter uma medida de complexidade / eficiência, seja Big O ou alguma outra métrica, que está disponível na documentação ou mesmo IntelliSense .

Respondeu 06/08/2008 em 11:59
fonte usuário

votos
6

Para o 1º caso, o loop interno é executado n-ivezes, de modo que o número total de execuções é a soma para ipassar de 0que n-1(por causa inferior, não inferior ou igual) do n-i. Você começa, finalmente n*(n + 1) / 2, por isso O(n²/2) = O(n²).

Para o segundo ciclo, ié entre 0e nincluídas para a espira externa; em seguida, o loop interno é executado quando jé estritamente maior que n, o que é então impossível.

Respondeu 31/01/2011 em 15:36
fonte usuário

votos
6

Quanto a "como você calcular" Big O, isso é parte da teoria da complexidade computacional . Para alguns (muitos) casos especiais que você pode ser capaz de chegar com algumas heurísticas simples (como multiplicando a contagem de loop de loops aninhados), esp. quando tudo que você quer é qualquer estimativa de limite superior, e você não se importa se é muito pessimista - que eu acho que é provavelmente o que sua pergunta é sobre.

Se você realmente quer para responder sua pergunta para qualquer algoritmo o melhor que você pode fazer é aplicar a teoria. Além da análise simplista "pior caso" Encontrei análise amortizado muito útil na prática.

Respondeu 10/03/2009 em 16:02
fonte usuário

votos
5

Além de utilizar o método principal (ou uma das suas especialidades), eu testar os algoritmos experimentalmente. Isso não pode provar que qualquer classe especial complexidade é alcançado, mas pode fornecer a garantia de que a análise matemática é apropriado. Para ajudar com esta garantia, eu uso as ferramentas de cobertura de código em conjunto com minhas experiências, para garantir que eu estou exercitando todos os casos.

Como um exemplo muito simples dizer que você queria fazer uma verificação de sanidade na velocidade da lista de classificação do framework .NET. Você poderia escrever algo como o seguinte, em seguida, analisar os resultados no Excel para se certificar que não excedeu um log n * (n) curva.

Neste exemplo, medir o número de comparações, mas também é prudente examinar o tempo real necessário para cada tamanho de amostra. No entanto, em seguida, você deve ser ainda mais cuidadoso que você está apenas medir o algoritmo e não incluindo artefatos de sua infra-estrutura de teste.

int nCmp = 0;
System.Random rnd = new System.Random();

// measure the time required to sort a list of n integers
void DoTest(int n)
{
   List<int> lst = new List<int>(n);
   for( int i=0; i<n; i++ )
      lst[i] = rnd.Next(0,1000);

   // as we sort, keep track of the number of comparisons performed!
   nCmp = 0;
   lst.Sort( delegate( int a, int b ) { nCmp++; return (a<b)?-1:((a>b)?1:0)); }

   System.Console.Writeline( "{0},{1}", n, nCmp );
}


// Perform measurement for a variety of sample sizes.
// It would be prudent to check multiple random samples of each size, but this is OK for a quick sanity check
for( int n = 0; n<1000; n++ )
   DoTest(n);
Respondeu 05/09/2008 em 19:33
fonte usuário

votos
4

O que muitas vezes fica esquecido é o esperado comportamento de seus algoritmos. Isso não muda o Big-O do seu algoritmo , mas não se relacionam com a afirmação "otimização prematura.. .."

comportamento esperado do seu algoritmo é - muito estúpidos - o quão rápido você pode esperar que seu algoritmo para trabalhar com dados você é mais provável para ver.

Por exemplo, se você estiver procurando por um valor em uma lista, é O (n), mas se você sabe que a maioria das listas que você vê tem o seu valor na frente, comportamento típico de seu algoritmo é mais rápido.

Para realmente pregá-lo para baixo, você precisa ser capaz de descrever a distribuição de probabilidade do seu "espaço de entrada" (se você precisa para classificar uma lista, quantas vezes é que lista já vai ser resolvido? Quantas vezes é totalmente revertida? Como muitas vezes é na maior parte ordenados?) nem sempre é possível que você sabe disso, mas às vezes você faz.

Respondeu 10/03/2009 em 15:30
fonte usuário

votos
4

Não se esqueça de também permitir complexidades espaço que também pode ser um motivo de preocupação quando se tem limitado recursos de memória. Assim, por exemplo, você pode ouvir alguém que quer um algoritmo de espaço constante que é basicamente uma forma de dizer que a quantidade de espaço ocupado pelo algoritmo não depende de quaisquer fatores dentro do código.

Às vezes, a complexidade pode vir de quantas vezes é algo chamado, quantas vezes é um loop executado, como frequentemente é a memória alocada, e assim por diante é uma outra parte para responder a esta pergunta.

Por último, ó grande pode ser usado para o pior caso, melhor caso, e os casos de amortização onde Geralmente é o pior caso que é usado para descrever o quão ruim um algoritmo pode ser.

Respondeu 14/10/2008 em 21:16
fonte usuário

votos
2

grande questão!

Se você estiver usando o Big O, você está falando sobre o pior caso (mais sobre o que isso significa mais tarde). Além disso, há teta capital para caso médio e um grande omega para o melhor caso.

Confira este site para uma bela definição formal de Big O: https://xlinux.nist.gov/dads/HTML/bigOnotation.html

f (n) = O (g (n)) significa que não são constantes c e k positivo, nomeadamente que 0 ≤ f (n) ≤ CG (n) para todos os n ≥ k. Os valores de c e k deve ser fixado para a função f e não deve depender n.


Ok, então agora o que queremos dizer com "melhor caso" e "pior caso" complexidades?

Isto é provavelmente mais claramente ilustradas por meio de exemplos. Por exemplo, se estamos usando a pesquisa linear para encontrar um número em um array ordenado, em seguida, o pior caso é quando decidimos procurar o último elemento da matriz, pois isso levaria tantos passos, pois há itens na matriz. O melhor caso seria quando buscamos o primeiro elemento , uma vez que seria feito após a primeira verificação.

O ponto de todos esses adjetivos complexidades -Case é que nós estamos procurando uma maneira para representar graficamente a quantidade de tempo que um programa hipotético corre para a conclusão em termos de tamanho de variáveis específicas. No entanto, para muitos algoritmos você pode argumentar que não há uma única vez para um determinado tamanho de entrada. Observe que isso contradiz com a exigência fundamental de uma função, qualquer entrada não deve ter mais do que uma saída. Então, nós vimos acima com múltiplas funções para descrever a complexidade de um algoritmo. Agora, apesar de pesquisar uma matriz de tamanho n pode levar quantidades variáveis de tempo, dependendo do que você está procurando na matriz e, dependendo proporcionalmente ao n, podemos criar uma descrição informativa do algoritmo usando melhor caso, média-caso e aulas de pior caso.

Desculpe isso é tão mal escrito e não tem muita informação técnica. Mas espero que ele vai fazer classes de complexidade tempo mais fácil para pensar. Uma vez que você se sentir confortável com isso, torna-se uma simples questão de analisar através de seu programa e procurando coisas como para-loops que dependem de tamanhos de matriz e raciocínio com base em suas estruturas de dados que tipo de entrada resultaria em casos triviais e que a entrada resultariam no pior dos casos.

Respondeu 20/08/2016 em 04:57
fonte usuário

votos
2

Eu não sei como resolver programaticamente isso, mas a primeira coisa que as pessoas fazem é que nós provar o algoritmo para certos padrões no número de operações feitas, dizem 4n ^ 2 + 2n + 1 temos 2 regras:

  1. Se temos uma soma de termos, o termo com a maior taxa de crescimento é mantido, com outros termos omitido.
  2. Se temos um produto de vários fatores fatores constantes são omitidas.

Se simplificar f (x), onde f (x) é a fórmula para o número de operações feito, (4n ^ 2 + 2n + 1 explicado acima), obtém-se o valor de big-O [O (n ^ 2) neste caso]. Mas isso teria de responder por Lagrange interpolação no programa, o que pode ser difícil de implementar. E se o valor big-O real foi de O (2 ^ n), e poderíamos ter algo como O (x ^ n), de modo que este algoritmo provavelmente não seria programável. Mas se alguém prove que estou errado, dá-me o código. . . .

Respondeu 11/05/2013 em 02:32
fonte usuário

votos
2

Para o código de A, a espira externa será executado para N + 1 vezes, o tempo de '1' significa o processo que se verifica o i ainda satisfaz o requisito. E corre laço interno n vezes, n-2 vezes .... Assim, 0 + 2 + .. + (n-2) + n = (0 + n) (n + 1) / 2 = S (N²).

Para o código de B, embora circuito interno não entrar e executar o foo (), o loop interno será executado para n vezes depende do tempo de execução de circuito externo, o qual é o (n)

Respondeu 31/01/2011 em 21:07
fonte usuário

votos
0

Gostaria de explicar o Big-O em um aspecto pouco diferente.

Big-O é apenas para comparar a complexidade dos programas que significa o quão rápido eles estão crescendo quando as entradas estão aumentando e não o tempo exato que é gasta para fazer a ação.

IMHO nas fórmulas Big-O é melhor não usar equações mais complexas (que você pode se ater apenas aos no gráfico a seguir.) No entanto, você ainda pode usar outra fórmula mais precisa (como 3 ^ n, n ^ 3, .. .), mas mais do que isso pode ser às vezes enganador! Então é melhor para mantê-lo o mais simples possível.

digite descrição da imagem aqui

Eu gostaria de enfatizar mais uma vez que aqui nós não queremos para obter uma fórmula exata para o nosso algoritmo. Nós só queremos mostrar como ela cresce quando as entradas estão crescendo e comparar com os outros algoritmos nesse sentido. Caso contrário, você seria melhor usar métodos diferentes, como bench-marking.

Respondeu 20/02/2019 em 10:44
fonte usuário

Cookies help us deliver our services. By using our services, you agree to our use of cookies. Learn more