Você está na página 1de 31

PROGRAMA DE PÓS-GRADUAÇÃO EM CIÊNCIAS DA COMPUTAÇÃO

PAULO ROBERTO DE LIMA GIANFELICE*

Disciplina de Análise de Algoritmo e Estrutura de Dados


Curso de Doutorado

Programa de Pós Graduação em Ciências da Computação

Instituto de Ciências e Tecnologia da Universidade Federal de São Paulo

PPG-CC/ ICT-UNIFESP

2021 - SÃO JOSÉ DOS CAMPOS

Notas Didáticas das Aulas do Professor


Jurandy Almeida

Correspondência

paulo.gianfelice@unifesp.br Resumo
id Instagran: @paulogianfelice

id Lattes: 2458665575792001 Livro Base: Algoritmos: Teoria e Prática, 2 Estruturas de Dados: Algoritmos, Aná-
1
cel.: 18981250413

Presidente Prudente, SP
lise de Complexidade em Java, C e C++, 3 Estruturas de Dados e Seus Algoritmos,
Outras informações pertinentes
4
Algorithms
Autores: T. H. Cormen; et al., A. F. G. Ascencio e G. S. Araújo, J. L. Szwarcter
sobre o autor podem ser 1 2 3
solicitadas por um dos links

anteriores e L. Markenzon, R. Sedgewick e K. Wayne


4
* Único autor
Keywords: Algoritmo de Computador, Crescimento de Funções, Estrutura de
Dados, Ordenação, Recorrência
Apresentação

Um algoritmo é um processo sistemático para a resolução de um problema e, pela

própria natureza do instrumento utilizado, seu desenvolvimento é particularmente

importante para problemas a serem solucionados em um computador. Existem dois

aspectos básicos no estudo de algoritmos: a correção e a análise. O primeiro consiste

em vericar a exatidão do método empregado e realizado através de uma prova ma-

temática. Por sua vez, a análise visa à obtenção de parâmetros que possam avaliar a

eciência do algoritmo em termos de tempo de execução e a memória computacional

que a execução consome, e é realizada através de um estudo do comportamento do

algoritmo. Sobretudo, um algoritmo computa uma saída, o resultado do problema, a

partir de uma entrada, os dados que estabelecem as informações inicialmente conhe-

cidas e que permitem encontrar a solução do problema. As estruturas diferem umas

das outras pela disposição ou manipulação de seus dados, de modo que, enquanto

a disposição dos dados em uma estrutura obedece a condições preestabelecidas e

caracteriza a estrutura. Na prática, o estudo de estruturas de dados não pode ser

desvinculado de seus aspectos algorítmicos, por isso a escolha correta da estrutura

adequada em cada caso depende diretamente do conhecimento de algoritmos para

manipular a estrutura de maneira eciente, tanto que o conhecimento de princí-

pios da complexidade computacional é, portanto, requisito básico para se avaliar

corretamente a adequação de uma estrutura de dados.


Motivação

A proposta de um trabalho de análise de algoritmos e estrutura de dados é con-

feccionar um estudo de conteúdos desenvolvidos na área de computação (também

chamada de informática em algumas regiões). Ao desenvolver uma solução compu-

tacional de um problema do mundo real, não basta apenas apresentar a solução

proposta, é ainda relevante apresentar a melhor solução possível para o problema

abordado, uma vez que, em muitos casos, a solução apropriada para o momento e

os recursos disponíveis é analisada por meio dos pontos positivos e negativos, sem-

pre objetivando que os pontos negativos sejam mínimos. Comumente, em particular

em salas de aulas ou disciplinas básicas, essa abordagem para soluções computaci-

onais não são apropriadas pois os conceitos aplicados são fortemente inuenciados

pelo imediatismo, de modo que a análise da solução não são necessárias, tornando

insignicante a busca por melhores respostas. Um curso de estrutura de dados e

algoritmos, análise de complexidade ou análise de algoritmos e estrutura de dados

tem por objetivo básico conceituar, descrever, implementar e, obviamente, analisar

algoritmos de busca sequencial ou binária e algoritmos de estruturação de dados,

tais como a estruturação básica que consiste em ordenar, fundir, selecionar e inserir

dados, ou a estruturação não linear baseada em listas de propriedades, árvores e gra-

fos. De problemas de simulação em física à problemas de sequenciamento genético

em biologia molecular, os métodos básicos descritos em uma análise de algoritmo

e estrutura de dados se tornaram essenciais e, em particular, na pesquisa cientíca

desde a constituição de sistemas de modelagem arquitetônica à simulação de aero-

naves, tornaram-se ferramentas essenciais nos mais diversos ramos da engenharia

e de sistemas de banco de dados por exemplo, compondo etapas da confecção de

motores de busca na Internet e a interligação das partes essenciais dos sistemas de

software modernos. No entanto, à medida que o escopo dos aplicativos de computa-

dor continua a crescer, também aumenta o impacto dos métodos básicos abordados

para a constatação de sua ecácia e, em geral, o estudo de algoritmos e estruturas

de dados é fundamental para qualquer currículo de ciência da computação, mas

não é apenas para programadores e estudantes de ciência da computação, qualquer

usuário de um computador que pretenda conduzi-lo a um funcionamento mais rá-

pido ou à resolução de problemas mais complexos, demandará de uma análise de

algoritmos e estrutura de dados.


4

Sumário

Resumo 1
1 Introdução 6
1.1 Algoritmos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6

1.2 Estrutura de dados (ou da instância) . . . . . . . . . . . . . . . . . . 6

1.3 Apresentação dos Algoritmos . . . . . . . . . . . . . . . . . . . . . . 8

1.4 Recursividade . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8

1.5 Complexidade de Algoritmos . . . . . . . . . . . . . . . . . . . . . . 9

1.6 Eciência . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10

2 Análise Assintótica de Algoritmos 11


2.1 Notação assintótica, funções e tempos de execução . . . . . . . . . . 12

2.2 Elementos da análise assintótica . . . . . . . . . . . . . . . . . . . . 13

2.2.1 Notação O . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15

2.2.2 Notação Ω . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17

2.2.3 Notação Θ . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19

2.2.4 Notação o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20

2.2.5 Notação ω. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21

3 Relações de Recorrência 22
3.1 Detalhes técnicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23

3.2 Métodos Para Resolver Recorrências . . . . . . . . . . . . . . . . . . 25

3.2.1 Método de Substituição . . . . . . . . . . . . . . . . . . . . . 25

3.2.2 Método da Árvore de Recursão . . . . . . . . . . . . . . . . . 27

3.2.3 Método Mestre . . . . . . . . . . . . . . . . . . . . . . . . . . 29


Resumo Teórico

A orientação da disciplina é estudar algoritmos que provavelmente serão de uso prá-

tico em alguma etapa de uma implementação computacional da resolução de um

dado problema. O conceito aborda uma ampla variedade de algoritmos e estruturas

de dados para fornecer informações sucientes sobre a composição, aplicação e a

necessidade de resoluções de problemas para que estes sejam implementados, depu-

rados e colocados em prática com segurança em qualquer ambiente computacional.

A abordagem envolve o conceito de algoritmos baseado em implementações comple-

tas, em uma discussão das operações de programas computacionais em um conjunto

consistente de exemplos e, em vez de apresentar pseudocódigos, se necessário, con-

sidera um código real para que os programas possam ser rapidamente colocados em

prática, sejam eles escritos em C, C++ , Java ou em outras linguagens de programa-

ção modernas, tal como Python ou R, para que os tipos de dados sejam também

utilizados em um estilo de programação moderno baseado na abstração, para que os

algoritmos e suas estruturas de dados sejam conjuntamente contemplados. Simulta-

neamente, uma abordagem cientíca é estabelecida enfatizando o desenvolvimento

de modelos matemáticos para descrever o desempenho dos algoritmos, usando os

modelos para desenvolver hipóteses sobre o desempenho e, em seguida, testando as

hipóteses executando os algoritmos em contextos realistas. A disciplina é proposta

para cobrir os tipos de dados abstratos básicos, algoritmos de classicação, algorit-

mos de pesquisa, processamento de gráco e processamento de string. A disciplina

busca manter o material em contexto algorítmico, descrevendo estruturas de dados,

paradigmas de projeto de algoritmo, redução e modelos de resolução de problemas,

abrangendo ainda métodos clássicos que foram ensinados desde os primórdios da

computação como ciência e, de acordo com o livro base (Algoritmos: teoria e prática

de T. H. Cormen; et al.), métodos que vem sendo propostos nos últimos anos.
6

1 Introdução

Este capítulo contém, principalmente, algumas notações e os conceitos básicos

utilizados ao longo deste texto.

Na Seção 1.1 é apresentada uma descrição para o que se pode denir como algo-

ritmo, enquanto que na Seção 1.2 é descrito o que é uma estrutura de dados dando

ênfase a sua utilização em algoritmos computacionais com exemplicação de sua

aplicação em um algoritmo de ordenação.

A Seção 1.3 retoma o conceito de algoritmo para denir uma possível apresentação

em linguagem de pseudocódigo para descrever a linguagem em que os algoritmos

serão apresentados, e como alguns desses algoritmos são recursivos, na sequência

uma noção geral de recursividade é, então, apresentada na Seção 1.4.

O conceito de complexidade é introduzido na seção seguinte, 1.5, e é utilizado

para avaliar a eciência de algoritmos, ressaltando que as complexidades são ob-

tidas, geralmente, através de uma notação matemática especial, abordadas mais

detalhadamente sob o tema da análise assintótica.

Finalmente, de certo modo, a Seção 1.6 linka todas as seções anteriores para

descrever o conceito que de fato caracteriza um estudo de análise de algoritmo

e estrutura de dados, a eciência de algoritmos segundo a estrutura de dados de

tamanho n considerada.

1.1 Algoritmos

Informalmente, um algoritmo é qualquer procedimento computacional bem de-

nido que toma algum valor ou conjunto de valores como entrada e produz algum

valor ou conjunto de valores como saída. Portanto, um algoritmo é uma sequência

