Escolar Documentos
Profissional Documentos
Cultura Documentos
com
Breve Conteúdo
v
Esta página foi intencionalmente deixada em branco
Conteúdo
Prefácio xix
1Introdução 1
1.1 O que é um algoritmo? 3
Exercícios 1.1 7
1.2 Fundamentos da resolução algorítmica de problemas 9
Entendendo o problema 9
Determinando as Capacidades do Dispositivo Computacional 9
Escolhendo entre Técnicas de Projeto de Algoritmo de Resolução de 11
Problemas Exatas e Aproximadas 11
Projetando um Algoritmo e Estruturas de Dados 12
Métodos de Especificação de um Algoritmo 12
Provando a Correção de um Algoritmo Analisando 13
um Algoritmo 14
Codificando um Algoritmo 15
Exercícios 1.2 17
1.3 Tipos de problemas importantes 18
Ordenação 19
Procurando 20
Processamento de strings 20
Problemas gráficos 21
Problemas Combinatórios 21
Problemas Geométricos 22
Problemas Numéricos 22
Exercícios 1.3 23
vii
viii Conteúdo
3.3 Problemas de par mais próximo e casco convexo por força bruta 108
Problema do par mais próximo 108
Problema do casco convexo 109
Exercícios 3.3 113
6Transforme-e-Conquiste 201
6.1 Pré-classificação 202
Exercícios 6.1 205
6.2 Eliminação Gaussiana 208
LUDecomposição 212
Calculando uma Matriz Inversa 214
Calculando um Determinante 215
Exercícios 6.2 216
6.3 Árvores de Busca Balanceadas 218
Árvores AVL 218
2-3 Árvores 223
Exercícios 6.3 225
6.4 Heaps e Heapsort 226
Noção de Heap 227
Heapsort 231
Exercícios 6.4 233
6.5 Regra de Horner e Exponenciação Binária 234
Regra de Horner 234
Exponenciação binária 236
Exercícios 6.5 239
6.6 Redução de Problemas 240
Calculando os caminhos de contagem múltipla 241
menos comuns em um gráfico 242
Redução de Problemas de Otimização 243
Programação Linear 244
Redução a Problemas Gráficos 246
Exercícios 6.6 248
Resumo 250
Epílogo 471
APÊNDICE A
Fórmulas Úteis para a Análise de Algoritmos 475
Propriedades da Logaritmo 475
Combinatória 475
Fórmulas de Soma Importantes 476
Regras de Manipulação de Somas 476
Conteúdo xv
APÊNDICE B
Breve tutorial sobre relações de recorrência 479
Sequências e métodos de relações de recorrência para 479
resolver relações de recorrência Tipos de recorrência 480
comuns na análise de algoritmos 485
Referências 493
Índice 547
Esta página foi intencionalmente deixada em branco
Novidade na terceira edição
xvii
Esta página foi intencionalmente deixada em branco
Prefácio
xix
xx Prefácio
A maioria das aplicações das técnicas de projeto do livro são para problemas
clássicos da ciência da computação. (A única inovação aqui é a inclusão de algum
material sobre algoritmos numéricos, que são abordados dentro da mesma
estrutura geral.) Mas essas técnicas de projeto podem ser consideradas
ferramentas gerais de solução de problemas, cujas aplicações não se limitam à
computação tradicional e problemas matemáticos. Dois fatores tornam este
ponto particularmente importante. Primeiro, cada vez mais aplicativos de
computação vão além do domínio tradicional e há motivos para acreditar que
essa tendência se fortalecerá no futuro. Em segundo lugar, o desenvolvimento
das habilidades de resolução de problemas dos alunos passou a ser reconhecido
como um dos principais objetivos da educação universitária. Entre todos os cursos
de um currículo de ciência da computação,
Não estou propondo que um curso sobre projeto e análise de algoritmos deva se
tornar um curso sobre resolução de problemas gerais. Mas eu acredito que o
Prefácio xxi
oportunidade única fornecida pelo estudo do projeto e análise de algoritmos não deve ser
desperdiçada. Com esse objetivo, o livro inclui aplicativos para quebra-cabeças e jogos
semelhantes a quebra-cabeças. Embora o uso de quebra-cabeças no ensino de algoritmos
certamente não seja uma ideia nova, o livro tenta fazer isso sistematicamente indo muito além de
alguns exemplos padrão.
Meu objetivo era escrever um texto que não banalizasse o assunto, mas que pudesse ser lido pela
maioria dos alunos por conta própria. Aqui estão algumas das coisas feitas em direção a este
objetivo.
Pré-requisitos
O livro assume que o leitor passou por um curso introdutório de programação e
um curso padrão sobre estruturas discretas. Com tal experiência, ele ou ela deve
ser capaz de manusear o material do livro sem maiores dificuldades.
Prefácio xxiii
Usar no currículo
O livro pode servir como um livro-texto para um curso básico sobre projeto e análise de
algoritmos organizados em torno de técnicas de projeto de algoritmos. Pode conter um
pouco mais de material do que pode ser coberto em um curso típico de um semestre. Em
geral, partes dos capítulos 3 a 12 podem ser puladas sem o perigo de tornar as partes
posteriores do livro incompreensíveis para o leitor. Qualquer parte do livro pode ser
designada para auto-estudo. Em particular, as Seções 2.6 e 2.7 sobre análise empírica e
visualização de algoritmos, respectivamente, podem ser atribuídas em conjunto com
projetos.
Aqui está um plano possível para um curso de um semestre; assume um formato de reunião de 40
classes.
1 Introdução 1.1–1.3
2, 3 Enquadramento de análise;O,-,-notações Análise 2.1, 2.2
4 matemática de algoritmos não recursivos Análise 2.3
5, 6 matemática de algoritmos recursivos Algoritmos de 2.4, 2.5 (+ Ap. B)
7 força bruta 3.1, 3.2 (+ 3.3)
8 Pesquisa exaustiva 3.4
9 Pesquisa em profundidade e pesquisa em largura Diminuir por um: 3.5
10, 11 classificação por inserção, classificação topológica 4.1, 4.2
12 Pesquisa binária e outros algoritmos de diminuição 4.4
por fator constante
13 Algoritmos de diminuição de tamanho variável 4.5
14, 15 Divisão e conquista: mergesort, quicksort Outros 5.1–5.2
16 exemplos de divisão e conquista 5.3 ou 5.4 ou 5.5
17–19 Simplificação de instâncias: pré-ordenação, eliminação 6.1–6.3
gaussiana, árvores de busca balanceadas
20 Mudança de representação: heaps e heapsort ou 6.4 ou 6.5
regra de Horner e exponenciação binária
21 redução de problemas 6.6
22–24 Compensações de espaço-tempo: correspondência de strings, hashing, 7.2–7.4
Btrees
Agradecimentos
Gostaria de expressar minha gratidão aos revisores e aos muitos leitores que
compartilharam comigo suas opiniões sobre as duas primeiras edições do
livro e sugeriram melhorias e correções. A terceira edição certamente se
beneficiou das críticas de Andrew Harrington (Loyola University Chicago),
David Levine (Saint Bonaventure University), Stefano Lombardi (UC Riverside),
Daniel McKee (Mansfield University), Susan Brilliant (Virginia Commonwealth
University), David Akers (Universidade de Puget Sound) e dois revisores
anônimos.
Meus agradecimentos vão para todas as pessoas da Pearson e seus associados
que trabalharam em meu livro. Sou especialmente grato ao meu editor, Matt
Goldstein; a assistente editorial, Chelsea Bell; o gerente de marketing, Yez Alayan; e a
supervisora de produção, Kayla Smith-Tarbox. Também sou grato a Richard Camp
pela edição do livro, Paul Anagnostopoulos da Windfall Software e Jacqui Scarlott pelo
gerenciamento do projeto e composição tipográfica, e MaryEllen Oliver pela revisão do
livro.
Finalmente, estou em dívida com dois membros da minha família. Viver com
um cônjuge escrevendo um livro é provavelmente mais difícil do que escrever.
Minha esposa, Maria, viveu vários anos disso, ajudando-me como podia. E ela
ajudou: mais de 400 figuras do livro e do Manual do Instrutor foram criadas por
ela. Minha filha Miriam tem sido minha guru da prosa inglesa por muitos anos. Ela
leu grandes porções do livro e foi fundamental para encontrar as epígrafes dos
capítulos.
Anany Levitin
anany.levitin@villanova.edu
junho de 2011
Esta página foi intencionalmente deixada em branco
1
Introdução
Mas mesmo que você não seja aluno de um programa relacionado à computação,
existem razões convincentes para estudar algoritmos. Para ser franco, os programas de
computador não existiriam sem algoritmos. E com os aplicativos de computador se
tornando indispensáveis em quase todos os aspectos de nossas vidas profissionais e
pessoais, estudar algoritmos se torna uma necessidade para mais e mais pessoas.
Outra razão para estudar algoritmos é sua utilidade no desenvolvimento de habilidades
analíticas. Afinal, os algoritmos podem ser vistos como tipos especiais de soluções para problemas
– não apenas respostas, mas procedimentos definidos com precisão para obter respostas.
Consequentemente, técnicas específicas de design de algoritmos podem ser interpretadas como
estratégias de resolução de problemas que podem ser úteis independentemente de um
computador estar envolvido. Claro, a precisão inerentemente imposta pelo pensamento
algorítmico limita os tipos de problemas que podem ser resolvidos com um algoritmo. Você não
encontrará, por exemplo, um algoritmo para viver uma vida feliz ou se tornar rico e famoso. Sobre
1
2 Introdução
por outro lado, essa precisão exigida tem uma importante vantagem educacional.
Donald Knuth, um dos mais proeminentes cientistas da computação na história da
algorítmica, disse o seguinte:
Uma pessoa bem treinada em informática sabe como lidar com algoritmos: como
construí-los, manipulá-los, entendê-los, analisá-los. Esse conhecimento é uma
preparação para muito mais do que escrever bons programas de computador; é
uma ferramenta mental de propósito geral que será uma ajuda definitiva para a
compreensão de outros assuntos, sejam eles química, lingüística ou música, etc. A
razão para isso pode ser entendida da seguinte maneira: que uma pessoa
realmente não entende algo até depois de ensiná-lo a outra pessoa. Na verdade,
uma pessoa nãorealmente entender algo até depois de ensiná-lo a um
computador, ou seja, expressando-o como um algoritmo . . . Uma tentativa de
formalizar as coisas como algoritmos leva a uma compreensão muito mais
profunda do que se simplesmente tentarmos compreender as coisas da maneira
tradicional. [Knu96, pág. 9]
Adotamos a noção de algoritmo na Seção 1.1. Como exemplos, usamos três algoritmos
para o mesmo problema: calcular o máximo divisor comum. Existem várias razões para esta
escolha. Em primeiro lugar, trata de um problema familiar a todos desde os tempos de
escola secundária. Em segundo lugar, destaca o ponto importante de que o mesmo
problema geralmente pode ser resolvido por vários algoritmos. Normalmente, esses
algoritmos diferem em sua ideia, nível de sofisticação e eficiência. Em terceiro lugar, um
desses algoritmos merece ser apresentado primeiro, tanto por causa de sua idade —
apareceu no famoso tratado de Euclides há mais de dois mil anos — quanto por seu poder e
importância duradouros. Finalmente, a investigação desses três algoritmos leva a algumas
observações gerais sobre várias propriedades importantes dos algoritmos em geral.
Para uma exposição mais detalhada, há uma abundância de bons livros sobre o assunto, a maioria
deles adaptados a uma linguagem de programação específica.
Esta definição pode ser ilustrada por um diagrama simples (Figura 1.1).
A referência a “instruções” na definição implica que existe algo ou alguém
capaz de entender e seguir as instruções dadas. Chamamos isso de “computador”,
lembrando que antes da invenção do computador eletrônico, a palavra
“computador” significava um ser humano envolvido na realização de cálculos
numéricos. Hoje em dia, é claro, “computadores” são aqueles dispositivos
eletrônicos onipresentes que se tornaram indispensáveis em quase tudo o que
fazemos. Observe, no entanto, que embora a maioria dos algoritmos seja de fato
destinada a uma eventual implementação computacional, a noção de algoritmo
não depende de tal suposição.
Como exemplos que ilustram a noção do algoritmo, consideramos nesta seção
três métodos para resolver o mesmo problema: calcular o máximo divisor comum de
dois números inteiros. Esses exemplos nos ajudarão a ilustrar vários pontos
importantes:
O requisito de não ambigüidade para cada etapa de um algoritmo não pode ser
comprometido.
O intervalo de entradas para o qual um algoritmo funciona deve ser especificado com
cuidado. O mesmo algoritmo pode ser representado de várias maneiras diferentes.
Podem existir vários algoritmos para resolver o mesmo problema.
problema
algoritmo
Algoritmos para o mesmo problema podem ser baseados em ideias muito diferentes e podem
resolver o problema com velocidades dramaticamente diferentes.
Lembre-se de que o máximo divisor comum de dois inteiros não negativos, não ambos
iguais a zeromen, denotado gcd(m,n), é definido como o maior inteiro que divide ambosmen
uniformemente, ou seja, com resto zero. Euclides de Alexandria (século IIIbc)delineou um
algoritmo para resolver este problema em um dos volumes de seuelementosmais famoso
por sua exposição sistemática da geometria. Em termos modernos,algoritmo de euclides
baseia-se na aplicação repetida da igualdade
gcd(m,n)=gcd(n, mmodn),
gcd(60,24)=gcd(24,12)=gcd(12,0)=12.
(Se você não estiver impressionado com este algoritmo, tente encontrar o máximo divisor comum
de números maiores, como os do Problema 6 nos exercícios desta seção.)
Aqui está uma descrição mais estruturada deste algoritmo:
ALGORITMOEuclides(m, n)
//Calcula o gcd(m,n)pelo algoritmo de Euclides
//Entrada: Dois inteiros não negativos, não ambos iguais a zerom
en //Saída: Máximo divisor comum demen enquanton-=0fazer
r←mmodn
m←n
n←r
retornarm
Assim como em muitos outros problemas, existem vários algoritmos para calcular
o máximo divisor comum. Vejamos os outros dois métodos para esse problema. A
primeira é simplesmente baseada na definição do máximo divisor comum de men
como o maior inteiro que divide os dois números igualmente. Obviamente, tal divisor
comum não pode ser maior que o menor desses números, que denotaremos port=
min{m, n}. Então podemos começar verificando setdivide os dois men: se for,té a
resposta; se não, simplesmente diminuímostpor 1 e tente novamente. (Como sabemos
que o processo acabará parando?) Por exemplo, para os números 60 e 24, o algoritmo
tentará primeiro 24, depois 23 e assim por diante, até chegar a 12, onde para.
Note que ao contrário do algoritmo de Euclides, este algoritmo, na forma apresentada, não
funciona corretamente quando um de seus números de entrada é zero. Este exemplo ilustra por
que é tão importante especificar o conjunto de entradas de um algoritmo de forma explícita e
cuidadosa.
O terceiro procedimento para encontrar o máximo divisor comum deve ser familiar
para você desde o ensino médio.
60 = 2.2.3.5 24 =
2.2.2.3 gcc(60,24)
=2.2.3 = 12.
A nostalgia dos dias em que aprendemos esse método não deve nos impedir de notar
que o último procedimento é muito mais complexo e lento que o algoritmo de Euclides.
(Discutiremos métodos para encontrar e comparar tempos de execução de algoritmos no
próximo capítulo.) Além da eficiência inferior, o procedimento do ensino médio não se
qualifica, na forma apresentada, como um algoritmo legítimo. Por que? Como os passos da
fatoração primária não são definidos de forma inequívoca: eles
6 Introdução
requerem uma lista de números primos, e eu suspeito fortemente que seu professor de
matemática do ensino médio não explicou como obter tal lista. Esta não é uma questão de
picuinhas desnecessárias. A menos que esse problema seja resolvido, não podemos, digamos,
escrever um programa implementando esse procedimento. Aliás, a Etapa 3 também não está
definida com clareza suficiente. Entretanto, sua ambigüidade é muito mais fácil de corrigir do que
a das etapas de fatoração. Como você encontraria elementos comuns em duas listas classificadas?
Então, vamos introduzir um algoritmo simples para gerar números primos consecutivos
que não excedam nenhum inteiro dadon >1. Provavelmente foi inventado na Grécia antiga e
é conhecido como openeira de Eratóstenes(ca. 200aC).O algoritmo começa inicializando
uma lista de candidatos principais com inteiros consecutivos de 2 an. Então, em sua primeira
iteração, o algoritmo elimina da lista todos os múltiplos de 2, ou seja, 4, 6 e assim por diante.
Em seguida, passa para o próximo item da lista, que é o 3, e elimina seus múltiplos. (Nesta
versão direta, há uma sobrecarga porque alguns números, como 6, são eliminados mais de
uma vez.) Não é necessário passar para o número 4: como o próprio 4 e todos os seus
múltiplos também são múltiplos de 2, eles já foram eliminados em passagem anterior. O
próximo número restante na lista, que é usado na terceira passagem, é 5. O algoritmo
continua dessa maneira até que nenhum outro número possa ser eliminado da lista. Os
inteiros restantes da lista são os primos necessários.
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 2
3 5 7 9 11 13 15 17 19 21 23 25
2 3 5 7 11 13 17 19 23 25
2 3 5 7 11 13 17 19 23
Para este exemplo, não são necessárias mais passagens porque eliminariam
números já eliminados em iterações anteriores do algoritmo. Os números
restantes na lista são os primos consecutivos menores ou iguais a 25.
Qual é o maior númeropcujos múltiplos ainda podem permanecer na lista para tornar
necessárias outras iterações do algoritmo? Antes de responder a esta pergunta, vamos
primeiro notar que sepé um número cujos múltiplos estão sendo eliminados na passagem
atual, então o primeiro múltiplo que devemos considerar épág. pporque todos os seus
múltiplos menores 2p, . . . , (p−1)pforam eliminados em passagens anteriores pela lista. Esta
observação ajuda a evitar eliminar o mesmo número mais do que
uma vez. Obviamente,pág. pdeve⌊n√ ot b⌋e maior quen, e portantopnão pode exceder
√
narredondado para baixo (indicado nusando o chamadoflfunção de piso). nós como⌊soma
√⌋e
no seguinte pseudocódigo que existe uma função disponível para computação n;
alternativamente, poderíamos verificar a desigualdadepág. p≤ncomo a condição de
continuação do loop lá.
ALGORITMOPeneira(n)
//Implementa o crivo de Eratóstenes //
Entrada: Um inteiro positivon >1
//Saída: Arrayeude todos os números primos menores ou iguais an
1.1O que é um algoritmo? 7
parap←2para⌊nfazer p]← p
√⌋A[
parap←2para nfazer //ver nota antes do pseudocódigo
seA[p] -= 0 //pnão foi eliminado em passes anteriores
j←p∗p
enquantoj≤nfazer
A[j]←0 //marca o elemento como eliminado
j←j+p
//copia os elementos restantes deApara matrizeudos primos eu
←0
parap←2paranfazer
seA[p] -= 0
eu[eu]←A[p]
eu←eu+1
retornareu
Exercícios 1.1
1.Faça alguma pesquisa sobre al-Khorezmi (também al-Khwarizmi), o homem de cujo
nome a palavra “algoritmo” é derivada. Em particular, você deve aprender o que as
origens das palavras “algoritmo” e “álgebra” têm em comum.
2.Dado que o propósito oficial do sistema de patentes dos EUA é a promoção das
“artes úteis”, você acha que os algoritmos são patenteáveis neste país? Eles
deveriam ser?
3. a.Anote as instruções de direção para ir de sua escola até sua casa com a
precisão exigida pela descrição de um algoritmo.
b.Anote uma receita para cozinhar seu prato favorito com a precisão
exigida por um algoritmo.
⌊√⌋
4.Projetar um algoritmo para computação npara qualquer inteiro positivon. Além do mais
atribuição e comparação, seu algoritmo só pode usar as quatro operações
aritméticas básicas.
8 Introdução
8.O que o algoritmo de Euclides faz para um par de inteiros em que o primeiro é
menor que o segundo? Qual é o número máximo de vezes que isso pode
acontecer durante a execução do algoritmo em tal entrada?
10. a.O algoritmo de Euclides, conforme apresentado no tratado de Euclides, usa subtrações em
vez de divisões inteiras. Escreva pseudocódigo para esta versão do algoritmo de Euclides.
Essas soluções não são respostas, mas instruções específicas para obter respostas. É essa
ênfase em procedimentos construtivos precisamente definidos que torna a ciência da
computação distinta de outras disciplinas. Em particular, isso a distingue da matemática
teórica, cujos praticantes geralmente se satisfazem apenas em provar a existência de uma
solução para um problema e, possivelmente, investigar as propriedades da solução.
Agora listamos e discutimos brevemente uma sequência de etapas que uma pessoa normalmente
percorre ao projetar e analisar um algoritmo (Figura 1.2).
Entendendo o problema
De uma perspectiva prática, a primeira coisa que você precisa fazer antes de projetar
um algoritmo é entender completamente o problema dado. Leia atentamente a
descrição do problema e faça perguntas se tiver alguma dúvida sobre o problema, faça
alguns pequenos exemplos à mão, pense em casos especiais e pergunte novamente,
se necessário.
Existem alguns tipos de problemas que surgem com bastante frequência em aplicativos
de computação. Nós os revisamos na próxima seção. Se o problema em questão for um
deles, você poderá usar um algoritmo conhecido para resolvê-lo. Claro, ajuda entender
como tal algoritmo funciona e conhecer seus pontos fortes e fracos, especialmente se você
tiver que escolher entre vários algoritmos disponíveis. Mas muitas vezes você não
encontrará um algoritmo prontamente disponível e terá que criar o seu próprio. A sequência
de etapas descritas nesta seção deve ajudá-lo nessa tarefa empolgante, mas nem sempre
fácil.
Uma entrada para um algoritmo especifica uminstânciado problema que o algoritmo
resolve. É muito importante especificar exatamente o conjunto de instâncias que o
algoritmo precisa manipular. (Como exemplo, relembre as variações no conjunto de
instâncias para os algoritmos dos três maiores divisores comuns discutidos na seção
anterior.) Se você não fizer isso, seu algoritmo pode funcionar corretamente para a maioria
das entradas, mas travar em alguns " valor. Lembre-se de que um algoritmo correto não é
aquele que funciona na maioria das vezes, mas aquele que funciona corretamente para
todosentradas legítimas.
Não economize nesta primeira etapa do processo algorítmico de resolução de problemas;
caso contrário, você correrá o risco de retrabalho desnecessário.
Entenda o problema
Decida sobre:
meios computacionais,
resolução exata vs. aproximada,
técnica de design de algoritmo
Desenhe um algoritmo
Provar correção
Analise o algoritmo
Codifique o algoritmo
Verifique o sumário deste livro e você verá que a maioria de seus capítulos é
dedicada a técnicas de projeto individuais. Eles destilam algumas ideias-chave que
provaram ser úteis no projeto de algoritmos. Aprender essas técnicas é de
extrema importância pelas seguintes razões.
Primeiro, eles fornecem orientação para projetar algoritmos para novos problemas, ou seja,
problemas para os quais não há algoritmo satisfatório conhecido. Portanto - para usar a
linguagem de um provérbio famoso - aprender tais técnicas é semelhante a aprender
12 Introdução
pescar em vez de receber um peixe pescado por outra pessoa. Não é verdade, claro,
que cada uma dessas técnicas gerais será necessariamente aplicável a todos os
problemas que você possa encontrar. Mas juntos, eles constituem uma poderosa
coleção de ferramentas que você achará bastante úteis em seus estudos e trabalho.
Em segundo lugar, os algoritmos são a pedra angular da ciência da computação. Toda ciência
está interessada em classificar seu assunto principal, e a ciência da computação não é exceção. As
técnicas de design de algoritmos permitem classificar algoritmos de acordo com uma ideia de
design subjacente; portanto, eles podem servir como uma maneira natural de categorizar e
estudar algoritmos.
Analisando um Algoritmo
Geralmente queremos que nossos algoritmos possuam várias qualidades. Depois da correção, de
longe o mais importante éeficiência. Na verdade, existem dois tipos de eficiência do algoritmo:
Eficiência de tempo, indicando a velocidade com que o algoritmo é executado eeficiência de
espaço, indicando quanta memória extra ele usa. Uma estrutura geral e técnicas específicas para
analisar a eficiência de um algoritmo aparecem no Capítulo 2.
Outra característica desejável de um algoritmo ésimplicidade. Ao contrário da eficiência, que
pode ser definida com precisão e investigada com rigor matemático, a simplicidade, assim como a
beleza, depende em grande parte dos olhos de quem vê. Por exemplo, a maioria das pessoas
concordaria que o algoritmo de Euclides é mais simples do que o procedimento do ensino médio
para calcular mdc(m,n), mas não está claro se o algoritmo de Euclides é mais simples do que o
algoritmo de verificação de inteiros consecutivos. Ainda assim, a simplicidade é uma característica
importante do algoritmo a ser buscada. Por que? Porque algoritmos mais simples são mais fáceis
de entender e mais fáceis de programar; consequentemente, os programas resultantes
geralmente contêm menos bugs. Há também o inegável apelo estético da simplicidade. Às vezes,
algoritmos mais simples também são mais eficientes do que alternativas mais complicadas.
Infelizmente, nem sempre é verdade, caso em que um compromisso criterioso precisa ser feito.
Codificando um Algoritmo
A maioria dos algoritmos está destinada a ser finalmente implementada como programas
de computador. Programar um algoritmo apresenta tanto um perigo quanto uma
oportunidade. O perigo reside na possibilidade de fazer a transição de um algoritmo para
um programa de forma incorreta ou muito ineficiente. Alguns cientistas da computação
influentes acreditam firmemente que, a menos que a correção de um programa de
computador seja comprovada com total rigor matemático, o programa não pode ser
considerado correto. Eles desenvolveram técnicas especiais para fazer tais provas (ver
[Gri81]), mas o poder dessas técnicas de verificação formal é limitado até agora a programas
muito pequenos.
Na prática, a validade dos programas ainda é estabelecida por meio de testes.
Testar programas de computador é uma arte e não uma ciência, mas isso não
significa que não haja nada a aprender. Procure livros dedicados a testes e
depuração; ainda mais importante, teste e depure seu programa minuciosamente
sempre que implementar um algoritmo.
Observe também que, ao longo do livro, presumimos que as entradas dos algoritmos
pertencem aos conjuntos especificados e, portanto, não requerem verificação. Ao implementar
algoritmos como programas a serem usados em aplicativos reais, você deve fornecer tais
verificações.
Claro, implementar um algoritmo corretamente é necessário, mas não suficiente: você
não gostaria de diminuir o poder do seu algoritmo por uma implementação ineficiente. Os
compiladores modernos fornecem uma certa rede de segurança a esse respeito,
especialmente quando são usados no modo de otimização de código. Ainda assim, você
precisa estar ciente de truques padrão como calcular a invariante de um loop (uma
expressão que não altera seu valor) fora do loop, coletar subexpressões comuns, substituir
operações caras por baratas e assim por diante. (Consulte [Ker99] e [Ben00] para uma boa
discussão sobre ajuste de código e outras questões relacionadas à programação de
algoritmos.) Normalmente, essas melhorias podem acelerar um programa apenas por um
fator constante, enquanto um algoritmo melhor pode fazer diferença na execução tempo
por ordens de grandeza. Mas uma vez que um algoritmo é selecionado,
1. Encontrei esse apelo à simplicidade de design em uma coleção de ensaios de Jon Bentley [Ben00]; os ensaios lidam
com uma variedade de questões no projeto e implementação de algoritmos e são justificadamente intitulados
pérolas de programação. Recomendo vivamente os escritos de Jon Bentley e Antoine de Saint-Exupéry.
16 Introdução
Mesmo que você tenha tido a sorte de obter uma ideia algorítmica que pareça perfeita,
você ainda deve tentar ver se ela pode ser melhorada.
Na verdade, esta é uma boa notícia, pois torna o resultado final muito mais
agradável. (Sim, pensei em nomear este livroA alegria dos algoritmos.) Por outro
lado, como saber quando parar? No mundo real, na maioria das vezes, o
cronograma de um projeto ou a impaciência de seu chefe o impedirão. E assim
deveria ser: a perfeição é cara e, de fato, nem sempre necessária. Projetar um
algoritmo é uma atividade de engenharia que exige compromissos entre objetivos
concorrentes sob as restrições dos recursos disponíveis, sendo o tempo do
designer um dos recursos.
No mundo acadêmico, a questão leva a uma investigação interessante, mas geralmente
difícil, da capacidade de um algoritmo.otimização. Na verdade, esta questão não é sobre a
eficiência de um algoritmo, mas sobre a complexidade do problema que ele resolve: Qual é o
mínimo de esforçoqualqueralgoritmo precisará se esforçar para resolver o problema? Para alguns
problemas, a resposta a esta pergunta é conhecida. Por exemplo, qualquer algoritmo que
classifique uma matriz comparando valores de seus elementos precisa de cerca de nregistro2n
comparações para algumas matrizes de tamanhon(consulte a Seção 11.2). Mas para muitos
problemas aparentemente fáceis, como a multiplicação de números inteiros, os cientistas da
computação ainda não têm uma resposta final.
Outra questão importante da resolução de problemas algorítmicos é a questão de
saber se todo problema pode ou não ser resolvido por um algoritmo. Não estamos falando
aqui de problemas que não têm solução, como encontrar raízes reais de uma equação do
segundo grau com discriminante negativo. Para tais casos, uma saída indicando que o
problema não tem solução é tudo o que podemos e devemos esperar de um algoritmo.
Tampouco estamos falando de problemas formulados de forma ambígua. Mesmo alguns
problemas inequívocos que devem ter uma resposta simples sim ou não são “indecidíveis”,
isto é, insolúveis por qualquer algoritmo. Um exemplo importante desse problema aparece
na Seção 11.3. Felizmente, a grande maioria dos problemas na computação práticapodeser
resolvido por um algoritmo.
Antes de deixar esta seção, certifique-se de que você não tenha a ideia errada
— possivelmente causada pela natureza um tanto mecânica do diagrama da
Figura 1.2 — de que projetar um algoritmo é uma atividade monótona. Nada mais
longe da verdade: inventar (ou descobrir?) algoritmos é um processo muito
criativo e recompensador. Este livro foi concebido para convencê-lo de que este é
o caso.
1.2Fundamentos da resolução algorítmica de problemas 17
Exercícios 1.2
1.quebra-cabeça do velho mundoUm camponês se encontra na margem de um rio
com um lobo, uma cabra e um repolho. Ele precisa transportar os três para o
outro lado do rio em seu barco. No entanto, o barco tem espaço apenas para o
próprio camponês e mais um item (seja o lobo, a cabra ou o repolho). Na sua
ausência, o lobo comia a cabra e a cabra comia a couve. Resolva esse problema
para o camponês ou prove que não tem solução. (Nota: o camponês é
vegetariano, mas não gosta de repolho e, portanto, não pode comer nem a cabra
nem o repolho para ajudá-lo a resolver o problema. E nem é preciso dizer que o
lobo é uma espécie protegida.)
2.quebra-cabeça do novo mundoHá quatro pessoas que querem atravessar uma ponte
frágil; todos começam do mesmo lado. Você tem 17 minutos para levá-los para o outro
lado. É noite e eles têm uma lanterna. No máximo duas pessoas podem atravessar a
ponte ao mesmo tempo. Qualquer pessoa que atravesse, seja uma ou duas pessoas,
deve ter a lanterna consigo. A lanterna deve ser percorrida para frente e para trás; não
pode ser lançado, por exemplo. A pessoa 1 leva 1 minuto para atravessar a ponte, a
pessoa 2 leva 2 minutos, a pessoa 3 leva 5 minutos e a pessoa 4 leva 10 minutos. Um
par deve caminhar junto no ritmo da pessoa mais lenta. (Observação: de acordo com
um boato na Internet, entrevistadores de uma conhecida empresa de software
localizada perto de Seattle deram esse problema aos entrevistados.)
3.Qual das seguintes fórmulas pode ser considerada um algoritmo para calcular a área de
um triângulo cujos comprimentos laterais são dados números positivosa,b,
ec ?
√
a.S=p(p−a)(p−b)(p−c),ondep=(a+b+c)/2
b.S=1 2bcpecadoA,ondeAé o ângulo entre os ladosbec
c.S=1 2aha,ondehaé a altura até a basea
4.Escreva pseudocódigo para um algoritmo para encontrar raízes reais da equação
machado2+ bx+c=0 para coeficientes reais arbitráriosum, b,ec.(Você pode assumir a
disponibilidade da função de raiz quadradaquadrado (x).)
5.Descrever o algoritmo padrão para encontrar a representação binária de um
inteiro decimal positivo
a.Em inglês.
b.em pseudocódigo.
6.Descreva o algoritmo usado por seu caixa eletrônico favorito para dispensar
dinheiro. (Você pode fornecer sua descrição em inglês ou em pseudocódigo, o
que achar mais conveniente.)
7. a.Pode o problema de calcular o númeroπser resolvido exatamente?
b.Quantas instâncias esse problema tem?
c.Procure um algoritmo para esse problema na Internet.
18 Introdução
ALGORITMOMinDistance(A[0..n−1])
//Entrada: ArrayA[0..n−1] de números
//Saída: Distância mínima entre dois de seus elementos
dmin← ∞
paraeu←0paran−1fazer
paraj←0paran−1fazer
seeu-=je|A[eu] −A[j]|< dmin
dmin← |A[eu] −A[j]|
retornardmin
Procurando
Processamento de strings
problemas gráficos
problemas combinatórios
problemas geométricos
problemas numéricos
1.3Tipos de problemas importantes 19
Esses problemas são usados nos capítulos subseqüentes do livro para ilustrar
diferentes técnicas de projeto de algoritmos e métodos de análise de algoritmos.
Ordenação
Por que queremos uma lista ordenada? Para começar, uma lista classificada pode ser uma saída
necessária de uma tarefa, como classificar resultados de pesquisa na Internet ou classificar alunos por
suas pontuações GPA. Além disso, a classificação torna mais fácil responder a muitas perguntas sobre a
lista. O mais importante deles é a busca: é por isso que os dicionários, listas telefônicas, listas de classe e
assim por diante são classificados. Você verá outros exemplos da utilidade da pré-ordenação de lista na
Seção 6.1. Da mesma forma, a classificação é usada como uma etapa auxiliar em vários algoritmos
importantes em outras áreas, por exemplo, algoritmos geométricos e compressão de dados. A
abordagem gananciosa - uma importante técnica de projeto de algoritmo discutida posteriormente neste
livro - requer uma entrada classificada.
Até agora, os cientistas da computação descobriram dezenas de diferentes algoritmos de
classificação. Na verdade, inventar um novo algoritmo de classificação foi comparado a projetar a
proverbial ratoeira. E fico feliz em informar que a busca por uma ratoeira de melhor classificação
continua. Essa perseverança é admirável em vista dos seguintes fatos. Por um lado, existem
alguns bons algoritmos de classificação que classificam uma matriz arbitrária de tamanhonusando
sobrenregistro2ncomparações. Por outro lado, nenhum algoritmo que classifique por
comparações de chaves (ao contrário de, digamos, comparar pequenas partes de chaves) pode se
sair substancialmente melhor do que isso.
Há uma razão para esse embaraço de riquezas algorítmicas na terra da classificação. Embora alguns
algoritmos sejam de fato melhores que outros, não existe um algoritmo que seja a melhor solução em
todas as situações. Alguns dos algoritmos são simples, mas relativamente lentos, enquanto outros são
mais rápidos, mas mais complexos; alguns funcionam melhor em entradas ordenadas aleatoriamente,
enquanto outros funcionam melhor em listas quase ordenadas; alguns são adequados apenas para listas
residentes na memória rápida, enquanto outros podem ser adaptados para classificar arquivos grandes
armazenados em um disco; e assim por diante.
Duas propriedades dos algoritmos de ordenação merecem menção especial. Um algoritmo
de ordenação é chamadoestábulose ele preserva a ordem relativa de quaisquer dois elementos
iguais em sua entrada. Em outras palavras, se uma lista de entrada contiver dois elementos iguais
em posições euejondeeu < j,então, na lista classificada, eles devem estar em posiçõeseu′ej′,
20 Introdução
respectivamente, tal queeu′<j′.Essa propriedade pode ser desejável se, por exemplo, tivermos uma
lista de alunos classificados alfabeticamente e quisermos classificá-la de acordo com o GPA do
aluno: um algoritmo estável produzirá uma lista na qual os alunos com o mesmo GPA ainda serão
classificados alfabeticamente. De um modo geral, os algoritmos que podem trocar chaves
localizadas distantes não são estáveis, mas geralmente funcionam mais rápido; você verá como
esse comentário geral se aplica a algoritmos de classificação importantes mais adiante neste livro.
Procurando
Oprocurando problematrata de encontrar um determinado valor, chamado dechave de
pesquisa, em um determinado conjunto (ou um multiconjunto, que permite que vários
elementos tenham o mesmo valor). Existem muitos algoritmos de pesquisa para escolher.
Eles variam desde a busca sequencial direta até uma busca binária espetacularmente
eficiente, mas limitada, e algoritmos baseados na representação do conjunto subjacente de
uma forma diferente, mais propícia à busca. Os últimos algoritmos são de particular
importância para aplicações do mundo real porque são indispensáveis para armazenar e
recuperar informações de grandes bancos de dados.
Também para a pesquisa, não existe um algoritmo único que se adapte melhor a todas as
situações. Alguns algoritmos funcionam mais rápido que outros, mas requerem mais memória;
alguns são muito rápidos, mas aplicáveis apenas a arrays classificados; e assim por diante. Ao
contrário dos algoritmos de classificação, não há problema de estabilidade, mas surgem
problemas diferentes. Especificamente, em aplicativos em que os dados subjacentes podem
mudar frequentemente em relação ao número de pesquisas, a pesquisa deve ser considerada em
conjunto com duas outras operações: uma adição e uma exclusão do conjunto de dados de um
item. Em tais situações, estruturas de dados e algoritmos devem ser escolhidos para encontrar um
equilíbrio entre os requisitos de cada operação. Além disso, organizar conjuntos de dados muito
grandes para pesquisas eficientes apresenta desafios especiais com implicações importantes para
aplicativos do mundo real.
Processamento de strings
Nas últimas décadas, a rápida proliferação de aplicativos que lidam com dados não numéricos
intensificou o interesse de pesquisadores e profissionais de computação em algoritmos de
manipulação de strings. Acordaé uma sequência de caracteres de um alfabeto. Strings de
interesse particular são strings de texto, que compreendem letras, números e caracteres
especiais; cadeias de bits, que compreendem zeros e uns; e sequências de genes, que podem ser
modeladas por cadeias de caracteres do alfabeto de quatro caracteres {A,
C, G, T}.Deve-se ressaltar, no entanto, que os algoritmos de processamento de strings têm sido
importantes para a ciência da computação há muito tempo em conjunto com linguagens de
computador e problemas de compilação.
1.3Tipos de problemas importantes 21
Problemas gráficos
Uma das áreas mais antigas e interessantes da algorítmica são os algoritmos de grafos.
Informalmente, umgráficopode ser pensado como uma coleção de pontos chamados vértices,
alguns dos quais são conectados por segmentos de linha chamados arestas. (Uma definição mais
formal é dada na próxima seção.) Os gráficos são um assunto interessante para estudar, tanto por
razões teóricas quanto práticas. Os gráficos podem ser usados para modelar uma ampla
variedade de aplicações, incluindo transporte, comunicação, redes sociais e econômicas,
programação de projetos e jogos. Estudar diferentes aspectos técnicos e sociais da Internet em
particular é uma das áreas ativas de pesquisa atual envolvendo cientistas da computação,
economistas e cientistas sociais (ver, por exemplo, [Eas10]).
Algoritmos básicos de grafos incluem algoritmos de travessia de grafos (como alguém pode
alcançar todos os pontos em uma rede?), algoritmos de caminho mais curto (qual é a melhor rota
entre duas cidades?), e classificação topológica para grafos com arestas direcionadas (é um
conjunto de cursos com seus pré-requisitos consistentes ou autocontraditórios?). Felizmente,
esses algoritmos podem ser considerados ilustrações de técnicas gerais de projeto; portanto, você
os encontrará nos capítulos correspondentes do livro.
Alguns problemas de grafos são computacionalmente muito difíceis; os exemplos mais
conhecidos são o problema do caixeiro viajante e o problema da coloração de grafos. O
problema do caixeiro viajante (TSP)é o problema de encontrar o caminho mais curto
atravésncities que visita todas as cidades exatamente uma vez. Além de aplicações óbvias
envolvendo planejamento de rotas, ele surge em aplicações modernas como fabricação de
placas de circuito e chip VLSI, cristalografia de raios-X e engenharia genética. Oproblema de
coloração de gráficoprocura atribuir o menor número de cores aos vértices de um grafo,
de modo que não haja dois vértices adjacentes com a mesma cor. Este problema surge em
diversas aplicações, como escalonamento de eventos: se os eventos são representados por
vértices conectados por uma aresta se e somente se os eventos correspondentes não
podem ser escalonados ao mesmo tempo, uma solução para o problema de coloração de
grafos produz um cronograma ideal.
Problemas Combinatórios
De uma perspectiva mais abstrata, o problema do caixeiro viajante e o problema da coloração de
grafos são exemplos deproblemas combinatórios. Esses são problemas que pedem, explícita ou
implicitamente, para encontrar um objeto combinatório – como uma permutação, uma
combinação ou um subconjunto – que satisfaça certas restrições. Também pode ser necessário
que um objeto combinatório desejado tenha alguma propriedade adicional, como um valor
máximo ou um custo mínimo.
22 Introdução
Alguns problemas combinatórios podem ser resolvidos por algoritmos eficientes, mas devem
ser considerados felizes exceções à regra. O problema do caminho mais curto mencionado
anteriormente está entre essas exceções.
Problemas Geométricos
Algoritmos geométricoslidar com objetos geométricos, como pontos, linhas e polígonos.
Os antigos gregos estavam muito interessados em desenvolver procedimentos (eles não os
chamavam de algoritmos, é claro) para resolver uma variedade de problemas geométricos,
incluindo problemas de construção de formas geométricas simples – triângulos, círculos e
assim por diante – com uma régua não marcada e um compasso. Então, por cerca de 2.000
anos, o intenso interesse em algoritmos geométricos desapareceu, para ser ressuscitado na
era dos computadores – sem réguas e compassos, apenas bits, bytes e a boa e velha
engenhosidade humana. Claro, hoje as pessoas estão interessadas em algoritmos
geométricos com aplicações bem diferentes em mente, como computação gráfica, robótica
e tomografia.
Discutiremos algoritmos para apenas dois problemas clássicos de geometria computacional:
o problema do par mais próximo e o problema do casco convexo. Oproblema do par mais
próximoé auto-explicativo: dadonpontos no plano, encontre o par mais próximo entre eles. O
problema do casco convexopede para encontrar o menor polígono convexo que incluiria todos
os pontos de um determinado conjunto. Se você estiver interessado em outros algoritmos
geométricos, encontrará uma riqueza de material em monografias especializadas como [deB10],
[ORo98] e [Pre85].
Problemas Numéricos
problemas numéricos, outra grande área especial de aplicações, são problemas que
envolvem objetos matemáticos de natureza contínua: resolução de equações e sistemas de
equações, cálculo de integrais definidas, avaliação de funções e assim por diante. A maioria
desses problemas matemáticos pode ser resolvida apenas aproximadamente. Outra
dificuldade principal decorre do fato de que tais problemas normalmente requerem a
manipulação de números reais, que podem ser representados em um computador apenas
aproximadamente. Além disso, um grande número de operações aritméticas realizadas em
números representados aproximadamente pode levar a um acúmulo de arredondamento.
1.3Tipos de problemas importantes 23
erro a um ponto onde pode distorcer drasticamente uma saída produzida por um algoritmo
aparentemente sólido.
Muitos algoritmos sofisticados foram desenvolvidos ao longo dos anos nesta área e
continuam a desempenhar um papel crítico em muitas aplicações científicas e de
engenharia. Mas nos últimos 30 anos, a indústria de computação mudou seu foco para
aplicativos de negócios. Esses novos aplicativos requerem principalmente algoritmos para
armazenamento de informações, recuperação, transporte através de redes e apresentação
aos usuários. Como resultado dessa mudança revolucionária, a análise numérica perdeu sua
posição anteriormente dominante tanto na indústria quanto nos programas de ciência da
computação. Ainda assim, é importante para qualquer pessoa alfabetizada em computação
ter pelo menos uma ideia rudimentar sobre algoritmos numéricos. Discutimos vários
algoritmos numéricos clássicos nas Seções 6.2, 11.4 e 12.4.
Exercícios 1.3
1.Considere o algoritmo para o problema de ordenação que classifica um array contando,
para cada um de seus elementos, o número de elementos menores e então usa esta
informação para colocar o elemento em sua posição apropriada no array ordenado:
ALGORITMOComparaçãoContagemClassificação(A[0..n−1])
//Classifica um array por contagem de comparação //
Input: ArrayA[0..n−1] de valores ordenáveis //Saída:
ArrayS[0..n−1] deA's elementos classificados // em ordem
não decrescente
paraeu←0paran−1fazer
Contar[eu]←0
paraeu←0paran−2fazer
paraj←eu+1paran−1fazer
seA[eu]<A[j]
Contar[j]←Contar[j] +1 outro
Contar[eu]←Contar[eu] + 1
paraeu←0paran−1fazer
S[Contar[eu]]←A[eu]
retornarS
a.Aplique este algoritmo para classificar a lista 60, 35, 81, 98, 14, 47.
b.Este algoritmo é estável?
c.Está no local?
2.Nomeie os algoritmos para o problema de busca que você já conhece. Dê uma boa
descrição sucinta de cada algoritmo em inglês. Se você não conhece esses
algoritmos, use esta oportunidade para criar um.
3.Projete um algoritmo simples para o problema de casamento de strings.
24 Introdução
b.Esse problema tem solução? Se você acredita que sim, desenhe tal passeio; se
você acredita que não, explique por que e indique o menor número de novas
pontes que seriam necessárias para tornar esse passeio possível.
5.Jogo IcosianoUm século depois da descoberta de Euler (ver Problema 4), outro
quebra-cabeça famoso — inventado pelo renomado matemático irlandês Sir
William Hamilton (1805-1865) — foi apresentado ao mundo sob o nome de Jogo
Icosiano. O tabuleiro do jogo era um tabuleiro circular de madeira no qual estava
esculpido o seguinte gráfico:
a.A declaração do problema é um tanto vaga, o que é típico dos problemas da vida
real. Em particular, que critério razoável pode ser usado para definir a “melhor”
rota?
b.Como você modelaria esse problema por meio de um gráfico?
b
a
d
c
e
f
a.Explique como podemos usar o problema de coloração de grafos para colorir o mapa de forma
que duas regiões vizinhas não sejam coloridas da mesma forma.
b.Use sua resposta da parte (a) para colorir o mapa com o menor número de
cores.
9.Projete um algoritmo para o seguinte problema: Dado um conjunto denpontos no
plano cartesiano, determine se todos eles estão na mesma circunferência.
10.Escreva um programa que leia como entrada o(x, y)coordenadas das
extremidades de dois segmentos de linhaP1Q1eP2Q2e determina se os
segmentos têm um ponto comum.
As duas estruturas de dados elementares mais importantes são o array e a lista encadeada.
A (unidimensional)variedadeé uma sequência denitens do mesmo tipo de dados que
26 Introdução
números inteiros ou reais). As principais operações em uma fila de prioridade são encontrar seu
maior elemento, excluir seu maior elemento e adicionar um novo elemento. Obviamente, uma fila
de prioridade deve ser implementada para que as duas últimas operações gerem outra fila de
prioridade. As implementações diretas dessa estrutura de dados podem ser baseadas em uma
matriz ou em uma matriz classificada, mas nenhuma dessas opções produz a solução mais
eficiente possível. Uma implementação melhor de uma fila de prioridade é baseada em uma
estrutura de dados engenhosa chamadaamontoar. Discutimos heaps e um importante algoritmo
de ordenação baseado neles na Seção 6.4.
Gráficos
Como mencionamos na seção anterior, um gráfico é pensado informalmente como uma
coleção de pontos no plano chamados “vértices” ou “nós”, alguns deles conectados por
segmentos de linha chamados “arestas” ou “arcos”. Formalmente, umgráficoG=〈V , E〉 é
definido por um par de dois conjuntos: um conjunto finito não vazioVde itens chamados
vértices e um conjuntoEde pares desses itens chamadosarestas. Se esses pares de vértices
não forem ordenados, ou seja, um par de vértices(você, v)é o mesmo do par(v, você),
dizemos que os vérticesvocêevsãoadjacenteentre si e que eles estão conectados pelo borda
não direcionada(u, v).Chamamos os vérticesvocêevpontos finaisda borda(você, v) e diga
issovocêevsãoincidentea esta borda; também dizemos que a aresta(você, v)é incidente em
seus endpointsvocêev.Um gráficoGé chamadonão direcionadose todas as arestas nele não
forem direcionadas.
Se um par de vértices(você, v)não é igual ao par(v, você),dizemos que a borda(você, v)é
dirigidodo vérticevocê,chamado de bordacauda, para o vérticev, chamado de bordacabeça.
Dizemos também que a aresta(você, v)folhasvocêe entrav.Um grafo cujas arestas são
direcionadas é chamadodirigido. Grafos direcionados também são chamadosdígrafos.
V= {a, b, c, d, e, f}, E= {(a, c), (a, d), (b, c), (b, f), (c, e), (d, e), (e, f)}.
O dígrafo representado na Figura 1.6b tem seis vértices e oito arestas direcionadas:
V= {a, b, c, d, e, f}, E= {(a, c), (b, c), (b, f ), (c, e), (d, a), (d, e), (e, c), (e, f )}.
a c b a c b
d e f d e f
(a) (b)
(Obtemos o maior número de arestas em um grafo se houver uma aresta conectando cada
uma de suas |V|vértices com todos |V| −1 outros vértices. Temos que dividir o produto |V|(
|V| −1)por 2, no entanto, porque inclui cada aresta duas vezes.)
Um grafo com cada par de seus vértices conectados por uma aresta é chamadocompleto.
Uma notação padrão para o grafo completo com |V|vértices ék|V|. Um grafo com relativamente
poucas arestas possíveis ausentes é chamadodenso; um grafo com poucas arestas em relação ao
número de seus vértices é chamadoescasso. O fato de estarmos lidando com um grafo denso ou
esparso pode influenciar a forma como escolhemos representar o grafo e, consequentemente, o
tempo de execução de um algoritmo que está sendo projetado ou utilizado.
abcdef
a 001100 a → c → d
b 001001 b → c → f
c 110010 c → a → b → e
d 100010 d → a → e
e 001101 e → c → d → f
f 010010 f → b → e
(a) (b)
FIGURA 1.7(a) Matriz de adjacência e (b) listas de adjacência do grafo da Figura 1.6a.
30 Introdução
listas de adjacência indicam colunas da matriz de adjacência que, para um determinado vértice,
contém 1's.
Se um grafo for esparso, a representação da lista de adjacência pode usar menos
espaço que a matriz de adjacência correspondente, apesar do armazenamento extra
consumido pelos ponteiros das listas encadeadas; a situação é exatamente oposta para
grafos densos. Em geral, qual das duas representações é mais conveniente depende da
natureza do problema, do algoritmo utilizado para resolvê-lo e, possivelmente, do tipo de
grafo de entrada (esparso ou denso).
Caminhos e CiclosDentre as diversas propriedades dos grafos, duas são importantes para um
grande número de aplicações:conectividadeeaciclicidade. Ambos são baseados na noção de um
caminho. Acaminhodo vérticevocêpara o vérticevde um gráficoGpode ser definido como uma
sequência de vértices adjacentes (conectados por uma aresta) que começa comvocêe termina com
v.Se todos os vértices de um caminho são distintos, diz-se que o caminho ésimples. O
comprimento de um caminho é o número total de vértices na sequência de vértices que define o
caminho menos 1, que é igual ao número de arestas no caminho. Por exemplo,a, c, b, fé um
caminho simples de comprimento 3 deaparafno gráfico da Figura 1.6a, enquanto
a, c, e, c, b, fé um caminho (não simples) de comprimento 5 deaparaf.
a b c d
5
a b a ∞5 1 ∞ a → b,5→c,1
1 7 4
b 5 ∞7 4 b → a,5→c,7→d,4
c 1 7 ∞ 2 c → a,1→b,7→d,2
c
2
d d∞ 4 2 ∞ d → b,4→c,2
FIGURA 1.8(a) Gráfico ponderado. (b) Sua matriz de peso. (c) Suas listas de adjacência.
1.4Estruturas de dados fundamentais 31
a f
b c e g h
d eu
árvores
Aárvore(com mais precisão, umárvore livre) é um grafo acíclico conectado (Figura 1.10a). Um
grafo que não tem ciclos, mas não é necessariamente conexo, é chamado defloresta: cada um de
seus componentes conectados é uma árvore (Figura 1.10b).
2. Umsubgrafode um determinado gráficoG=〈V , E〉é um gráficoG′= 〈V′, E′〉de tal modo queV′⊆VeE′⊆E.
32 Introdução
a b a b h
c d c d e eu
f g f g j
(a) (b)
eu d
a
b d e
c b a e
c g f
h g f h eu
(a) (b)
FIGURA 1.11(a) Árvore livre. (b) Sua transformação em uma árvore enraizada.
As árvores têm várias propriedades importantes que outros gráficos não têm. Em
particular, o número de arestas em uma árvore é sempre um a menos que o número de
seus vértices:
Como demonstra o gráfico da Figura 1.9, essa propriedade é necessária, mas não suficiente para
que um gráfico seja uma árvore. No entanto, para grafos conectados, é suficiente e, portanto,
fornece uma maneira conveniente de verificar se um grafo conectado possui um ciclo.
Árvores EnraizadasOutra propriedade muito importante das árvores é o fato de que para
cada dois vértices em uma árvore, sempre existe exatamente um caminho simples de um
desses vértices para o outro. Esta propriedade permite selecionar um vértice arbitrário em
uma árvore livre e considerá-lo como oraizdos chamadosárvore enraizada. Uma árvore
enraizada geralmente é representada colocando sua raiz no topo (nível 0 da árvore), os
vértices adjacentes à raiz abaixo dela (nível 1), os vértices a duas arestas da raiz ainda abaixo
(nível 2) e breve. A Figura 1.11 apresenta tal transformação de uma árvore livre para uma
árvore enraizada.
1.4Estruturas de dados fundamentais 33
Árvores OrdenadasUmárvore ordenadaé uma árvore enraizada na qual todos os filhos de cada vértice
são ordenados. É conveniente assumir que em um diagrama de árvore todos os filhos são ordenados da
esquerda para a direita.
Aárvore bináriapode ser definida como uma árvore ordenada na qual cada vértice não tem
mais do que dois filhos e cada filho é designado como umfilho deixadoou um criança certade
seu pai; uma árvore binária também pode estar vazia. Um exemplo de árvore binária é dado na
Figura 1.12a. A árvore binária com sua raiz no filho esquerdo (direito) de um vértice em uma
árvore binária é chamada deesquerda(certo)subárvoredesse vértice. Como as subárvores
esquerda e direita também são árvores binárias, uma árvore binária também pode ser definida
recursivamente. Isso torna possível resolver muitos problemas envolvendo árvores binárias por
meio de algoritmos recursivos.
34 Introdução
5 12
1 7 10
(a) (b)
5 12 nulo
nulo 4 nulo
Na Figura 1.12b, alguns números são atribuídos aos vértices da árvore binária da Figura
1.12a. Observe que um número atribuído a cada vértice parental é maior que todos os números
em sua subárvore esquerda e menor que todos os números em sua subárvore direita. Tais árvores
são chamadasárvores de pesquisa binária. Árvores binárias e árvores binárias de busca têm uma
ampla variedade de aplicações em ciência da computação; você encontrará alguns deles ao longo
do livro. Em particular, as árvores de busca binárias podem ser generalizadas para tipos mais
gerais de árvores de busca chamadasárvores de pesquisa multiway, que são indispensáveis
para o acesso eficiente a conjuntos de dados muito grandes.
Como você verá mais adiante neste livro, a eficiência dos algoritmos mais importantes
para árvores binárias de busca e suas extensões depende da altura da árvore. Portanto, as
seguintes desigualdades para a alturahde uma árvore binária comnnós são especialmente
importantes para a análise de tais algoritmos:
registro2n≤h≤n−1.
1.4Estruturas de dados fundamentais 35
Uma árvore binária geralmente é implementada para fins de computação por uma
coleção de nós correspondentes aos vértices da árvore. Cada nó contém alguma
informação associada ao vértice (seu nome ou algum valor atribuído a ele) e dois
ponteiros para os nós que representam o filho esquerdo e o filho direito do vértice,
respectivamente. A Figura 1.13 ilustra tal implementação para a árvore de busca
binária da Figura 1.12b.
Uma representação computacional de uma árvore ordenada arbitrária pode ser feita
simplesmente fornecendo a um vértice pai o número de ponteiros igual ao número de seus
filhos. Essa representação pode ser inconveniente se o número de filhos variar muito entre
os nós. Podemos evitar esse inconveniente usando nós com apenas dois ponteiros, como
fizemos para árvores binárias. Aqui, no entanto, o ponteiro esquerdo apontará para o
primeiro filho do vértice e o ponteiro direito apontará para o próximo irmão. Assim, essa
representação é chamada defiprimeiro filho-próximo representação do irmão. Assim,
todos os irmãos de um vértice são vinculados por meio dos ponteiros à direita dos nós em
uma lista encadeada individualmente, com o primeiro elemento da lista apontado pelo
ponteiro esquerdo de seu pai. A Figura 1.14a ilustra essa representação para a árvore da
Figura 1.11b. Não é difícil ver que essa representação efetivamente transforma uma árvore
ordenada em uma árvore binária dita associada à árvore ordenada. Obtemos essa
representação “girando” os ponteiros cerca de 45 graus no sentido horário (consulte a
Figura 1.14b).
Conjuntos e dicionários
A noção de conjunto desempenha um papel central na matemática. Adefinirpode ser descrito
como uma coleção não ordenada (possivelmente vazia) de itens distintos chamadoselementosdo
a nulo a
b nulo d e nulo b
eu f
(a) (b)
FIGURA 1.14(a) Representação do primeiro filho-próximo irmão da árvore na Figura 1.11b. (b) Seu
representação de árvore binária.
36 Introdução
definir. Um conjunto específico é definido por uma lista explícita de seus elementos (por exemplo,S= {2, 3,
5, 7})ou especificando uma propriedade que todos os elementos do conjunto e somente eles devem
satisfazer (por exemplo,S= {n:né um número primo menor que 10}). As operações de conjunto mais
importantes são: verificar a pertinência de um determinado item em um determinado conjunto;
encontrar a união de dois conjuntos, que compreende todos os elementos de um ou de ambos; e
encontrar a interseção de dois conjuntos, que compreende todos os elementos comuns nos conjuntos.
Você deve ter notado que em nossa revisão de estruturas de dados básicas quase
sempre mencionamos operações específicas que normalmente são executadas para a
estrutura em questão. Essa íntima relação entre os dados e as operações é reconhecida há
muito tempo pelos cientistas da computação. Levou-os, em particular, à ideia de umatipo de
dado abstrato(ADT): um conjunto de objetos abstratos que representam itens de dados
com uma coleção de operações que podem ser executadas neles. Como ilustração dessa
noção, releia, digamos, nossas definições de fila de prioridade e dicionário. Embora os tipos
de dados abstratos possam ser implementados em linguagens procedurais mais antigas,
como Pascal (consulte, por exemplo, [Aho83]), é muito mais conveniente fazer isso em
linguagens orientadas a objetos, como C++ e Java, que suportam tipos de dados abstratos
por meio de deAulas.
Exercícios 1.4
1.Descreva como se pode implementar cada uma das seguintes operações em um array de
forma que o tempo necessário não dependa do tamanho do arrayn.
a.Excluir oeuo elemento de um array (1≤eu≤n).
b.Excluir oeuo elemento de uma matriz classificada (a matriz restante deve permanecer
classificada, é claro).
2.Se você tiver que resolver o problema de busca de uma lista dennúmeros, como você pode
tirar proveito do fato de que a lista é conhecida por ser classificada? Dê respostas separadas
para
a.listas representadas como arrays.
5.Dê uma descrição detalhada de um algoritmo para transformar uma árvore livre em uma árvore
enraizada em um determinado vértice da árvore livre.
38 Introdução
registro2n≤h≤n−1.
8.Como você implementaria um dicionário de tamanho razoavelmente pequenonse você soubesse que
todos os seus elementos são distintos (por exemplo, nomes dos 50 estados dos Estados Unidos)?
Especifique uma implementação de cada operação de dicionário.
9.Para cada uma das seguintes aplicações, indique a estrutura de dados mais
apropriada:
a.atender chamadas telefônicas na ordem de suas prioridades conhecidas
b.envio de pedidos pendentes aos clientes na ordem em que foram recebidos
c.implementando uma calculadora para calcular expressões aritméticas simples
RESUMO
Os algoritmos podem ser especificados em uma linguagem natural ou pseudocódigo; eles também podem
ser implementados como programas de computador.
Embora projetar um algoritmo seja, sem dúvida, uma atividade criativa, pode-se
identificar uma sequência de ações inter-relacionadas envolvidas em tal processo. Eles
estão resumidos na Figura 1.2.
Muitas vezes, o mesmo problema pode ser resolvido por vários algoritmos. Por exemplo, três
algoritmos foram fornecidos para calcular o máximo divisor comum de dois números
inteiros:algoritmo de euclides, o algoritmo de verificação de números inteiros consecutivos e
o método do ensino médio aprimorado pelopeneira de Eratóstenespara gerar uma lista de
primos.
Uma coleção abstrata de objetos com várias operações que podem ser executadas
neles é chamada detipo de dado abstrato(ADT). Olista, opilha, o fila, oFila de
prioridade,e adicionáriosão exemplos importantes de tipos de dados abstratos.
Linguagens modernas orientadas a objetos suportam a implementação de ADTs
por meio de classes.
Esta página foi intencionalmente deixada em branco