Às vezes, é fácil identificar a complexidade de tempo de um algoritmo, examinando-o cuidadosamente. Algoritmos com dois loops aninhados de são obviamente . Algoritmos que explore todas as combinações possíveis de grupos de dois valores são obviamente .N 2 N 2 N
No entanto, não sei como "detectar" um algoritmo com complexidade . Uma implementação de fusão recursiva, por exemplo, é uma. Quais são as características comuns do mergesort ou de outros algoritmos que me dariam uma pista se eu estivesse analisando uma?Θ ( N log N )
Tenho certeza de que há mais de uma maneira pela qual um algoritmo pode ter a complexidade , para que todas e quaisquer respostas sejam apreciadas. Aliás, estou buscando características e dicas gerais, não provas rigorosas.
fonte
Respostas:
Seu arquetípico é um algoritmo de dividir e conquistar , que divide (e recombina) o trabalho em tempo linear e se repete sobre as peças. A classificação por mesclagem funciona dessa maneira: gaste tempo dividindo a entrada em duas partes aproximadamente iguais, classifique recursivamente cada peça e gaste tempo combinando as duas metades classificadas.O ( n ) Θ ( n )Θ ( n logn ) O ( n ) Θ ( n )
Intuitivamente, continuando a idéia de dividir e conquistar, cada estágio de divisão leva um tempo linear no total, porque o aumento no número de peças a serem divididas corresponde exatamente à diminuição no tamanho das peças, pois o tempo gasto pela divisão é linear. O tempo total de execução é o produto do custo total de um estágio de divisão multiplicado pelo número de estágios de divisão. Como o tamanho das peças é dividido pela metade a cada vez, existem estágios de divisão do , portanto o tempo total de execução é n ⋅ log ( n ) . (Até uma constante multiplicativa, a base do logaritmo é irrelevante.)registro2( N ) n ⋅ log( N )
Colocando-o nas equações (), uma maneira de estimar o tempo de execução de um algoritmo é expressá-lo recursivamente: T ( n ) = 2 T ( n / 2 ) + Θ ( n ) . É claro que esse algoritmo leva mais que o tempo linear e podemos ver quanto mais dividindo por n : T ( n )T( N ) T( n ) = 2 T( n / 2 ) + Θ ( n ) n
Quandondobra,T(n)/naumenta em uma quantidade constante:T(n)/naumenta logaritmicamente, ou seja,T(n)=Θ(nlogn).
Este é um exemplo de um padrão mais geral: o teorema mestre . Para qualquer algoritmo recursivo que divide a sua entrada de tamanho em um pedaços de tamanho n / b e leva um tempo f ( n ) para realizar a divisão e recombinação, o satisfaz tempo de execução T ( n ) = um ⋅ T ( n / b ) + f ( n ) . Isto conduz a uma forma fechada que depende dos valores de um e b e a forma den uma n / b f( N ) T( n ) = a ⋅ T( n / b ) + f( N ) uma b . Se a = b e f ( n ) = Θ ( n ) , o teorema mestre declara que T ( n ) = Θ ( n log n ) .f a = b f( n ) = Θ ( n ) T( n ) = Θ ( n logn )
fonte
Duas outras categorias de algoritmos que levam :Θ(nlogn)
Algoritmos em que cada item é processado por vez e leva tempo logarítmico para processar cada item (por exemplo, HeapSort ou muitos dos algoritmos de geometria computacional de varredura de avião).
Algoritmos em que o tempo de execução é dominado por uma etapa de pré-processamento de classificação. (Por exemplo, no algoritmo de Kruskal para árvore de abrangência mínima, podemos classificar as arestas por peso como o primeiro passo).
fonte
Outra categoria: os algoritmos nos quais a saída possui tamanho e, portanto, Θ ( n log n ) o tempo de execução é linear no tamanho da saída .Θ(nlogn) Θ(nlogn)
Embora os detalhes de tais algoritmos usem frequentemente técnicas de dividir e conquistar, eles não precisam necessariamente. O tempo de execução vem fundamentalmente da pergunta que está sendo feita e, portanto, acho que vale a pena mencionar separadamente.
Isso ocorre nas estruturas de dados baseadas em uma árvore de pesquisa binária aumentada, em que cada nó armazena uma estrutura de dados de tamanho linear para pesquisar sobre as folhas na subárvore desse nó. Tais estruturas de dados surgem frequentemente na pesquisa por faixa geométrica e geralmente são baseadas em um esquema de decomposição . Veja a Pesquisa da Agarwal .
fonte
fonte
Esses são tipicamente algoritmos da variedade "dividir e conquistar", em que o custo de dividir e combinar subsoluções não é "muito grande". Dê uma olhada nesta FAQ para ver que tipos de recorrências causam esse comportamento.
fonte
Normalmente, os algoritmos de divisão e conquista renderão complexidade O (N log N).
fonte
Não posso dar um exemplo concreto de um algoritmo que use esse loop, mas ele aparece frequentemente ao codificar algoritmos personalizados.
fonte