de etapas computacionais que transformam a entrada na saída.

Também podemos considerar um algoritmo como uma ferramenta para resolver

um problema computacional bem especicado. O enunciado do problema especica,

em termos gerais, a relação desejada entre entrada e saída. O algoritmo descreve um

procedimento computacional especíco para se conseguir essa relação entre entrada

e saída.

Por exemplo, poderia ser necessário ordenar uma sequência de números em ordem

não decrescente. Na prática, esse problema surge com frequência e oferece um solo

fértil para a apresentação de muitas técnicas de projeto e ferramentas de análise

padronizadas.

Contudo, um algoritmo pode ser especicado em linguagem comum como um

programa de computador ou mesmo como um projeto de hardware. O único re-

quisito é que a especicação deve fornecer uma descrição precisa do procedimento

computacional a ser seguido.

1.2 Estrutura de dados (ou da instância)

Uma estrutura de dados é um modo de armazenar e organizar os dados de um

problema com o objetivo de facilitar acesso e modicações. Nenhuma estrutura de

dados única funciona bem para todas as nalidades e, por isso, é importante co-

nhecer os pontos fortes e as limitações de várias delas.


7

Dada, por exemplo, a sequência de entrada 31, 41, 59, 26, 41, 58, um algoritmo

de ordenação devolve como saída a sequência 26, 31, 41, 41, 58, 59. Tal sequência

de entrada é denominada instância do problema de ordenação e, em geral, uma

instância de um problema consiste na entrada (que satisfaz quaisquer restrições

impostas no enunciado do problema) necessária para calcular uma solução para o

problema.

Vejamos como denir formalmente um problema de ordenação pela sua estrutura

de dados de entrada e saída:

Entrada: uma sequência de n números dada por

(m) (n) (l) (2)


a1 , a2 , a3 , . . . , al , . . . , a(1) (3)
m , . . . , an ,

um conjunto A de n números (ou os n dados numéricos de um conjunto A)


onde

(m) (n) (l) (2)


A = {a1 , a2 , a3 , . . . , al , . . . , a(1) (3)
m , . . . , an }

e/ou um conjunto B de palavras (ou os dados alfanuméricos de um conjunto

B) onde

B = {SOL, EST RELA, AT M OSF ERA, . . . , LU A}

são exemplos de instâncias, ou dados de entrada.

Saída: uma permutação (ou reordenação) de entrada tal que, quando a entrada for

numérica, cada elemento de entrada ca disposto como

(2) (l) (m)


a(1) (3)
m < ak < an < · · · < a3 < · · · < a1 · · · < a(n)
m

e de modo que a sequência ca redenida como

(2) (l) (m)


a(1) (3) (n)
m , al , an , . . . , a3 , . . . , a1 , . . . , am ,

o conjunto A é reescrito como

(2) (l) (m)


A = {a(1) (3) (n)
m , al , an , . . . , a3 , . . . , a1 , . . . , am }

e/ou quando a entrada for alfanumérica tem-se

B = {AT M OSF ERA, EST RELA, LU A, . . . , SOL}

são exemplos de instância, ou dados de saída.

Diz-se que um algoritmo é correto se, para toda instância (ou todos os dados

de entrada), ele parar com a saída correta, neste caso dizemos que um algoritmo

correto resolve o problema computacional dado. Um algoritmo incorreto poderia

não parar em algumas instâncias de entrada ou poderia parar com uma resposta

incorreta e ao contrário do que se poderia esperar, às vezes os algoritmos incorretos

podem ser úteis, se pudermos controlar sua taxa de erros.


8

1.3 Apresentação dos Algoritmos

Algoritmos são, pelo menos neste texto, descritos através de uma linguagem de

leitura simples e esta linguagem possui estrutura semelhante a linguagem de pro-

gramação Pascal, por exemplo. Contudo, para facilitar a sua interpretação adota-se

também o estilo do livre formato, quando conveniente.

As convenções seguintes, quando ocorrer, serão utilizadas com respeito à lingua-

gem.

ˆ A linguagem possui uma estrutura de blocos semelhante ao Pascal. O início

e o nal de cada bloco são determinados por endentação, isto é, pela posição

da margem esquerda, de modo que, se uma certa linha do algoritmo inicia

um bloco, ele se estende até a última linha seguinte, cuja margem esquerda

se localiza mais à direita do que a primeira do bloco.

ˆ A declaração de atribuição é indicada pelo símbolo :=.


ˆ As declarações seguintes são empregadas com signicado semelhante ao usual.

se. . . então
se. . . então. . . senão
enquanto. . . faça
para. . . faça
pare

ˆ Variáveis simples, vetores, matrizes e registros são considerados como tradi-

cionalmente em linguagens de programação, de modo que os elementos de

vetores e matrizes são identicados por índices entre colchetes. Por exemplo,

A[5] e B[i, 3] indicam, respectivamente, o quinto elemento do vetor A e o ele-

mento identicado no índice (i, 3), o elemento da matriz B na i−ésima linha

com a 3° coluna.

ˆ No caso de registros, a notação T() indica o campo chave do registro T.

ˆ A referência a registros pode ser também realizada por meio de ponteiros, que

armazenam endereços, com o uso do símbolo ↑. Cada ponteiro é associado a um


único tipo de registro e por essa razão, o nome do registro pode ser omitido.

Por exemplo, pt↑. info representa o campo info de um registro alocado no

endereço contido em pt.

ˆ São usados procedimentos e funções, de modo que a passagem de parâmetros

é feita por referência, isto é, o endereço do parâmetro é transmitido para

a rotina. Essa forma de transmissão possibilita a alteração do conteúdo da

variável utilizada.

ˆ A sentença imediatamente posterior ao símbolo % deve ser interpretada como

comentário.

1.4 Recursividade

Consiste de um tipo especial de procedimento comumente utilizado em implemen-

tações, e consiste daquele que contém, em sua descrição, uma ou mais chamadas a
9

si mesmo. Um procedimento dessa natureza é denominado recursivo, e a chamada

a si mesmo é dita chamada recursiva que, naturalmente, deve possuir pelo menos

uma chamada proveniente de um local exterior a ele e essa chamada é denominada

externa.

Um procedimento não recursivo é, pois, aquele em que todas as chamadas são ex-

ternas e, de modo geral, a todo procedimento recursivo corresponde um outro não

recursivo que executa, exatamente, a mesma computação. Contudo, a recursividade

pode apresentar vantagens concretas.

Frequentemente, os procedimentos recursivos são mais concisos do que um não

recursivo correspondente, e além disso, muitas vezes é aparente a relação direta en-

tre um procedimento recursivo e uma prova por indução matemática. Nesses casos,

a vericação da correção pode se tornar mais simples, entretanto, muitas vezes há

desvantagens no emprego prático da recursividade, pois um algoritmo não recursivo

equivalente pode vim a ser mais eciente.

O exemplo clássico mais simples de recursividade é o cálculo do fatorial de um

inteiro n>0 cujo algoritmo recursivo para efetuar esse cálculo encontra-se descrito

a seguir.

função fat(i)
se i ≤ 1 então fat(i) := 1, senão f at(i) := i · f at(i − 1)

A ideia do algoritmo é muito simples, basta observar que o fatorial de n é n vezes

o fatorial de n˘1, para n > 0. Por convenção, sabe-se que 0! = 1, para o qual

convenciona-se os termos condicionais então e senão.

1.5 Complexidade de Algoritmos

Comumente, uma característica muito importante de qualquer algoritmo é o seu

tempo de execução. Naturalmente, é possível determiná-lo através de métodos em-

píricos, isto é, obter o tempo de execução através da execução propriamente dita

do algoritmo, considerando-se entradas diversas.

Em contrapartida, é possível obter uma ordem de grandeza do tempo de execução

através de métodos analíticos, cujo objetivo é determinar uma expressão matemá-

tica que traduza o comportamento de tempo de um algoritmo. Ao contrário do

método empírico, o analítico visa aferir o tempo de execução de forma indepen-

dente do computador utilizado, da linguagem e dos compiladores empregados e das

condições locais de processamento.

A tarefa de obter uma expressão matemática para avaliar o tempo de um algo-

ritmo em geral não é simples, mesmo considerando-se uma expressão aproximada.

As seguintes simplicações podem ser introduzidas para um modelo proposto.

ˆ Suponha que a quantidade de dados a serem manipulados pelo algoritmo seja

sucientemente grande, isto é, algoritmos cujas entradas consistam em uma

quantidade reduzida de dados não serão considerados. Somente o comporta-

mento assintótico será avaliado, ou seja, a expressão matemática fornecerá


10

valores de tempo que serão válidos unicamente quando a quantidade de dados

correspondente crescer o suciente.

ˆ Não serão consideradas constantes aditivas ou multiplicativas na expressão

matemática obtida, ou seja, a expressão matemática obtida será válida, a

menos de tais constantes.

Como exemplo, sejam os problemas de determinar as matrizes soma C e produto

D entre duas matrizes dadas, A = [ai,j ] e B = [bi,j )], ambas quadradas de dimensão
n. Nesse caso, C e D também possuem dimensão n e seus elementos ci,j e di,j podem
ser calculados, respectivamente, por

ci,j = ai,j + bi,j

n
X
di,j = ai,k + bk,j
k=1

1.6 Eciência

Algoritmos diferentes e criados para resolver o mesmo problema muitas vezes são

muito diferentes em termos de eciência, e tais diferenças podem ser muito mais

signicativas do que as diferenças relativas a hardware e software.

Como exemplo, tomando dois algoritmos de ordenação, que serão abordados nas

seções posteriores, sendo o primeiro conhecido como ordenação por inserção (que

leva um tempo aproximadamente igual a c1 n 2 para ordenar n itens, onde c1 é uma

constante que não depende de n, isto é, ele demora um tempo aproximadamente

proporcional a m1 ) e o segundo, o de ordenação por intercalação (que leva um

tempo aproximadamente igual a c2 n log2 (n) e c2 é outra constante que demora um


tempo aproximadamente proporcional a m2 , que também não depende de n), a or-

denação por inserção normalmente tem um fator constante menor que a ordenação

por intercalação, assim, c1 < c2 .


Na prática, como será mostrado quando estes algoritmos forem abordados, os

fatores constantes podem causar um impacto muito menor sobre o tempo de exe-

cução que a dependência do tamanho da entrada n. Se representarmos o tempo

de execução da ordenação por inserção por c1 n 2 e o tempo de execução da orde-

nação por intercalação por c2 n log2 (n), veremos que, enquanto o fator do tempo

de execução da ordenação por inserção é n, o da ordenação por intercalação é

log2 (n), que é muito menor (por exemplo, n = 1000 ⇒ log2 (n) ≈ 10 enquanto que

n = 107 ⇒ log2 (n) ≈ 20).


Embora a ordenação por inserção em geral seja mais rápida que a ordenação por

intercalação para pequenos tamanhos de entradas, tão logo o tamanho da entrada

n se torne grande o suciente a vantagem da ordenação por intercalação de log2 (n)


contra n compensará com sobras a diferença em fatores constantes. Independente-

mente do quanto c1 seja menor que c2 , sempre haverá um ponto de corte além do

qual a ordenação por intercalação será mais rápida.

Supondo que o computador A execute 101 0 de instruções por segundo (mais ra-

pidamente do que qualquer computador sequencial existente na época da redação


11

deste livro) e que o computador B execute apenas 107 de instruções por segundo,

assim, o computador A será 1000 vezes mais rápido que o computador B em capa-

cidade bruta de computação.

Supondo ainda que um programador médio implemente a ordenação por interca-

lação utilizando uma linguagem de alto nível com um compilador ineciente, sendo

que o código resultante totaliza 50n log2 (n) instruções, para ordenar 107 de núme-

ros, o computador A levará

2(107 )2 inst
= 20000seg
1010 inst/seg

ou seja, mais de 5.5 horas e, por outro lado, o computador B levará

50(107 ) log2 (107 )inst


≈ 1163seg
107 inst/seg

ou seja, apenas 20 minutos no máximo.

Usando um algoritmo cujo tempo de execução cresce mais lentamente, até mesmo

com um compilador fraco, o computador B funciona mais de 17 vezes mais rapida-

mente que o computador A, mostrando mais eciente, sendo portanto, superior a

A.

2 Análise Assintótica de Algoritmos

A ordem de crescimento do tempo de execução de um algoritmo dá uma caracteri-

zação simples da eciência do algoritmo e também permite comparar o desempenho

relativo de algoritmos alternativos.

Embora, às vezes, seja possível determinar o tempo exato de execução de um

algoritmo, o ganho em precisão em geral não vale o esforço do cálculo. Para en-

tradas sucientemente grandes, as constantes multiplicativas e os termos de ordem

mais baixa de um tempo de execução exato são dominados pelos efeitos do próprio

tamanho da entrada, de modo que. quando observamos tamanhos de entrada suci-

entemente grandes para tornar relevante apenas a ordem de crescimento do tempo

de execução, estamos estudando a eciência assintótica dos algoritmos.

Isso signica que estamos preocupados com o modo como o tempo de execução

de um algoritmo aumenta com o tamanho da entrada no limite, à medida que o

tamanho da entrada aumenta sem limitação. Em geral, um algoritmo que é assin-

toticamente mais eciente será a melhor escolha para todas as entradas, exceto as

muito pequenas.

As notações comumente usadas para descrever o tempo de execução assintótico de

um algoritmo são denidas em termos de funções cujos domínios são o conjunto dos

números naturais N = {0, 1, 2, ...}, e tais notações são convenientes para descrever

a função do tempo de execução do pior caso, que em geral é denida somente para

tamanhos de entrada inteiros.

Contudo, às vezes, consideramos que é conveniente abusar da notação assintótica

de vários modos. Por exemplo, é possível estender a notação ao domínio dos núme-

ros reais ou, como alternativa, restringi-la a um subconjunto dos números naturais,

porém, é importante entender o signicado preciso da notação para que, quando

abusos forem necessário, ela não seja mal utilizada.


12

2.1 Notação assintótica, funções e tempos de execução

O conceito de assintótico é vasto, partindo da teoria assintótica com aplicações

de alta abstração nas áreas da matemática teórica, como em análise funcional por

exemplo, e em estatística para praticamente todas as aplicações de propriedades

dos estimadores, em particular em testes e no cálculo de probabilidades.

Em ciências da computação, este conceito é aplicado para descrever o comporta-

mento limite e o desempenho de algoritmos e sistemas físicos, em particular, por

meio da notação assintótica descreve-se o tempo de execução de algoritmos e sis-

temas. Todavia, na prática, tanto a teoria como a notação assintótica aplicam-se a

funções.

As funções às quais se aplica a notação, normalmente caracterizarão os tempos

de execução de algoritmos, porém, a notação pode se aplicar a funções que carac-

terizam algum outro aspecto dos algoritmos, como a quantidade de espaço que eles

usam, por exemplo, ou até mesmo a funções que absolutamente nada têm a ver

com algoritmos. Mesmo quando se utiliza a notação assintótica para o tempo de

execução de um algoritmo, é necessário entender a qual tempo de execução o estudo

está se referindo.

Às vezes, o interesse consiste no tempo de execução do pior caso, no entanto, fre-

quentemente pretende-se caracterizar o tempo de execução, seja qual for a entrada.

Em outras palavras, muitas vezes deseja-se propor um enunciado abrangente que

se aplique a todas as entradas, e não apenas ao pior caso.

Em análise de algoritmos e estrutura de dados as notações assintóticas prestam-se

bem à caracterização de tempos de execução, não importando qual seja a entrada.

Segundo Cormen (2002), analisar um algoritmo signica prever os recursos de que

ele necessitará e, em geral, memória, largura de banda de comunicação e os compo-

nentes do computador são a preocupação primordial, mas frequentemente é o tempo

de processamento (ou computação) que se deseja medir, e a notação assintótica é

proposta para confeccionar isso.

Outra preocupação, tão relevante quanto os recursos que o computador consome,

é o custo de utilização e o tempo de execução que um algoritmo leva para executar

sua tarefa, e é medido por meio de seu execução. Os resultados obtidos podem ser

considerados inadequados quando se leva em conta que

1 a dependência de um compilador, que privilegia algumas construções e outras

não;

2 a dependência de hardwares ; e
3 a quantidade de memória de processamento disponível na máquina.

Uma maneira usualmente empregada para medir o custo de utilização e o tempo

de execução é por meio de um modelo matemático ou por meio de um modelo de

computação genérico com um único processador, a RAM (Random Access Machine

- Máquina de Acesso Aleatório), onde as instruções são executadas uma após a

outra, sem operações concorrentes. Apesar de na RAM estar contida as tarefas

básicas de computadores reais, tais como as operações aritméticas (soma, multi-

plicação, piso, teto, resto) e as instruções de movimentação e controle de dados


13

(carregar, armazenar, copiar, desvio condicional, chamada e retorno de funções),

um modelo matemático é o comumente aplicado.

Para Cormen (2002), deve-se denir as operações e instruções do modelo que

estabelecem os custos associados aos algoritmos, e como as mais necessárias e sig-

nicativas para isso são as que contribuem para o tempo de execução do algoritmo,

tal modelo acaba denido como uma função do custo ou tempo. Tal função é con-

veniente para descrever o tempo de execução do pior caso e, em geral, é denida

somente para tamanhos de entrada inteiros n como segue.

Denição 2.1 (Função complexidade) Seja T uma função não negativa que
representa o custo ou o tempo de execução de um algoritmo em função do tamanho
de entrada n de um dado problema. Deni-se como função complexidade a função
T (n) que estabelece a medida do custo ou de tempo necessário para executar o al-
goritmo de um problema de tamanho n.

Veja que T (n) é uma medida, e comumente está associada ao tempo necessário

para executar um algoritmo, por isso, T pode ser denida ainda como uma função

complexidade de tempo do algoritmo. Nos casos em que T (n) é a medida associada

a memória necessária para a execução do algoritmo, T ca denida como função

complexidade de espaço (ou custo).

É importante enfatizar que T (n) representa qualquer uma das complexidades (de
tempo ou de custo), no entanto, T (n) refere-se diretamente ao número de vezes que

certa operação relevante no algoritmo é executada, sendo essa relevância demarcada

pelo tempo ou o custo de memória.

É comum encontrar na literatura a função complexidade denida como f (n) ou

s(n) para especicar o tratamento de tempo e custo, respectivamente. Contudo,

podemos considerar que é conveniente abusar da notação para medir a complexidade

de um algoritmo considerando simultaneamente seu tempo e custo para denir a

função complexidade como T (n) = f (n) + s(n).

2.2 Elementos da análise assintótica

Nesta seção serão apresentados os tipos de notações assintóticas mais comuns. Na

prática, a notação assintótica consiste de um método para representar, por meio de

majorações ou minorações, o comportamento assintótico das funções de complexi-

dade para relacionar seus comportamentos entre dois ou mais algoritmos.

A análise assintótica parte do princípio do domínio assintótico, cuja denição é

apresentada na sequência.

Denição 2.2 (Domínio assintótico) Sejam f (n) e g(n) duas funções assinto-
ticamente positivas ou não negativas e distintas. Deni-se como domínio assintótico
da função f (n), a função g(n) quando, existem duas constantes estritamente posi-
tivas c e n0 , tais que, para todo n ≥ n0 ocorrer f (n) ≤ cg(n).

Em outras palavras, para todo n ≥ n0 , a função f (n) é igual a g(n) dentro de


um fator constante c e dizemos que g(n) é um limite assintoticamente restrito para
14

f (n). Por isso, considera-se que toda função tomada para satisfazer essa denição

seja assintoticamente não negativa e essa premissa também se mantém para as ou-

tras notações assintóticas que não serão aqui denidas.

Sobretudo, uma função assintoticamente positiva é uma função positiva para todo

n sucientemente grande e será assintoticamente não negativa quando a função for

não negativa sempre que n for sucientemente grande.

A denição de domínio assintótico não necessariamente exige que todos os mem-

bros f (n) e g(n) sejam assintoticamente positiva ou não negativo, isto é, que
f (n) e g(n) sejam positivas ou não negativas sempre que n for sucientemente
grande. Considerando f (n) e g(n) quaisquer, eventualmente assintoticamente nega-

tivas ou não positivas, ou simplesmente negativas em algum ponto n, basta tomar

|f (n)| ≤ c|g(n)|.

Exemplo 2.1 Considerando f (n) = n e g(n) = −n2 , é facilmente vericável que,


para todo n ≥ 1, |f (n)| ≤ c|g(n)|.

De fato!

1
|f (n)| ≤ c|g(n)| ⇒ |n| ≤ c| − n2 | ⇒ n ≤ cn2 ⇒ c ≥
n

onde, xando c = n−1 , para qualquer n ocorre |f (n)| = |g(n)| e a desigualdade é

satisfeita quando for xado que c = n−1


0 para qualquer n0 > 0, pois

|f (n)| ≤ c|g(n)| ⇒ n ≤ n0−1 n2 ⇒ n0 n ≤ n2 ⇒ n ≥ n0 > 0

ou seja, para qualquer n0 > 0 existe um c, e c = n−1


0 , tais que |f (n)| = n ≤ cn2 =
c|g(n)|. Basta tomar n0 = 1.

Exemplo 2.2 Considerando f (n) = (n + 1)3 e g(n) = n3 , verica-se que essas


funções dominam assintoticamente uma a outra, isto é, existem c e n0 pelos quais
se vericaque f (n) domina assintoticamente g(n) e g(n) domina assintoticamente
f (n).

De fato! Note que, para todo n ≥ 1, f (n) = (n + 1)3 = n3 + 3n2 + 3n + 1 e


3
g(n) = n são estritamente positivas e tais que

|f (n)| = n3 + 3n2 + 3n + 1 ≤ n3 + 3n3 + 3n3 + n3 = 8n3 ≤ cn3 = c|g(n)| ⇒ c ≥ 8

e, é imediato que |g(n)| = n3 ≤ c(n + 1)3 = c|g(n)| para qualquer c ≥ 1.


Portanto, em ambos os casos existem as constantes c e n0 , sejam elas xadas
como n0 = 1 e c = 9 para constatar que ocorrem |f (n)| ≤ c|g(n)| e |g(n)| ≤ c|f (n)|.
3 3 3 3 3
Em particular, para todo n ≥ 1, n ≤ 9(n + 1) e (n + 1) ≤ 8n ≤ 9n .

De certo modo, a análise assintótica, por meio de alguma das notações que serão

denidas a seguir, deriva da denição 2.2. Na prática, as funções f (n) e g(n) são

funções de complexidade, isto é, f (n) = T1 (n) e g(n) = T2 (n).


15

Como a variável independente das análises é o valor n, conclui-se que as com-

plexidades de qualquer caso são todas iguais entre si para cada algoritmo, tanto

que, quando se considera o número de passos efetuados por um algoritmo despreza-

se as constantes aditivas ou multiplicativas. Por exemplo, um número de passos

igual a 3n será aproximado para n, além disso, como o interesse é restrito a valo-

res assintóticos, termos de menor grau também podem ser desprezados, tal como

100n2 + 10n + 1 passos ca aproximado por n2 .


Torna-se útil, portanto, descrever operadores matemáticos que sejam capazes de

representar situações como essas, este é o principal objetivo das notações assintóticas

para funções reais positivas de variável inteira n que representam as complexidades.

2.2.1 Notação O

De uma forma simples, a notação O limita assintoticamente uma função por ma-

joração, ou seja, quando temos apenas um limite assintótico superior. Para uma

dada função g(n), denotamos por O[g(n)] (lê-se Ó 'grande' de g de n ou, às vezes,

apenas ó de g de n) o conjunto de funções

O[g(n)] = {f (n) ∈ R∗+ : ∃ c, n0 ∈ R∗+ ⇒ ∀ n ≥ n0 > 0, f (n) ≤ cg(n)}.

A condição f (n) ≤ cg(n) permite armar que f (n) é dominada assintoticamente

g(n) dentro de um fator constante c e para todos os valores de n à direita de n0 ,


isto é, o valor da função f (n) está em ou abaixo g(n) por uma constante c e sempre
que n for maior que n0 . Matematicamente, dene-se, sob o contexto da denição

2.2, a notação O pode ser denida como segue.

Denição 2.3 (Convergência Forte da Notação O) Sejam f (n) e g(n) duas


funções não negativas. Dizemos que f (n) é dominada assintoticamente por g(n)
quando

f (n)
lim = 0.
n→∞ g(n)

Exemplo 2.3 Sejam f (n) = 10n + 7 e g(n) = 3n2 + 2n + 6. Então existem as


constantes c, n0 > 0 tais que f (n) é O[g(n)].

De fato! Pela denição anterior

f (n) 10n + 7 10n−1 + 7n−2 0


lim = lim 2
= lim −1 −2
= = 0.
n→∞ g(n) n→∞ 3n + 2n + 6 n→∞ 3 + 2n + 6n 3

ou seja,f (n) = 10n + 7 é dominada assintoticamente por g(n) = 3n2 + 2n + 6, razão


pela qual existem as constantes c, n0 > 0 tais que f (n) é O[g(n)].

Veja ainda que, somando 0 = 8n − 8n + 1 − 1 em g(n, resulta que

f (n) ≤ cg(n) ⇒ 10n+7 ≤ c(3n2 +2n+6+8n−8n+1−1) = c(3n2 −8n−1)+c(10n+7)


16

onde, xando c = 1, resulta ainda que

10n + 7 ≤ 3n2 − 8n − 1 + 10n + 7 ⇒ 0 ≤ 3n2 − 8n − 1

cuja desigualdade no segundo membro da implicação é verdadeira para todos os

n ≥ 3.
Portanto, existem as constantes c, n0 > 0 tais que f (n) é O[g(n)], e elas valem

c=1 e n0 = 3.

Com base no conjunto O[g(n)], uma segunda denição para a notação O pode ser

descrita como segue.

Denição 2.4 (Notação O) Dados f (n) e g(n), duas funções não negativas, po-
demos armar que f (n) ∈ O[g(n)] se, e somente se, para qualquer c > 0 e todos os
n ≥ n0 > 0 ocorrer f (n) ≤ cg(n).

Exemplo 2.4 Seja f (n) = n2 /3 − 3n. Então f (n) é O(n2 ), quando c = n0 = 1.

De fato! Neste caso temos que g(n) = n2 , e como f (n) = n2 /3 − 3n = (n2 − 9n)/3,
tem-se

f (n) n2 − 9n 1 + 9n−1 1
lim = lim 2
= lim = > 0.
n→∞ g(n) n→∞ 3n n→∞ 3 3

do qual nada podemos armar pela denição 2.3. Mas

n2 − 9n
f (n) = ≤ cn2 = cg(n) ⇔ n − 9 ≤ 3cn ⇔ n ≤ 3cn + 9
3

onde, xando c = 1 e n = n0 tem-se n0 ≤ 3n0 +9 ⇒ 1 ≤ 3+9n−1


0 , no qual, tomando
o limite no innito à direita em ambos os membros, verica-se que

1 = lim 1 ≤ lim 3 + 9n−1


0 = 3.
n0 →∞ n0 →∞

Logo, quando n0 → ∞ a implicação 1 ≤ 3+9n−1 0 é verdadeira e, consequentemente,


n0 ≤ 3n0 + 9 também o é e, se para todo n0 ≥ 0 é verdadeira, o é para n0 = 1.
2 2
Portanto, quando c = n0 = 1 ocorre f (n) ≤ cn e conclui-se que f (n) é O(n ).

A notação O é comumente empregada para exprimir a complexidade do algo-

ritmo, geralmente empregado para determinar as complexidades de pior caso. Essa

notação, bem como as que são denidas a seguir, apresentam a propriedade de um

número n de passos manter-se o mesmo quando aplicados a entradas diferentes de

mesmo tamanho, ou seja, para um mesmo valor de n o número de passos mantém-se


constante.
17

2.2.2 Notação Ω

Da mesma maneira que a notação O fornece um limite assintótico superior para

uma função, a notação Ω nos dá um limite assintótico inferior. Para uma determi-
nada função g(n), denotamos por Ω[g(n)] (lê-se ômega grande de g de n ou, às
vezes, ômega de g de n) o conjunto de funções

Ω[g(n)] = {f (n) ∈ R∗+ : ∃ c, n0 ∈ R∗+ ⇒ ∀ n ≥ n0 > 0, 0 < cg(n) ≤ f (n)}

onde, se c ∈ R∗+ , então cg(n) > 0 ⇒ g(n) > 0.


Quando dizemos que o tempo de execução de um algoritmo é Ω[g(n)], queremos

dizer que, não importando qual entrada especíca de tamanho n seja escolhida para
cada valor de n, o tempo de execução para essa entrada é no mínimo uma constante
vezes g(n), para n sucientemente grande.
De modo equivalente, estamos dando um limite inferior para o tempo de execução

de um algoritmo que, sob o contexto de domínio assintótico, dene-se:

Denição 2.5 (Convergência Forte da Notação Ω) Sejam f (n) e g(n) duas


funções não negativas. Dizemos que f (n) ∈ Ω[g(n)] quando g(n) é dominada assin-
toticamente por f (n), ou seja, quando

g(n)
lim = 0.
n→∞ f (n)

Exemplo 2.5 Sejam f (n) = 3n2 e g(n) = log (n). Então existem as constantes
c, n0 > 0 tais que f (n) é Ω[g(n)].

De fato! Pela denição anterior

g(n) log n
lim = lim = 0.
n→∞ f (n) n→∞ 3n2

então, pelo teorema do confronto, sejam g1 (n) = 0 e g2 (n) = n, tais que, para todo

n>0 resulta que g1 (n) < g(n) < g2 (n) e, no limite

0 = lim 0 < lim log n ≤ lim 3n2 ⇒ lim g1 (n) < lim g(n) ≤ lim g2 (n).
n→∞ n→∞ n→∞ n→∞ n→∞ n→∞

Daí, como

g1 (n) g(n) g2 (n) 0 log n n


< ≤ →0= < 2
< 2
f (n) f (n) f (n) f (n) 3n 3n

então

g1 (n) log n g(n) g2 (n) n


0 = lim < = lim ≤ lim = lim =0
n→∞ f (n) 3n2 n→∞ f (n) n→∞ f (n) n→∞ 3n2

ou seja, pelo teorema do confronto

g(n) log n
0 < lim = lim ≤0
n→∞ f (n) n→∞ 3n2
18

Logo, g(n)/f (n) é limitada em ambos os lados por 0, e então pela denição 2.5 g(n)
é dominada assintoticamente por f (n) e podemos armar que f (n) ∈ Ω[g(n)] e que
existem um c, n0 > 0 tais que para todos os n ≥ n0 ocorre f (n) ≥ cg(n).
2
Portanto, dadas f (n) = 3n e g(n) = log (n), para as constantes c = n0 = 1

conclui-se que f (n) é Ω[g(n)].

Em outras palavras, quando dizemos que o tempo de execução f (n) de um


algoritmo é Ω[g(n)], isto é, armamos que dadas as complexidades f (n) e g(n),
f (n) = Ω[g(n)] e que independente da entrada especíca de tamanho escolhido
para qualquer valor de n, o tempo de execução sobre ela é pelo menos cg(n) para

um valor de n sucientemente grande.

Formalmente, podemos ainda denir

Denição 2.6 (Notação Ω) Dados f (n) e g(n), duas funções não negativas, po-
demos armar que f (n) ∈ Ω[g(n)] se, e somente se, para qualquer c > 0 e todos os
n ≥ n0 > 0 ocorrer f (n) ≥ cg(n).

Exemplo 2.6 Seja f (n) = n2 /2 − 3n. Então f (n) é Ω(n2 ) para quaisquer c > 1/7
com n0 = 7.

De fato! Seja g(n) = n2 , então

n2 n2 n2 1
f (n) = − 3n ≥ cn2 = cg(n) ⇔ ≥ cn2 + 3n > cn2 ⇔ > cn2 ⇔ c <
2 2 2 2

ou seja, como as desigualdades são verdadeiras, existe um c de modo que f (n) ≥


cg(n) e tal que c < 1/2.
Neste problema, a obtenção de c e n0 , estritamente positivos, pode ser efetuada por
meio de método numérico para diferentes valores de c ≤ 1/2. Por exemplo, xando

que c = 1/k , para todos os k > 2 e inteiros, verica-se que c = 1/3 ⇒ n0 = 18,

c = 1/4 ⇒ n0 = 12, c = 1/5 ⇒ n0 = 10, ∀ c ∈ [1/6, 1/7] ⇒ n0 = 9,


∀ c ∈ [1/8, 1/13] ⇒ n0 = 8 e ∀ c ≥ 1/14 ⇒ n0 = 7. Em particular, f (n) é
O[g(n)] para qualquer c ≥ 1/2.

Exemplo 2.7 Sejam f (n) = 2n3 + 3n2 + n e g(n) = n3 . Então existem as cons-
tantes c, n0 > 0 tais que 2n3 + 3n2 + n = O(n3 ).

De fato!

f (n) ≥ cg(n) ⇔ 2n3 +3n2 +n = n3 (2+3n−1 +n−2 ) ≥ cn3 ⇔ 2+3n−1 +n−2 ≥ c.

Note ainda que, para todo n ≥ n0 = 0

c ≤ 2 + 3n−1 + n−2 = 2 + 3 + 1 = 6 ⇔ c ≤ 6.

Portanto, para qualquer 0 < c ≤ 6, verica-se que 2n3 + 3n2 + n ≥ cn3 , para todo

n > 0, isto é, que 2n + 3n2 + n = O(n3 ).


3
19

2.2.3 Notação Θ

A notação Θ é utilizada para dar um limite assintótico rme sobre uma função.

Para todos os valores de n à direita de n0 o valor da função f (n) é majorada por


c1 g(n) e minorada por c2 g(n), ou seja, f (n) = g(n) a menos das constantes c1 e c2 .
Para uma determinada função g(n) > 0, denotamos por Θ[g(n)] (lê-se téta grande

de g de n ou, às vezes, téta de g de n) o conjunto de funções

Θ[g(n)] = {f (n) ∈ R∗+ : ∃ c1 , c2 , n0 ∈ R∗+ ⇒ ∀ n ≥ n0 > 0, c1 g(n) ≤ f (n) ≤ c2 g(n)}

onde, se c ∈ R∗+ , então g(n) > 0 ⇒ c1 g(n) > 0.


Essa notação não admite uma denição sob o contexto de domínio assintótico

pois, teoricamente, a menos das constantes c1 e c2 tem-se f (n) = g(n) e mesmo

quando f (n) ̸= g(n) apenas um dos casos descritos pelas denições 2.4 e 2.5 é

atendido. No entanto, formalmente, deni-se como segue.

Denição 2.7 (Notação Θ) Dados f (n) e g(n), duas funções não negativas, po-
demos armar que f (n) ∈ Θ[g(n)] se, e somente se, para qualquer c1 , c2 , n0 > 0,
com c1 ̸= c2 e todos os n ≥ n0 > 0 ocorrer c1 g(n) ≤ f (n) ≤ c2 g(n).

Simplesmente, dizemos que função f (n) pertence ao conjunto Θ[g(n)] se existirem


constantes positivas c1 e c2 pelos quais possamos encaixada f (n) entre c1 g(n) e

c2 g(n), para um valor de n sucientemente grande. Como Θ[g(n)] é um conjunto,


podemos ainda escrever  f (n) ∈ Θ[g(n)] para indicar que f (n) é um membro de

(ou pertence a) Θ[g(n)].

Como nas notações anteriores, também podemos escrever  f (n) = Θ[g(n)] para

expressar a mesma noção. Esse abuso da igualdade para denotar a condição de

membro de um conjunto (pertinência) pode parecer confuso, mas tem a vantagem

prática e simplicada de resumir seu signicado.

Consideremos, por exemplo, qualquer função quadrática f (n) = an2 +bn+c, onde
a, b e c são constantes quais quer em R e a restrição a > 0. Descartando os termos
2
de ordem mais baixa e ignorando a constante, produzimos f (n) = Θ(n ), mas, for-

malmente, para mostrar a mesma coisa, tomamos as constantes c1 = a/4, c2 = 7a/4


p
e n0 = 2 max |b|/a, |c|/a.

Exemplo 2.8 Sejam f (n) = n2 /2 + 3n. Então, para as constantes c1 = 1/8 e


c2 = 1/2, f (n) = Θ(n ), para todos os n ≥ n0 = 8.
2

De fato! Pelo exemplo 2.6, verica-se f (n) = Ω[g(n)] para todos os n ≥ n0 = 7


quando c = 1/14 e f (n) = O[g(n)] para todos os n ≥ n0 = 1 quando c = 1/2.

Portanto, tomando c1 = 1/14, c2 = 1/2 e n ≥ n0 = 7 verica-se, simultanea-

mente, que f (n) ≥ c2 g(n) ⇒ f (n) = Ω[g(n)] e f (n) ≤ c2 g(n) ⇒ f (n) = O[g(n)] ou
2
seja, que c1 g(n) ≤ f (n) ≤ c2 g(n) ⇒ f (n) = Θ(n ).

Quando escrevemos f (n) = Θ[g(n)], estamos armando que a curva formada

por f (n) está contida em [c1 g(n), c2 g(n)] exceto, possivelmente, para todos os n à
20

esquerda de n0 > 0. Em todos os casos, c1 e c2 são constantes positivas de modo


que c1 reduz e c2 aumenta o tamanho de g(n) para conterem f (n) para todos os
n ≥ n0 > 0.

2.2.4 Notação o

Informalmente, a notação O é utilizada para descrever o que ca denido usando a


notação Θ, o que se convém chamar de limites assintoticamente justos. No entanto,

o limite assintótico superior fornecido pela notação O pode ser ou não assintotica-

mente justo.

O limite f1 (n) = 2n2 = O(n2 ), por exemplo, é assintoticamente justo, mas o


2 2
limite f2 (n) = 2n = O(n ) não o é. Veja que f1 (n) = 2n e f2 (n) = 2n diferem-se

como polinômios por um grau, sendo assintoticamente justo o polinômio f1 (n) por
2
ser de mesma ordem que o polinômio g(n) = n .

Usamos a notação o[g(n)] (lê-se ó pequeno de g de n) para denotar um limite

superior que não é assintoticamente justo e que, formalmente, tem denição que,

apesar de apresentar diferenças sutis com as notações assintóticas denidas anteri-

ormente, ca mais facilmente interpretada como o conjunto

o[g(n)] = {f (n) ∈ R+ : ∃ c, n0 ∈ R∗+ ⇒ ∀ n ≥ n0 > 0, 0 ≤ f (n) < cg(n)}

que possibilita uma semelhança signicante com a denição da notação O.


A primeira diferença que se observa é o fato de que f (n) ∈ R+ , isto é, f (n) não

é necessariamente restrito nessa notação. Isso, e sua semelhança com a notação O


indica que, intuitivamente, sob essa notação uma função f (n) se torna insignicante
em relação a g(n) à medida que n → ∞, ou seja, que

f (n)
lim =0
n→∞ g(n)

o que permite armar que a convergência forte das notações O e o são as mes-
mas, mas em o se obtém uma convergência forte restrita à cg(n), de modo que
f (n) ≤ cg(n) ⇒ f (n) = O[g(n)] e f (n) < cg(n) ⇒ f (n) = O[g(n)].
Note que, pela denição do conjunto mostrado acima, é redundante a arma-

ção de que a desigualdade 0 ≤ f (n) < cg(n) seja a condição para que elementos
f (n) ∈ o[g(n)] ⊆ R∗+ , pois sef (n) ∈ R∗+ , é evidente que f (n) ≥ 0. Entretanto,
tal redundância surge como um pretexto para estabelecer a condição análoga a

0 ≤ f (n) < cg(n) como 0 ≤ c1 g(n) ≤ f (n) < c2 h(n), para interpretarmos o con-

junto o[g(n)] como

o[g(n)] = {f (n) ∈ R+ : ∃ k, c, n0 ∈ R+ ⇒ ∀ n ≥ n0 > 0, kg(n) ≤ f (n) < cg(n)}

Nas condições que denem este conjunto, vale ressaltar que k, c, n0 ∈ R+ indica
que k, c e n0 não são estritamente positivos mas, a condição n ≥ n0 > 0 destaca
que n0 é estritamente positivo. Além disso, como f (n) ∈ R+ e, como no conjunto

denido no início dessa seção, podemos ter f (n) = 0 de modo que a condição

c′ g(n) ≤ f (n) < ch(n) permite estabelecer que cg(n) > 0, destacando que c e g(n)
21

são estritamente positivos, e que 0 ≤ kg(n) ≤ f (n) com g(n) > 0 implica que

kg(n) = f (n) = 0 apenas quando k = 0.


Esse segundo conjunto fornece alguns traços do conjunto Ω[g(n)], e fornece a ideia
de uma denição formal para a notação o[g(n)] como segue.

Denição 2.8 (Notação o) Dados f (n) e g(n), duas funções não negativas, po-
demos armar que f (n) ∈ o[g(n)] se, e somente se, para qualquer k ≥ 0 e c, n0 > 0,
com k ̸= c e todos os n ≥ n0 > 0, ocorrer que f (n) ̸∈ Ω[g(n)] e f (n) ∈ O[g(n)] (ou
seja, se simultaneamente ocorrer f (n) ̸= Ω[g(n)] e o caso assintoticamente justo
de f (n) = O[g(n)]), ou seja, que 0 ≤ kg(n) < f (n) e f (n) ≤ cg(n), respectivamente.

Exemplo 2.9 Sejam f (n) = 3n + 2. Então existem as constantes k, c e n0 de modo


que, para todos os n ≥ n0 > 0, verica-se que f (n) = o(n2 ), mas f (n) ̸= o(n).

De fato!

Primeiramente, vamos mostrar que f (n) = o(n2 ) e para isso devemos mostrar

que f (n) = O(n ), assintoticamente justo (AJ) e com f (n) ̸= Ω(n2 ). Seja então
2

g(n) = n2 , como f (n) ∈ O(n2 ) ⇔ f (n) ≤ cg(n), sobre 3n + 2 ≤ cn2 verica-se que

3n + 2 ≤ 3n + 2n = 5n ≤ 5n2 < 6n2 = cn2 ⇒ f (n) = 3n + 2 ∈ O(n2 ) = O[g(n)]

ou seja, 3n + 2 ≤ cn2 ⇒ 3n + 2 ∈ O(n2 ) e como f (n) = 3n + 2 e g(n) = n2 são

polinômios de graus diferentes, são também AJ.

Além disso, veja que 3n + 2 ≤ cn2 ⇒ 3n + 2 ̸∈ Ω(n2 ), pois 3n + 2 ̸∈ Ω(n2 ) ⇒


3n + 2 ≥ cn2 que não ocorre entre f (n) = 3n + 2 e g(n) = n2 .
Portanto, existem e tomados como k = 0, c = 5 e n0 = 1, verica-se simultanea-
2 2
mente que 0 = 0n = kg(n) < 3n+2 = f (n) e que f (n) = 3n+2 ≤ 5n = cg(n) para
2
todo n ≥ 1, pelos quais f (n) ̸∈ Ω[g(n)] e f (n) ∈ O[g(n)] de modo que f (n) = o(n ).
2
Em particular, para k = 0, c = 6 e todo n ≥ 1 tem-se 0 = 0n = kg(n) ≤ 3n + 2 ≤

5n2 .
Agora, para mostrar que f (n) ̸= o(n) basta mostrar que f (n) = Ω(n). Para isso,

basta observar que, para h(n) = n

3n + 2 ≥ 3n = cn ⇒ f (n) = 3n + 2 ∈ Ω(n) = O[h(n)]

Além disso, verica-se facilmente que ocorre também um domínio assintotica-

mente não justo entre f (n) = 3n + 2 e h(n) = n, isto é, f (n) ∈ O[h(n)] mas não

são AJ.

Portanto, f (n) ̸= o(n), pois as condições necessárias não são satisfeitas. Em par-

ticular, f (n) = Ω(n).

2.2.5 Notação ω

Por analogia, a notação ω está para a notação Ω como a notação o está para a

notação O. Usamos a notação ω para denotar um limite inferior que não é assinto-

ticamente preciso e um modo de deni-lo é por meio do conjunto

ω[g(n)] = {f (n) ∈ R+ : g(n) ∈ o[g(n)]}


22

porém, mais especicamente, denimos ω[g(n)] (lê-se ômega pequeno de g de n)


como o conjunto

ω[g(n)] = {f (n) ∈ R+ : ∃ c, n0 ∈ R∗+ ⇒ ∀ n ≥ n0 > 0, 0 ≤ cg(n) < f (n)}

Note que a notação ω é a contra partida da notação o no contexto de convergência


forte, pois sob essa notação uma função f (n) se torna arbitrariamente qrande em

relação a g(n) à medida que n → ∞, ou seja, que

f (n)
lim = ∞.
n→∞ g(n)

Formalmente, dene-se:

Denição 2.9 (Notação ω) Dados f (n) e g(n), duas funções não negativas, po-
demos armar que f (n) ∈ ω[g(n)] se, e somente se, para qualquer k ≥ 0 e c, n0 > 0,
com k ̸= c e todos os n ≥ n0 > 0, ocorrer que g(n) ̸∈ Ω[f (n)] e g(n) ∈ O[f (n)] (ou
seja, se simultaneamente ocorrer g(n) ̸= Ω[f (n)] e o caso assintoticamente justo
de g(n) = O[f (n)]), ou seja, que 0 ≤ kf (n) < g(n) e g(n) ≤ cf (n), respectivamente.

3 Relações de Recorrência

Intitula-se como paradigma divisão e conquista a resolução recursiva de um pro-

blema aplicando três etapas em cada nível da recursão:

Divisão do problema em certo número de subproblemas que são instâncias menores

do mesmo problema.

Conquista (resolução) dos subproblemas resolvendo-os recursivamente, entre-

tanto, se os tamanhos dos subproblemas forem sucientemente pequenos,

basta resolvê-los de modo direto.

Combinação as soluções dos subproblemas na solução para o problema original.

Entretanto, dizemos que as resoluções de recorrências andam de mãos dadas com

o paradigma divisão e conquista porque nos dão um modo natural de caracterizar

os tempos de execução de algoritmos de divisão e conquista. Uma recorrência é

uma equação ou desigualdade que descreve uma função em termos de seu valor

para entradas menores. Por exemplo, descrevemos o tempo de execução do pior

caso T (n) do procedimento pela recorrência

T (1) = Θ(1), para n=1


T (n) = 2T (n/2) + Θ(n), para n≥2

cuja solução armamos ser T (n) = Θ[n log(n)].


As recorrências podem tomar muitas formas, como por exemplo, um algoritmo

recursivo poderia dividir problemas em tamanhos desiguais, como uma subdivisão


23

2/3 para 1/3. Se as etapas de divisão e combinação levarem tempo linear, tal algo-

ritmo dará origem à recorrência T (n) = T (2n/3) + T (n/3) + Θ(n).


Os subproblemas não estão necessariamente restritos a ser uma fração constante

do tamanho do problema original, uma versão recursiva da busca linear por exem-

plo, criaria apenas um subproblema contendo somente um elemento a menos do que

o problema original. Cada chamada recursiva levaria tempo constante mais o tempo

das chamadas recursivas que zer, o que produz a recorrência T (n) = T (n−1)+Θ(1).
Este capítulo apresenta três métodos para resolver recorrências, isto é, para obter

limites assintóticos  Θ ou  O  para a solução.

ˆ No método de substituição, arriscamos um palpite para um limite e então


usamos indução matemática para provar que nosso palpite estava correto.

ˆ O método da árvore de recursão converte a recorrência em uma árvore

cujos nós representam os custos envolvidos em vários níveis da recursão, e

então usamos técnicas para limitar somatórios para resolver a recorrência.

ˆ O método mestre dá limites para recorrências da forma T (n) = aT (n/b) +


f (n), onde a ≥ 1, b > 1 e f (n) é uma função dada.

Tais recorrências ocorrem frequentemente. Uma recorrência da forma T (n) =


aT (n/b) + f (n) caracteriza um algoritmo de divisão e conquista que cria a subpro-

blemas, cada um com 1/b do tamanho do problema original e no qual as etapas de

divisão e conquista, juntas, levam o tempo f (n).


Para utilizar o método mestre, você terá de memorizar três casos, porém, com

isso, será fácil determinar limites assintóticos para muitas recorrências simples. So-

bretudo, usaremos o método mestre para determinar os tempos de execução de

algoritmos de divisão e conquista para o problema do subarranjo máximo e para a

multiplicação de matrizes, bem como para outros algoritmos baseados no método

de divisão e conquista em outros tipos de problemas.

Ocasionalmente veremos recorrências que não são igualdades, porém, mais exata-

mente, desigualdades, como T (n) ≤ 2T (n/2) + Θ(n). Como tal recorrência declara
somente um limite superior para T (n), expressaremos sua solução usando a no-

tação O em vez da Θ. De modo semelhante, se a desigualdade for invertida para

T (n) ≥ 2T (n/2) + Θ(n), então, como a recorrência dá apenas um limite inferior


para T (n), usaríamos a notação Ω em sua solução.

3.1 Detalhes técnicos

Esta seção considera algumas funções e notações matemáticas padrões para ex-

plorar as relações entre as relações de recorrências consideradas, além de também

ilustrar o uso das notações assintóticas como o texto anterior já destacou.

Conceitos como monotonicidade de uma função f (n), no caso crescente (monotoni-


cidade crescente) quando para todos os m ≤ n tem-se f (m) ≤ f (n) ou descrescente

(monotonicidade decrescente) quando para todos os m ≥ n tem-se f (m) ≥ f (n), e

os casos monotônicos estritos (estritamente crescente) quando para todos os m < n

tem-se f (m) < f (n) ou (estritamente decrescente) quando para todos os m > n

tem-se f (m) > f (n).


24

Funções polinomiais, exponenciais, logarítmicas e fatoriais são também frequen-

temente empregadas para estabelecer as relações. Frequentemente, usa-se a notação

fi (n) para denotar a função f (n) aplicada iterativamente i vezes a um valor inicial
de n e, formalmente, se f (n) é uma função no domínio dos números reais, para

inteiros não negativos i, deni-se recursivamente


n, para i=0
f (n) =
f [f (n)], para i≥1

Outro importante conceito que comumente se aplica são os de pisos e tetos para

qualquer número n Nas aplicações denotamos o maior inteiro menor ou igual a n


por ⌊n⌋ (lê-se o piso de n) e o menor inteiro maior ou igual a n por ⌈n⌉ (lê-se o

teto de n).
Deste modo, para qualquer número n

n − 1 < ⌊n⌋ ≤ n ≤ ⌈n⌉ < n + 1

sob propriedades importantes, tais como

  j k   l m
⌊n/a⌋ n ⌈n/a⌉ n
= com =
b ab b ab

   
lam a + (b + 1) jak a − (b + 1)
≤ com ≥
b b b b

Além disso, tais conceitos podem ser ser aplicados simultaneamente para destacar

uma aplicação ou denir outros conceitos, como a aritmética modular, quando para

qualquer inteiro a e qualquer inteiro positivo n, o valor a mod n é o resto do

quociente a/n

a mod n = a − ⌊a/n⌋n

donde segue que 0 ≤ a mod n < n.


Vale ressaltar que, dada uma noção bem denida do resto da divisão de um inteiro

por outro, é conveniente providenciar notação especial para indicar a igualdade de

restos. Se (a mod n) = (b mod n), escrevemos a ≡ b(mod n) e dizemos que a é


equivalente a b, módulo n.

Em outras palavras, a ≡ b (mod n) se a e b têm o mesmo resto quando divididos

por n. De modo equivalente, a ≡ b (mod n) se, e somente se, n é um divisor de

b − a. Escrevemos a ≡
/ b (mod n) se a não é equivalente a b, módulo n.
Sobretudo, negligenciamos certos detalhes técnicos práticos quando enunciamos e

resolvemos recorrências. Por exemplo, se ao chamar um algoritmo conhecido como

MERGE-SORT para n elementos quando n é ímpar, terminaremos com subproble-

mas de tamanho n/2 e n/2, mas nenhum dos tamanhos é realmente n/2, porque
25

n/2 não é um inteiro quando n é ímpar.

Tecnicamente, a recorrência que descreve o tempo de execução de MERGE-SORT

é, na realidade


Θ(1), se n = 1
T (n) = l n m l n m
T +T + Θ(n), se n≥2
2 2

As condições de contorno representam uma outra classe de detalhes que em geral

ignoramos. Visto que o tempo de execução de um algoritmo para uma entrada

de tamanho constante é uma constante, as recorrências que surgem dos tempos

de execução de algoritmos geralmente têm T (n) = Θ(1) para n sucientemente

pequeno.

Em consequência disso, por conveniência, em geral omitiremos declarações sobre

as condições de contorno de recorrências e consideraremos que T (n) é constante para


n pequeno. Quando enunciamos e resolvemos recorrências, muitas vezes, omitimos

pisos, tetos e condições de contorno.

3.2 Métodos Para Resolver Recorrências

Como as recorrências caracterizam os tempos dos algoritmos de divisão e con-

quista, abordaremos nessa seção como resolver recorrências.

Infelizmente, não há nenhum modo geral para a obtenção das soluções corretas

para recorrências, por isso, arriscar um palpite para uma solução exige experiência

e, ocasionalmente, criatividade. Um modo de dar um bom palpite é comprovar li-

mites superiores e inferiores frouxos para a recorrência e, então, reduzir a faixa de

incerteza sobre o palpite dado.

Entretanto, por sorte, podemos usar a heurística para estimar uma solução e tam-

bém as árvores de recursão, que veremos posteriormente, para gerar bons palpites.

As vezes, você pode dar um palpite correto para um limite assintótico para a solução

de uma recorrência mas, por alguma razão, a matemática não consegue funcionar

na indução.

Em geral, observamos que a hipótese indutiva não é sucientemente forte para

comprovar o limite apontado. Se revisarmos nosso palpite subtraindo um termo de

ordem mais baixa quando chegar a um impasse como esse, a matemática frequen-

temente funcionará.

Sobretudo, o método de divisão e conquista produz um algoritmo que é assintoti-

camente mais rápido do que o método da força bruta, e produzirá o algoritmo assin-

toticamente mais rápido para um problema. Isso depende, obviamente, da aplicação

eciente de algum método para a resolução de recorrências, como os que veremos a

seguir.

3.2.1 Método de Substituição

O método de substituição para resolver recorrências envolve duas etapas:

1 arriscar um palpite (solução suposta) para a forma da solução; e


26

2 usar indução para determinar as constantes e mostrar que a solução funciona.

e então substituímos a forma da solução pela solução suposta na primeira etapa

quando aplicamos a hipótese indutiva a valores menores, daí o nome método de

substituição.

Esse método é poderoso, mas temos que adivinhar a forma da resposta para,

dentre diversas outros artifícios matemáticos que se pode aplicar no método de

substituição, estabelecer limites superiores ou inferiores para uma recorrência.

Como exemplo, vamos determinar um limite superior para uma suposta recor-

rência, arriscamos o palpite de que a solução é T (n) = O[n lg(n)]. O método de


substituição requer que provemos que T (n) ≤ cn log(n) para uma escolha apropri-
ada da constante c > 0.
Tal tarefa começa considerando que esse limite se mantém válido para todo

n > n0 ≥ 0 positivo. Numa situação particular para n/2, o que produz T (n/2) ≤
cn/2 lg(n/2), substituindo na recorrência obtemos

h jnk j n ki n


T (n) ≤ 2 c log + n ≤ cn log + n = cn log(n) + n − cn log(2) ≤
2 2 2
≤ cn log(n)

onde, xando que T (n) > 0, a última etapa é válida desde que c≥1 e n0 = 1.
Agora, a indução exige que mostremos que essa solução se mantém válida para as

condições de contorno. Normalmente, fazemos isso mostrando que as condições de

contorno são adequadas como casos-base para a prova indutiva, mas, nos casos gerais

de recorrência, devemos mostrar que podemos escolher a constante c sucientemente


grande de modo que o limite T (n) ≤ cn log(n) também funcione para as condições

de contorno.

Às vezes, essa exigência pode gerar problemas, como por exemplo, ao supor como

argumento que T (1) = 1 seja a única condição de contorno da recorrência, então,


para n = 1, o limite T (n) ≤ cn log(n) produz T (1) ≤ c log(1) = 0, o que está em

desacordo com T (1) = 1. Consequentemente, o caso-base de nossa prova indutiva

deixa de ser válido.

Isso nos remete a notação assintótica que só exige que provemos que T (n) ≤
cn log(n) para n ≥ n0 , onde n0 é uma constante de nossa escolha. Então, mantendo

a importuna condição de contorno T (1) = 1, mas não a consideramos na prova


indutiva, pois se T (n) > 0 para todo n ≥ 2, fazemos isso primeiro no caso-base da

prova indutiva tomando n0 = 2.

Nestas condições, como existem c≥1 e n ≥ n0 = 2 tais que T (n) ≤ cn log(n),


sob a condição de contorno T (1) = 1


Θ(1), para n = 1
T (n) = j n k
2T + n + Θ[n log(n)], para n≥2
2

que tem como solução T (n) = Θ[n log(n)].


27

3.2.2 Método da Árvore de Recursão

Em uma árvore de recursão, cada nó representa o custo de um único subproblema

em algum lugar no conjunto de invocações de função recursiva. Somamos os custos

em cada nível da árvore para obter um conjunto de custos por nível e depois soma-

mos todos os custos por nível para determinar o custo total de todos os níveis da

recursão.

Por exemplo, vejamos como uma árvore de recursão daria um bom palpite para

a recorrência T (n) = 3T (n/4) + Θ(n2 ). Começamos focalizando a determinação

de um limite superior para a solução. Como sabemos que pisos e tetos normal-

mente não têm importância na solução de recorrências (esse é um exemplo de des-

leixo que podemos tolerar), criamos uma árvore de recursão para a recorrência

T (n) = 3T (n/4) + cn2 , tendo explicitado o coeciente constante implícito c > 0.


A gura a seguir mostra como derivamos a árvore de recursão para T (n) =
3T (n/4) + cn2 . Por conveniência, supomos que n é uma potência exata de 4 (outro

exemplo de desleixo tolerável) de modo que os tamanhos de todos os subproblemas

são inteiros.

Figura 1 Construção de uma árvore de recursão para a recorrência T (n) = 3T (n/4) + cn2 .
28

A parte (a) da gura mostra T (n) como a suposta origem da árvore que, na parte
(b), expandimos para uma árvore equivalente representando a recorrência. O termo

cn2 na raiz representa o custo no nível superior da recursão, e as três subárvores da

raiz, representam os custos incorridos pelos subproblemas de tamanho n/4.


A parte (c) mostra a continuação desse processo em uma etapa posterior repre-

sentada pela expansão de cada nó com custo T (n/4) da parte (b), e o custo para

ambos os três lhos da raiz é c(n/4)2 . Continuamos a expandir cada nó na árvore,

desmembrando-o em suas partes constituintes conforme determinado pela recorrên-

cia.

Visto que os tamanhos dos subproblemas diminuem por um fator de 4 toda vez

que descemos um nível, a certa altura devemos alcançar uma condição de contorno

e como o tamanho do subproblema para um nó na profundidade i é n/4i , o tama-


i
nho do subproblema chega a n = 1 quando n/4 = 1, ou, ao que é equivalente,

quando i = log4 (n). Assim, a árvore tem log4 (n) + 1 níveis nas profundidades

0, 1, 2, . . . , log4 (n).
Em seguida, determinamos o custo em cada nível da árvore. Como cada nível tem

três vezes mais nós que o seu nível anterior, portanto o número de nós na profun-

didade i é 3i , e como os tamanhos dos subproblemas se reduzem por um fator de

4 para cada nível que descemos a partir da raiz, cada nó na profundidade i, para

0, 1, 2, . . . , log4 (n − 1), tem o custo de c(n/4i )2 .


Multiplicando então, vemos que o custo total para todos os nós em cada uma

das profundidades i é 3i c(n/4i )2 = (3/16)i cn2 , de modo que o nível inferior, na


log4 (3)
profundidade log4 (n), tem 3 log4 (n) = n nós e cada um deles contribui com o
 log (3) 
custo T (1), para um custo total de n log4 (3)T (1), que é Θ n 4 , já que supomos

que T (1) é uma constante.

Somamos então os custos em todos os níveis para determinar o custo da árvore

inteira:

 2  log4 (n−1)
3 3 3 h i
T (n) = cn + cn2 + 2 2
cn + · · · + cn2 + Θ nlog4 (3) =
16 16 16
h i log4X(n−1) 
3
i
= Θ nlog4 (3) + cn2 =
i=0
16
h i (3/16 − 1)log4 (n)
= Θ nlog4 (3) + cn2 (E1)
3/16 − 1

Esta última fórmula parece um pouco confusa até percebermos que, mais uma

vez, é possível tirar proveito de um certo desleixo e usar uma série geométrica

decrescente innita como um limite superior. Retrocedendo uma etapa e aplicando

a equação (E1) anterior, temos

h i log4X(n−1) 
3
i h i X ∞ 
3
i
T (n) = Θ nlog4 (3) + cn2 < Θ nlog4 (3) + cn2 =
i=0
16 i=0
16
h i 1 h i 16
= Θ nlog4 (3) + cn2 = Θ nlog4 (3) + cn2 = Θ(n2 )
1 − 3/16 13

Assim, derivamos um palpite de T (n) = O(n2 ) para nossa recorrência original


2
T (n) = 3T (n/4) + Θ(n ).
29

Nesse exemplo, os coecientes de cn2 formam uma série geométrica decrescente

e, pela equação (E1), a soma desses coecientes é limitada na parte superior pela

constante 16/13. Visto que a contribuição da raiz para o custo total é cn2 , a raiz

contribui com uma fração constante do custo total, em outras palavras, o custo da

raiz domina o custo total da árvore.

De fato, se O(n2 ) é realmente um limite superior para a recorrência (como ve-

ricaremos à frente), ele deve ser um limite justo. A primeira chamada recursiva

contribui com o custo Θ(n2), então n2 deve ser um limite inferior para a recorrên-

cia.

Agora podemos usar o método de substituição para vericar que nosso palpite

era correto, isto é, queT (n) = O(n2 ) é um limite superior para a recorrência

T (n) = 3T (n/4) + Θ(n ). Queremos mostrar que T (n) ≤ dn2


2
para alguma cons-

tante d > 0 e usando a mesma constante c > 0 de antes, temos

jnk j n k2  n 2 3 2
T (n) ≤ 3T ( ) + cn2 ≤ 3d + cn2 ≤ 3d + cn2 ≤ dn + cn2 ≤
4 4 4 16
≤ dn2

onde a última etapa é válida desde que d ≥ (16/13)c.


Embora possamos usar o método de substituição para obter uma prova sucinta

de que uma solução para uma recorrência é correta, às vezes, é difícil apresentar um

bom palpite. Traçar uma árvore de recursão é um modo direto para dar um bom

palpite e efetuar uma conclusão pelo método da substituição.

3.2.3 Método Mestre

Para utilizar o método mestre você terá de memorizar três casos, mas poderá

resolver muitas recorrências com grande facilidade, muitas vezes sem se munir de

aparatos algébricos e abstratos. O método fornece uma receita para resolver re-

corrências da forma T (n) = aT (n/b) + f (n), onde a≥1 e b>1 são constantes e

f (n) é uma função assintoticamente positiva.

Esse tipo de recorrência descreve o tempo de execução de um algoritmo que divide

um problema de tamanho n em a subproblemas, cada um de tamanho n/b, onde a


eb são constantes positivas. Os a subproblemas são resolvidos recursivamente, cada
um no tempo T (n/b).

A função f (n) abrange o custo de dividir o problema e combinar os resultados

dos subproblemas. Por questão de correção técnica, na realidade a recorrência não

está bem denida porque n/b poderia não ser um inteiro. Porém, substituir cada
um dos a termos T (n/b) por T (⌊n/b⌋) ou T (⌈n/b⌉) não afetará o comportamento
assintótico da recorrência, como encontra-se provado em Cormen; et al. (seção 4.3).

Portanto, normalmente consideramos conveniente omitir as funções piso e teto

quando escrevemos recorrências de divisão e conquista dessa forma. O método mes-

tre depende do teorema a seguir.

Teorema 3.1 (Teorema mestre) Sejam a ≥ 1 e b > 1 constantes, seja f (n) uma
função e seja T (n) denida no domínio dos números inteiros não negativos pela
recorrência T (n) = aT (n/b) + f (n), onde interpretamos que n/b signica ⌊n/b⌋ ou
30

⌈n/b⌉. Então, T (n) tem os seguintes limites assintóticos:

1 Se f (n) = O nlogb (a−ϵ) para alguma constante ϵ > 0, então T (n) =


 

Θ nlogb (a) .
 

2 Se f (n) = Θ nlogb (a) , então T (n) = Θ nlogb (a) log(n) .


   

3 Se f (n) = Ω n b para alguma constante ϵ > 0, e se af (n/b) ≤ cf (n)


 log (a+ϵ) 

para alguma constante c < 1 e todos os n sucientemente grandes, então


T (n) = Θ[f (n)].

O teorema mestre expõe o signicado de que, em cada um dos três casos, com-

paramos a função f (n) com a função nlogb (a) e, intuitivamente, a maior das duas

funções determina a solução para a recorrência.

Se, como no caso 1, a função nlogb (a) for a maior, então a solução é T (n) =
Θ[nlogb (a) ]. Se, como no caso 3, a função f (n) for a maior, então a solução é T (n) =
Θ[f (n)] e se, como no caso 2, as duas funções tiverem o mesmo tamanho, multiplica-
 log (a) log(n) 
mos por um fator logarítmico e a solução é T (n) = Θ n b = Θ[f (n)lg(n)].
logb (a)
Além disso, no primeiro caso, f (n) não só tem de ser menor que n , mas deve
logb (a)
ser polinomialmente menor, isto é, f (n) deve ser assintoticamente menor que n

por um fator n para alguma constante ϵ > 0. No terceiro caso, f (n) não apenas
logb (a)
deve ser maior que n , ela tem de ser polinomialmente maior e, além disso, sa-

tisfazer à condição de regularidade expressa por af (n/b) ≤ cf (n), e essa condição

é satisfeita pela maioria das funções polinomialmente limitadas que encontraremos.

Observe que os três casos não abrangem todas as possibilidades para f (n). No
logb (a)
entanto, existe uma lacuna entre os casos 1 e 2 quando f (n) é menor que n ,

mas não polinomialmente menor e, de modo semelhante, há uma lacuna entre os

casos 2 e 3 quando f (n) é maior que nlogb (a) , mas não polinomialmente maior.

Se a função f (n) cair em uma dessas lacunas ou se a condição de regularidade no

caso 3 deixar de ser válida, o método mestre não poderá ser usado para resolver a

recorrência.

Exemplo 3.1 Considere T (n) = 9T (n/3) + n. Para essa recorrência, temos a = 9,


b = 3, f (n) = n e, portanto, temos que nlogb (a) = nlog3 (9) = Θ(n2 ). Visto que
f (n) = O nlog3 (9−ϵ) , onde ϵ = 1, podemos aplicar o caso 1 do teorema mestre e
 

concluir que a solução é T (n) = Θ(n2 ).

Exemplo 3.2 Considere T (n) = T (2n/3) + 1. Como a = 1, bh= 3/2, f (n) i =1e
n = n = 1, aplica-se o caso 2 do teorema, pois f (n) = Θ n
log3/2 (1) 0 log3/2 (1)
= Θ(1).
Portanto, a solução para a recorrência dada é T (n) = Θ[log(n)].

Exemplo 3.3 Considere T (n) = 3T (n/4) + n lg(n). Temos que a = 3, b = 4,


f (n) = n lg(n) e nlogb (a) = nlog4 (3) . Como f (n) = Ω nlog4 (3−ϵ) , onde ϵ ≈ 0.2,
 

o caso 3 será aplicado se pudermos mostrar que que as condições de regularida-


des são válidas para f (n) = n log(n). Para n sucientemente grande af (n/b) =
3(n/4) lg(n/4) ≤ (3/4)n lg(n) = cf (n) para c = 3/4 e, consequentemente, de acordo
com o caso 3, a solução para a recorrência é T (n) = O [n lg(n)].
31

Exemplo 3.4 Considere T (n) = T (n/2)+n lg(n). Neste caso, mesmo que na forma
apropriada, a recorrência dada não é da forma apropriada. Veja que, com a =
b = 2, f (n) = n lg(n) e nlogb (a) = nlog2 (2) = n, verica-se que f (n) = n lg(n) é
assintoticamente maior do que nlogb (a) = n, mas não o é polinomialmente. Para
todos as constantes ϵ > 0 a razão

f (n) n lg(n)
= log (a) = n
nlogb (a) n b

é assintoticamente menor do que nϵ e, consequentemente,a recorrência recai na


lacuna entre os casos 2 e 3.

Você também pode gostar