Você está na página 1de 62

Traduzido do Inglês para o Português - www.onlinedoctranslator.

com

Breve Conteúdo

Novo no prefácio da terceira xvii


edição xix
1Introdução 1
2Fundamentos da Análise de Eficiência de Algoritmos 3 41
Força Bruta e Busca Exaustiva 4Diminuir e Conquistar 97
131
5Dividir e conquistar 169
6Transforme-e-Conquiste 7 201
Compensações de espaço e 253
tempo 8Programaçao dinamica 283
9Técnica Gananciosa 315
10Melhoria iterativa 11Limitações 345
do Poder do Algoritmo 387
12Lidando com as limitações do poder do algoritmo 423
Epílogo 471
APÊNDICE A
Fórmulas Úteis para a Análise de Algoritmos 475
APÊNDICE B
Breve tutorial sobre relações de recorrência 479
Referências 493
Dicas para Exercícios 503
Índice 547

v
Esta página foi intencionalmente deixada em branco
Conteúdo

Novidade na terceira edição xvii

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

1.4 Estruturas de dados fundamentais 25


Estruturas de Dados Lineares 25
Gráficos 28
árvores 31
Conjuntos e dicionários 35
Exercícios 1.4 37
Resumo 38

2Fundamentos da Análise de Algoritmo


Eficiência 41
2.1 O Quadro de Análise 42
Medindo o Tamanho de uma Entrada Unidades 43
para Medir o Tempo de Execução Ordens de 44
Crescimento 45
Eficiências de pior caso, melhor caso e caso médio 47
Recapitulação da estrutura de análise 50
Exercícios 2.1 50
2.2 Notações Assintóticas e Classes Básicas de Eficiência 52
Introdução Informal 52
O-notação 53
- - notação 54
- - notação 55
Propriedade Útil Envolvendo as Notações Assintóticas 55
Usando Limites para Comparar Ordens de Crescimento 56
Classes Básicas de Eficiência 58
Exercícios 2.2 58
2.3 Análise Matemática de Algoritmos Não Recursivos 61
Exercícios 2.3 67
2.4 Análise Matemática de Algoritmos Recursivos 70
Exercícios 2.4 76
2.5 Exemplo: Calculando onnúmero de Fibonacci 80
Exercícios 2.5 83
2.6 Análise Empírica de Algoritmos 84
Exercícios 2.6 89
2.7 Visualização do Algoritmo 91
Resumo 94
Conteúdo ix

3Força Bruta e Busca Exaustiva 97

3.1 Ordenação por Seleção e Ordenação por Bolhas 98


Classificação de seleção 98
Tipo de bolha 100
Exercícios 3.1 102

3.2 Pesquisa Sequencial e Correspondência de String de Força Bruta 104


Pesquisa sequencial 104
Correspondência de strings de força bruta 105
Exercícios 3.2 106

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

3.4 Pesquisa Exaustiva 115


Problema do Caixeiro Viajante 116
Problema da Mochila 116
Problema de atribuição 119
Exercícios 3.4 120

3.5 Pesquisa em profundidade e pesquisa em largura 122


Pesquisa em profundidade 122
Pesquisa em largura 125
Exercícios 3.5 128
Resumo 130

4Diminuir e Conquistar 131

4.1 Ordenação por Inserção 134


Exercícios 4.1 136

4.2 Ordenação Topológica 138


Exercícios 4.2 142

4.3 Algoritmos para Geração de Objetos Combinatórios 144


Gerando Permutações 144
Gerando Subconjuntos 146
Exercícios 4.3 148
x Conteúdo

4.4 Algoritmos de diminuição por fator constante 150


Pesquisa Binária 150
Problema com Moedas Falsas 152
Problema Josefo da Multiplicação 153
Camponesa Russa 154
Exercícios 4.4 156

4.5 Algoritmos de Diminuição de Tamanho Variável 157


Calculando uma Mediana e a Pesquisa de Interpolação 158
do Problema de Seleção 161
Pesquisa e Inserção em uma Árvore de Busca Binária 163
O Jogo de Nim 164
Exercícios 4.5 166
Resumo 167

5Dividir e conquistar 169

5.1 Mesclagem 172


Exercícios 5.1 174

5.2 Classificação rápida 176


Exercícios 5.2 181

5.3 Percursos de árvore binária e propriedades relacionadas 182


Exercícios 5.3 185

5.4 Multiplicação de Inteiros Grandes e


Multiplicação da Matriz de Strassen 186
Multiplicação de Inteiros Grandes 187
Multiplicação da Matriz de Strassen 189
Exercícios 5.4 191

5.5 Problemas do par mais próximo e do casco convexo


por divisão e conquista 192
O problema do par mais próximo 192
Problema do casco convexo 195
Exercícios 5.5 197
Resumo 198
Conteúdo XI

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

7Compensações de espaço e tempo 253


7.1 Ordenando por Contagem 254
Exercícios 7.1 257
7.2 Aprimoramento de entrada na correspondência de strings 258
Algoritmo de Horspool 259
xii Conteúdo

Algoritmo de Boyer-Moore 263


Exercícios 7.2 267

7.3 Hashing 269


Hashing aberto (encadeamento separado) 270
Hashing fechado (endereçamento aberto) 272
Exercícios 7.3 274

7.4 Árvores B 276


Exercícios 7.4 279
Resumo 280

8Programaçao dinamica 283

8.1 Três Exemplos Básicos 285


Exercícios 8.1 290

8.2 O Problema da Mochila e as Funções de Memória 292


Funções de Memória 294
Exercícios 8.2 296

8.3 Árvores Binárias de Busca Ótimas 297


Exercícios 8.3 303

8.4 Algoritmos de Warshall e Floyd 304


Algoritmo de Warshall 304
Algoritmo de Floyd para o problema de todos os pares de caminhos mais curtos 308
Exercícios 8.4 311
Resumo 312

9Técnica Gananciosa 315

9.1 Algoritmo de Prim 318


Exercícios 9.1 322

9.2 Algoritmo de Kruskal 325


Subconjuntos disjuntos e algoritmos de localização de união 327
Exercícios 9.2 331

9.3 Algoritmo de Dijkstra 333


Exercícios 9.3 337
Conteúdo xiii

9.4 Árvores e Códigos de Huffman 338


Exercícios 9.4 342
Resumo 344

10Melhoria iterativa 345

10.1 O Método Simplex 346


Interpretação Geométrica da Programação Linear 347
Um Esboço do Método Simplex 351
Notas adicionais sobre o método Simplex 357
Exercícios 10.1 359

10.2 O problema do fluxo máximo 361


Exercícios 10.2 371

10.3 Correspondência Máxima em Gráficos Bipartidos 372


Exercícios 10.3 378

10.4 O problema do casamento estável 380


Exercícios 10.4 383
Resumo 384

11Limitações do Poder do Algoritmo 387

11.1 Argumentos do Limite Inferior 388


Limites inferiores triviais 389
Argumentos da Teoria da Informação 390
Argumentos do Adversário 390
Redução de problemas 391
Exercícios 11.1 393

11.2 Árvores de decisão 394


Árvores de decisão para classificação 395
Árvores de decisão para pesquisar uma matriz classificada 397
Exercícios 11.2 399

11.3P,NP,eNP-Problemas completos 401


PeNPproblemas NP- 402
Problemas completos 406
Exercícios 11.3 409
xiv Conteúdo

11.4 Desafios dos Algoritmos Numéricos 412


Exercícios 11.4 419
Resumo 420

12Lidando com as limitações do poder do algoritmo 423


12.1 Retrocesso 424
n-Problema das Rainhas 425
Problema do Circuito Hamiltoniano 426
Problema da Soma do Subconjunto 427
Observações Gerais 428
Exercícios 12.1 430
12.2 Ramificação e Limitação 432
Problema de atribuição 433
problema da mochila 436
Problema do Caixeiro Viajante 438
Exercícios 12.2 440
12.3 Algoritmos de Aproximação paraNP-Problemas Difíceis 441
Algoritmos de Aproximação para o Problema do Caixeiro Viajante 443
Algoritmos de Aproximação para o Problema da Mochila 453
Exercícios 12.3 457
12.4 Algoritmos para Resolução de Equações Não Lineares 459
Método da bisseção 460
Método da Posição Falsa 464
Método de Newton 464
Exercícios 12.4 467
Resumo 468

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

Aproximação de uma Soma por Fórmulas de Piso e 477


Teto Integrais Definidas 477
Diversos 477

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

Dicas para Exercícios 503

Índice 547
Esta página foi intencionalmente deixada em branco
Novidade na terceira edição

Reordenação dos capítulos para introduzir diminuir e conquistar antes de dividir e


conquistar
Reestruturação do capítulo 8 sobre programação dinâmica, incluindo todo o novo
material introdutório e novos exercícios com foco em aplicativos conhecidos
Mais cobertura das aplicações dos algoritmos discutidos
Reordenação de seções selecionadas ao longo do livro para obter um melhor
alinhamento de algoritmos específicos e técnicas gerais de design de algoritmos
Adição da partição Lomuto e algoritmos de código Gray
Setenta novos problemas adicionados aos exercícios do final do capítulo, incluindo quebra-cabeças
algorítmicos e perguntas feitas durante entrevistas de emprego

xvii
Esta página foi intencionalmente deixada em branco
Prefácio

As aquisições mais valiosas em uma educação científica ou técnica são as


ferramentas mentais de uso geral que permanecem úteis por toda a vida.
— George Forsythe, “O que fazer até que o cientista da computação chegue.” (1968)

A Os algoritmos desempenham o papel central tanto na ciência quanto na prática da


computação. O reconhecimento desse fato levou ao surgimento de um número
considerável de livros didáticos sobre o assunto. Em geral, eles seguem uma das duas alternativas
na apresentação de algoritmos. Um classifica algoritmos de acordo com um tipo de problema.
Esse livro teria capítulos separados sobre algoritmos para classificação, pesquisa, gráficos e assim
por diante. A vantagem dessa abordagem é que ela permite uma comparação imediata, digamos,
da eficiência de diferentes algoritmos para o mesmo problema. A desvantagem dessa abordagem
é que ela enfatiza os tipos de problemas em detrimento das técnicas de design de algoritmos.

A segunda alternativa organiza a apresentação em torno de técnicas de design de


algoritmos. Nessa organização, algoritmos de diferentes áreas da computação são agrupados se
tiverem a mesma abordagem de design. Eu compartilho a crença de muitos (por exemplo,
[BaY95]) de que essa organização é mais apropriada para um curso básico de projeto e análise de
algoritmos. Existem três razões principais para a ênfase em técnicas de projeto de algoritmos.
Primeiro, essas técnicas fornecem ao aluno ferramentas para projetar algoritmos para novos
problemas. Isso torna o aprendizado de técnicas de design de algoritmos um esforço muito
valioso do ponto de vista prático. Em segundo lugar, eles procuram classificar uma infinidade de
algoritmos conhecidos de acordo com uma ideia de design subjacente. Aprender a ver tal
semelhança entre algoritmos de diferentes áreas de aplicação deve ser um dos principais objetivos
da educação em ciência da computação. Afinal, toda ciência considera a classificação de seu
assunto principal como um dos principais, senão o ponto central de sua disciplina. Em terceiro
lugar, na minha opinião, as técnicas de design de algoritmos têm utilidade como estratégias gerais
de resolução de problemas, aplicáveis a problemas além da computação.

xix
xx Prefácio

Infelizmente, a classificação tradicional de técnicas de projeto de algoritmos tem


várias deficiências sérias, tanto do ponto de vista teórico quanto educacional. A mais
significativa dessas deficiências é a falha em classificar muitos algoritmos importantes.
Essa limitação forçou os autores de outros livros-texto a se afastarem da organização
da técnica de projeto e a incluírem capítulos que tratam de tipos de problemas
específicos. Tal mudança leva a uma perda de coerência do curso e quase
inevitavelmente cria uma confusão nas mentes dos alunos.

Nova taxonomia de técnicas de design de algoritmos


Minha frustração com as deficiências da classificação tradicional de técnicas de
projeto de algoritmos me motivou a desenvolver uma nova taxonomia delas
[Lev99], que é a base deste livro. Aqui estão as principais vantagens da nova
taxonomia:
A nova taxonomia é mais abrangente do que a tradicional. Ele inclui várias
estratégias – força bruta, diminuir e conquistar, transformar e conquistar,
compensações de espaço e tempo e melhoria iterativa – que raramente ou nunca
são reconhecidas como importantes paradigmas de design.
A nova taxonomia abrange naturalmente muitos algoritmos clássicos (algoritmo de
Euclides, heapsort, árvores de busca, hashing, classificação topológica, eliminação
gaussiana, regra de Horner — para citar alguns) que a taxonomia tradicional não pode
classificar. Como resultado, a nova taxonomia torna possível apresentar o corpo padrão
de algoritmos clássicos de forma unificada e coerente.
Acomoda naturalmente a existência de variedades importantes de várias
técnicas de design. Por exemplo, ele reconhece três variações de diminuir e
conquistar e três variações de transformar e conquistar.
Está mais bem alinhada com os métodos analíticos para a análise de eficiência (ver
Apêndice B).

Técnicas de design como estratégias gerais de resolução de problemas

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.

pedagogia de livro didático

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.

Compartilhando a opinião de George Forsythe expressa na epígrafe, procurei enfatizar


as principais ideias subjacentes ao projeto e análise de algoritmos. Ao escolher
algoritmos específicos para ilustrar essas ideias, limitei o número de algoritmos
cobertos àqueles que demonstram uma técnica de projeto subjacente ou um método
de análise mais claramente. Felizmente, a maioria dos algoritmos clássicos atende a
esse critério.
No Capítulo 2, dedicado à análise de eficiência, os métodos usados para analisar
algoritmos não recursivos são separados daqueles normalmente usados para
analisar algoritmos recursivos. O capítulo também inclui seções dedicadas à
análise empírica e visualização de algoritmos.
A narrativa é sistematicamente interrompida por perguntas ao leitor.
Algumas delas são feitas retoricamente, antecipando uma preocupação ou
dúvida, e são respondidas imediatamente. O objetivo dos demais é evitar que
o leitor vagueie pelo texto sem um nível satisfatório de compreensão.
Cada capítulo termina com um resumo recapitulando os conceitos e resultados mais
importantes discutidos no capítulo.
O livro contém mais de 600 exercícios. Alguns deles são exercícios; outros apresentam
pontos importantes sobre o material abordado no corpo do texto ou introduzem
algoritmos que não são abordados ali. Alguns exercícios aproveitam os recursos da
Internet. Problemas mais difíceis - não há muitos deles - são marcados por símbolos
especiais no Manual do Instrutor. (Como marcar os problemas como difíceis pode
desencorajar alguns alunos de tentar resolvê-los, os problemas não são marcados no
próprio livro.) Quebra-cabeças, jogos e questões semelhantes a quebra-cabeças são
marcados nos exercícios com um ícone especial.
O livro fornece dicas para todos os exercícios. Soluções detalhadas, exceto para projetos de
programação, são fornecidas no Manual do Instrutor, disponível para adotantes qualificados
por meio do Centro de Recursos do Instrutor da Pearson. (Entre em contato com seu
representante de vendas local da Pearson ou acesse www.pearsonhighered . com/irc para
acessar este material.) Os slides em PowerPoint estão disponíveis para todos os leitores deste
livro via ftp anônimo no site de suporte do CS: http://cssupport . pearsoncmg.com/.
xxii Prefácio

Mudanças para a terceira edição


Há algumas mudanças na terceira edição. O mais importante é a nova ordem dos
capítulos sobre diminuir-e-conquistar e dividir-e-conquistar. Existem várias vantagens
na introdução de diminuir e conquistar antes de dividir e conquistar:

Diminuir e conquistar é uma estratégia mais simples do que dividir e conquistar.


Diminuir e conquistar é aplicável a mais problemas do que dividir e
conquistar.
A nova ordem torna possível discutir a classificação por inserção antes do mergesort e
do quicksort.
A ideia de particionamento de array é agora introduzida em conjunto com o problema
de seleção. Aproveitei a oportunidade para fazer isso por meio da varredura
unidirecional empregada pelo algoritmo de Lomuto, deixando a varredura bidirecional
usada pelo particionamento de Hoare para uma discussão posterior em conjunto com o
quicksort.
A busca binária agora é considerada na seção dedicada aos algoritmos de
diminuição por fator constante, onde ela pertence.

A segunda mudança importante é a reestruturação do Capítulo 8 sobre programação


dinâmica. Especificamente:

A seção introdutória é completamente nova. Ele contém três exemplos básicos


que fornecem uma introdução muito melhor a essa importante técnica do que
calcular um coeficiente binomial, o exemplo usado nas duas primeiras edições.
Todos os exercícios da Seção 8.1 também são novos; eles incluem aplicativos
conhecidos não disponíveis nas edições anteriores.
Também mudei a ordem das outras seções neste capítulo para obter uma
progressão mais suave dos aplicativos mais simples para os mais avançados.

As outras alterações incluem o seguinte. Mais aplicações dos algoritmos discutidos


estão incluídas. A seção sobre algoritmos de travessia de grafos foi movida do capítulo de
diminuir e conquistar para o capítulo de força bruta e busca exaustiva, onde se encaixa
melhor, na minha opinião. O algoritmo de código Gray é adicionado à seção que trata de
algoritmos para geração de objetos combinatórios. O algoritmo de divisão e conquista para
o problema do par mais próximo é discutido com mais detalhes. As atualizações incluem a
seção sobre visualização de algoritmos, algoritmos de aproximação para o problema do
caixeiro viajante e, é claro, a bibliografia.
Eu também adicionei cerca de 70 novos problemas aos exercícios. Alguns deles são quebra-cabeças
algorítmicos e perguntas feitas durante entrevistas de emprego.

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

Ainda assim, estruturas de dados fundamentais, fórmulas de soma necessárias e


relações de recorrência são revisadas na Seção 1.4, Apêndice A e Apêndice B,
respectivamente. O cálculo é usado em apenas três seções (Seção 2.2, 11.4 e 12.4) e em
grau muito limitado; se os alunos carecem de cálculo como uma parte garantida de
sua formação, as partes relevantes dessas três seções podem ser omitidas sem
prejudicar sua compreensão do restante do material.

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.

Palestra Tema Seções

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

25–27 Algoritmos de programação dinâmica 3 de 8.1–8.4


xxiv Prefácio

28–30 Algoritmos gananciosos: Prim's, Kruskal's, Dijkstra's, 9.1–9.4


Huffman's
31–33 Algoritmos de melhoria iterativa 3 de 10,1–10,4
34 Argumentos de limite inferior 11.1
35 Árvores de decisão 11.2
36 P, NP, eNP-problemas completos 11.3
37 algoritmos numéricos 11,4 (+ 12,4)
38 retrocesso 12.1
39 Branch-and-bound 12.2
40 Algoritmos de aproximação paraNP-problemas difíceis 12.3

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

Duas ideias brilham no veludo do joalheiro. A primeira é o cálculo, a


segunda, o algoritmo. O cálculo e o rico corpo de análise matemática a
que deu origem tornaram a ciência moderna possível; mas foi o
algoritmo que tornou possível o mundo moderno.
—David Berlinski,O Advento do Algoritmo,2000

C Por que você precisa estudar algoritmos? Se você for um profissional de


computação, existem razões práticas e teóricas para estudar algoritmos. Do
ponto de vista prático, você precisa conhecer um conjunto padrão de algoritmos
importantes de diferentes áreas da computação; além disso, você deve ser capaz de
projetar novos algoritmos e analisar sua eficiência. Do ponto de vista teórico, o estudo
de algoritmos, às vezes chamados dealgorítmica, passou a ser reconhecido como a
pedra angular da ciência da computação. David Harel, em seu delicioso livro intitulado
Algoritmia: o espírito da computação, coloque da seguinte forma:

Algoritmia é mais do que um ramo da ciência da computação. É o núcleo da ciência da


computação e, com toda a justiça, pode ser considerado relevante para a maior parte da
ciência, negócios e tecnologia. [Har92, pág. 6]

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.

A Seção 1.2 trata da solução algorítmica de problemas. Lá discutimos várias


questões importantes relacionadas ao projeto e análise de algoritmos. Os diferentes
aspectos da resolução de problemas algorítmicos variam desde a análise do problema
e os meios de expressar um algoritmo até estabelecer sua correção e analisar sua
eficiência. A seção não contém uma receita mágica para projetar um algoritmo para
um problema arbitrário. É um fato bem estabelecido que tal receita não existe. Ainda
assim, o material da Seção 1.2 deve ser útil para organizar seu trabalho de projeto e
análise de algoritmos.
A Seção 1.3 é dedicada a alguns tipos de problemas que provaram ser particularmente
importantes para o estudo de algoritmos e sua aplicação. Na verdade, existem livros didáticos (por
exemplo, [Sed11]) organizados em torno desses tipos de problemas. Tenho a opinião —
compartilhada por muitos outros — de que uma organização baseada em técnicas de design de
algoritmos é superior. De qualquer forma, é muito importante conhecer os principais tipos de
problemas. Eles não são apenas os tipos de problemas mais comumente encontrados em
aplicações da vida real, mas também são usados ao longo do livro para demonstrar técnicas
específicas de projeto de algoritmos.
A Seção 1.4 contém uma revisão das estruturas de dados fundamentais. Destina-se a servir
como uma referência em vez de uma discussão deliberada deste tópico. Se você precisar
1.1O que é um algoritmo? 3

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.

1.1O que é um algoritmo?


Embora não haja uma redação universalmente aceita para descrever essa noção, há
um consenso geral sobre o que o conceito significa:

Umalgoritmoé uma sequência de instruções inequívocas para resolver um problema, ou


seja, para obter uma saída necessária para qualquer entrada legítima em um período de
tempo finito.

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

entrada "computador" saída

FIGURA 1.1A noção do algoritmo.


4 Introdução

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),

ondemmodné o resto da divisão demporn, atémmodné igual a 0. Como gcd


(m,0)=m(por quê?), o último valor demé também o máximo divisor comum da
inicialmen.
Por exemplo, gcd(60,24)pode ser calculado da seguinte forma:

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:

algoritmo de euclidespara computar gcd(m,n)


Passo 1Sen=0, retorna o valor demcomo a resposta e pare; de outra forma,
prossiga para a Etapa 2.

Passo 2Dividirmporne atribua o valor do restante ar. etapa 3Atribuir


o valor denparame o valor derparan. Vá para a Etapa 1.

Como alternativa, podemos expressar o mesmo algoritmo em pseudocódigo:

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

Como sabemos que o algoritmo de Euclides acaba parando? Isso decorre


da observação de que o segundo inteiro do par fica menor a cada iteração e
não pode se tornar negativo. De fato, o novo valor denna próxima iteração é
mmodn,que é sempre menor quen(por que?).Portanto, o valor do segundo
inteiro eventualmente se torna 0 e o algoritmo para.
1.1O que é um algoritmo? 5

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.

Algoritmo de verificação de número inteiro consecutivopara computar gcd(m,n)

Passo 1Atribua o valor de min{m, n}parat.


Passo 2Dividirmport.Se o resto desta divisão for 0, vá para a Etapa 3;
caso contrário, vá para a Etapa 4.

etapa 3Dividirnport.Se o resto desta divisão for 0, retorne o valor de


tcomo a resposta e pare; caso contrário, prossiga para a Etapa 4.
Passo 4Diminuir o valor detpor 1. Vá para a Etapa 2.

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.

procedimento do ensino médiopara computar gcd(m,n)


Passo 1Encontre os fatores primos dem.
Passo 2Encontre os fatores primos den.
etapa 3Identifique todos os fatores comuns nas duas expansões principais encontradas em
Passo 1 e Passo 2. (Sepé um fator comum que ocorrepmepnvezes emm
en,respectivamente, deve ser repetido min{pm,pn}vezes.)
Passo 4Calcule o produto de todos os fatores comuns e retorne-o como o
máximo divisor comum dos números dados.
Assim, para os números 60 e 24, obtemos

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.

Como exemplo, considere a aplicação do algoritmo para encontrar a lista de


primos que não excedamn=25:

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

Então agora podemos incorporar o crivo de Eratóstenes no procedimento do ensino


médio para obter um algoritmo legítimo para calcular o máximo divisor comum de dois
inteiros positivos. Observe que um cuidado especial deve ser exercido se um ou ambos os
números de entrada forem iguais a 1: como os matemáticos não consideram 1 como um
número primo, estritamente falando, o método não funciona para tais entradas.
Antes de deixarmos esta seção, cabe mais um comentário. Apesar dos exemplos
considerados nesta seção, a maioria dos algoritmos em uso hoje — mesmo aqueles
que são implementados como programas de computador — não lida com problemas
matemáticos. Procure algoritmos que nos ajudem em nossas rotinas diárias, tanto
profissionais quanto pessoais. Que essa onipresença de algoritmos no mundo de hoje
fortaleça sua determinação de aprender mais sobre esses mecanismos fascinantes da
era da informação.

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

5.Projete um algoritmo para encontrar todos os elementos comuns em duas listas


ordenadas de números. Por exemplo, para as listas 2, 5, 5, 5 e 2, 2, 3, 5, 5, 7, a saída
deve ser 2, 5, 5. Qual é o número máximo de comparações que seu algoritmo faz se os
comprimentos de as duas listas fornecidas sãomen,respectivamente?

6. a.Encontre mdc(31415, 14142) aplicando o algoritmo de Euclides.


b.Estime quantas vezes será mais rápido encontrar mdc(31415, 14142) pelo algoritmo
de Euclides em comparação com o algoritmo baseado na verificação de inteiros
consecutivos de min{m, n}para baixo para gcd(m, n).

7.Prove a igualdade mdc(m,n)=gcd(n, mmodn)para cada par de inteiros


positivosmen.

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?

9. a.Qual é o número mínimo de divisões feitas pelo algoritmo de Euclides entre


todas as entradas 1≤m, n≤10?
b.Qual é o número máximo de divisões feitas pelo algoritmo de Euclides entre
todas as entradas 1≤m, n≤10?

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.

b.jogo de euclides(veja [Bog]) começa com dois inteiros positivos desiguais no


tabuleiro. Dois jogadores se movem por sua vez. A cada jogada, o jogador deve
escrever no tabuleiro um número positivo igual à diferença de dois números já
existentes no tabuleiro; esse número deve ser novo, ou seja, diferente de todos os
números que já estão no tabuleiro. O jogador que não consegue se mover perde o
jogo. Você deve escolher mover primeiro ou segundo neste jogo?

11.Oalgoritmo de Euclides estendidodetermina não apenas o máximo divisor comumdde dois


inteiros positivosmenmas também números inteiros (não necessariamente positivos)xey, de
tal modo quemx+Nova Iorque=d.
a.Procure uma descrição do algoritmo de Euclides estendido (veja, por exemplo, [KnuI,
pág. 13]) e implementá-lo no idioma de sua escolha.
b.Modifique seu programa para encontrar soluções inteiras para a equação diofantina
machado+por=ccom qualquer conjunto de coeficientes inteirosa,b, ec.

12.Portas de armárioHánarmários em um corredor, numerados sequencialmente de 1 an.


Inicialmente, todas as portas dos armários estão fechadas. Você faznpassa pelos armários,
sempre começando pelo armário nº 1. Noeuª passagem,eu=1,2, . . . , n, você alterna a porta
de cadaeuo armário: se a porta estiver fechada, você abre; se estiver aberto, você o fecha.
Após a última passagem, quais portas do armário estão abertas e quais estão fechadas?
Quantos deles estão abertos?
1.2Fundamentos da resolução algorítmica de problemas 9

1.2Fundamentos da resolução algorítmica de problemas

Vamos começar reiterando um ponto importante feito na introdução deste


capítulo:

Podemos considerar os algoritmos como soluções processuais para problemas.

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.

Determinando as capacidades do dispositivo computacional


Depois de entender completamente um problema, você precisa verificar os recursos do
dispositivo computacional para o qual o algoritmo se destina. A grande maioria de
10 Introdução

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

FIGURA 1.2Projeto de algoritmo e processo de análise.

os algoritmos em uso hoje ainda estão destinados a serem programados para um


computador muito parecido com a máquina de von Neumann - uma arquitetura de
computador delineada pelo proeminente matemático húngaro-americano John von
Neumann (1903–1957), em colaboração com A. Burks e H. Goldstine , em 1946. A
essência desta arquitetura é captada pelo chamadomáquina de acesso aleatório(
BATER). Sua suposição central é que as instruções são executadas uma após a outra,
uma operação por vez. Assim, os algoritmos projetados para serem executados em tais
máquinas são chamadosalgoritmos sequenciais.
A suposição central do modelo de RAM não é válida para alguns computadores mais
novos que podem executar operações simultaneamente, ou seja, em paralelo. Algoritmos
que tiram proveito dessa capacidade são chamadosalgoritmos paralelos. Ainda assim,
estudar as técnicas clássicas de projeto e análise de algoritmos sob o modelo RAM continua
sendo a pedra angular da algorítmica no futuro próximo.
1.2Fundamentos da resolução algorítmica de problemas 11

Você deve se preocupar com a velocidade e a quantidade de memória de um


computador à sua disposição? Se você está projetando um algoritmo como um exercício
científico, a resposta é um não qualificado. Como você verá na Seção 2.1, a maioria dos
cientistas da computação prefere estudar algoritmos em termos independentes dos
parâmetros de especificação de um determinado computador. Se você estiver projetando
um algoritmo como uma ferramenta prática, a resposta pode depender de um problema
que você precisa resolver. Mesmo os computadores “lentos” de hoje são quase
inimaginavelmente rápidos. Conseqüentemente, em muitas situações, você não precisa se
preocupar com a lentidão do computador para a tarefa. Existem problemas importantes, no
entanto, que são muito complexos por sua natureza, ou precisam processar grandes
volumes de dados, ou lidar com aplicativos em que o tempo é crítico. Em tais situações,

Escolhendo entre resolução exata e aproximada de problemas


A próxima decisão principal é escolher entre resolver o problema exatamente ou resolvê-lo
aproximadamente. No primeiro caso, um algoritmo é chamado dealgoritmo exato; neste último
caso, um algoritmo é chamado dealgoritmo de aproximação. Por que alguém optaria por um
algoritmo de aproximação? Primeiro, existem problemas importantes que simplesmente não
podem ser resolvidos exatamente para a maioria de suas instâncias; os exemplos incluem
extração de raízes quadradas, resolução de equações não lineares e avaliação de integrais
definidas. Em segundo lugar, os algoritmos disponíveis para resolver um problema com exatidão
podem ser inaceitavelmente lentos devido à complexidade intrínseca do problema. Isso acontece,
em particular, para muitos problemas envolvendo um número muito grande de escolhas; você
verá exemplos desses problemas difíceis nos Capítulos 3, 11 e 12. Em terceiro lugar, um algoritmo
de aproximação pode ser parte de um algoritmo mais sofisticado que resolve um problema com
exatidão.

Técnicas de Design de Algoritmo


Agora, com todos os componentes da solução de problemas algorítmicos no lugar, como você
projeta um algoritmo para resolver um determinado problema? Esta é a principal questão que
este livro procura responder, ensinando várias técnicas gerais de design.
O que é uma técnica de design de algoritmo?

Umtécnica de design de algoritmo(ou “estratégia” ou “paradigma”) é uma


abordagem geral para resolver problemas algoritmicamente que é aplicável a uma
variedade de problemas de diferentes áreas da computação.

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.

Projetando um Algoritmo e Estruturas de Dados


Embora as técnicas de design de algoritmos forneçam um poderoso conjunto de
abordagens gerais para a solução de problemas algorítmicos, projetar um algoritmo para
um problema específico ainda pode ser uma tarefa desafiadora. Algumas técnicas de
projeto podem ser simplesmente inaplicáveis ao problema em questão. Às vezes, várias
técnicas precisam ser combinadas e existem algoritmos difíceis de identificar como
aplicações das técnicas de design conhecidas. Mesmo quando uma técnica de projeto
particular é aplicável, obter um algoritmo geralmente requer uma engenhosidade não trivial
por parte do projetista do algoritmo. Com a prática, ambas as tarefas — escolher entre as
técnicas gerais e aplicá-las — ficam mais fáceis, mas raramente são fáceis.
Obviamente, deve-se prestar muita atenção na escolha de estruturas de dados apropriadas
para as operações executadas pelo algoritmo. Por exemplo, o crivo de Eratóstenes introduzido na
Seção 1.1 seria mais longo se usássemos uma lista encadeada em vez de um array em sua
implementação (por quê?). Observe também que algumas das técnicas de projeto de algoritmos
discutidas nos Capítulos 6 e 7 dependem intimamente da estruturação ou reestruturação de
dados que especificam a instância de um problema. Muitos anos atrás, um livro influente
proclamou a importância fundamental de ambos os algoritmos e estruturas de dados para a
programação de computadores por seu próprio título:Algoritmos
+ Estruturas de Dados = Programas[Wir76]. No novo mundo da programação orientada a objetos, as
estruturas de dados permanecem de importância crucial tanto para o projeto quanto para a análise de
algoritmos. Revemos as estruturas básicas de dados na Seção 1.4.

Métodos de especificação de um algoritmo


Depois de projetar um algoritmo, você precisa especificá-lo de alguma forma. Na Seção
1.1, para dar um exemplo, o algoritmo de Euclides é descrito em palavras (de forma
livre e também passo a passo) e em pseudocódigo. Essas são as duas opções mais
utilizadas hoje em dia para especificar algoritmos.
Usar uma linguagem natural tem um apelo óbvio; no entanto, a ambigüidade inerente de
qualquer linguagem natural torna surpreendentemente difícil uma descrição sucinta e clara dos
algoritmos. No entanto, ser capaz de fazer isso é uma habilidade importante que você deve se
esforçar para desenvolver no processo de aprendizagem de algoritmos.
Pseudo-códigoé uma mistura de uma linguagem natural e construções semelhantes a linguagens
de programação. O pseudocódigo é geralmente mais preciso do que a linguagem natural, e sua
1.2Fundamentos da resolução algorítmica de problemas 13

o uso geralmente produz descrições de algoritmo mais sucintas. Surpreendentemente, os cientistas da


computação nunca concordaram com uma única forma de pseudocódigo, deixando os autores de livros
didáticos com a necessidade de projetar seus próprios “dialetos”. Felizmente, esses dialetos estão tão
próximos um do outro que qualquer pessoa familiarizada com uma linguagem de programação moderna
deve ser capaz de entendê-los todos.
O dialeto deste livro foi selecionado para causar o mínimo de dificuldade ao leitor.
Para simplificar, omitimos declarações de variáveis e usamos indentação para
mostrar o escopo de declarações comopara,se, eenquanto. Como você viu na seção
anterior, usamos uma seta “←”para a operação de atribuição e duas barras “//” para
comentários.
Nos primórdios da computação, o veículo dominante para especificar algoritmos era
umflowchart, um método de expressar um algoritmo por uma coleção de formas
geométricas conectadas contendo descrições das etapas do algoritmo. Essa técnica de
representação provou ser inconveniente para todos, exceto para algoritmos muito simples;
hoje em dia, ele pode ser encontrado apenas em livros antigos de algoritmos.
O estado da arte da computação ainda não atingiu um ponto em que a descrição de um
algoritmo — seja em linguagem natural ou em pseudocódigo — possa ser inserida
diretamente em um computador eletrônico. Em vez disso, ele precisa ser convertido em um
programa de computador escrito em uma linguagem de computador específica. Podemos
olhar para tal programa como outra forma de especificar o algoritmo, embora seja
preferível considerá-lo como a implementação do algoritmo.

Provando a exatidão de um algoritmo


Uma vez que um algoritmo foi especificado, você tem que provar suacorreção. Ou
seja, você precisa provar que o algoritmo produz um resultado necessário para cada
entrada legítima em um período de tempo finito. Por exemplo, a exatidão do algoritmo
de Euclides para calcular o máximo divisor comum decorre da exatidão da igualdade
gcd(m,n)=gcd(n, mmodn)(que, por sua vez, precisa de uma prova; veja o Problema 7
nos Exercícios 1.1), a simples observação de que o segundo inteiro fica menor a cada
iteração do algoritmo e o fato de que o algoritmo para quando o segundo inteiro se
torna 0.
Para alguns algoritmos, uma prova de correção é bastante fácil; para outros, pode ser
bastante complexo. Uma técnica comum para provar a correção é usar a indução matemática
porque as iterações de um algoritmo fornecem uma sequência natural de etapas necessárias para
tais provas. Pode valer a pena mencionar que, embora rastrear o desempenho do algoritmo para
algumas entradas específicas possa ser uma atividade muito valiosa, não pode provar a correção
do algoritmo de forma conclusiva. Mas, para mostrar que um algoritmo está incorreto, você
precisa de apenas uma instância de sua entrada para a qual o algoritmo falha.

A noção de correção para algoritmos de aproximação é menos direta do que para


algoritmos exatos. Para um algoritmo de aproximação, geralmente gostaríamos de
poder mostrar que o erro produzido pelo algoritmo não excede um limite predefinido.
Você pode encontrar exemplos de tais investigações no Capítulo 12.
14 Introdução

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.

Ainda outra característica desejável de um algoritmo égeneralidade. Existem, de


fato, duas questões aqui: a generalidade do problema que o algoritmo resolve e o
conjunto de entradas que ele aceita. Sobre a primeira questão, observe que às vezes é
mais fácil projetar um algoritmo para um problema colocado em termos mais gerais.
Considere, por exemplo, o problema de determinar se dois inteiros são relativamente
primos, ou seja, se seu único divisor comum é igual a 1. É mais fácil projetar um
algoritmo para um problema mais geral de calcular o máximo divisor comum de dois
inteiros e , para resolver o problema anterior, verifique se o mdc é 1 ou não. Há
situações, no entanto, em que projetar um algoritmo mais geral é desnecessário, difícil
ou mesmo impossível. Por exemplo, não é necessário ordenar uma lista dennúmeros
para encontrar sua mediana, que é suan/2º menor elemento. Para dar outro exemplo,
a fórmula padrão para raízes de uma equação quadrática não pode ser generalizada
para lidar com polinômios de graus arbitrários.
Quanto ao conjunto de entradas, sua principal preocupação deve ser projetar um algoritmo
que possa lidar com um conjunto de entradas que seja natural para o problema em questão. Por
exemplo, excluir números inteiros iguais a 1 como possíveis entradas para um algoritmo de
máximo divisor comum seria bastante artificial. Por outro lado, embora a fórmula padrão para as
raízes de uma equação quadrática seja válida para coeficientes complexos, normalmente não a
implementaríamos nesse nível de generalidade, a menos que essa capacidade seja explicitamente
exigida.
Se você não estiver satisfeito com a eficiência, simplicidade ou generalidade do
algoritmo, deverá retornar à prancheta e redesenhar o algoritmo. De fato, mesmo que sua
avaliação seja positiva, ainda assim vale a pena buscar outras soluções algorítmicas.
Lembre-se dos três algoritmos diferentes da seção anterior para calcular o máximo divisor
comum: geralmente, você não deve esperar obter o melhor algoritmo na primeira tentativa.
No mínimo, você deve tentar ajustar o algoritmo que você
1.2Fundamentos da resolução algorítmica de problemas 15

já tem. Por exemplo, fizemos várias melhorias em nossa implementação da


peneira de Eratóstenes em comparação com seu esboço inicial na Seção 1.1. (Você
pode identificá-los?) Você se sairá bem se tiver em mente a seguinte observação
de Antoine de Saint-Exupéry, escritor, piloto e projetista de aeronaves francês:
“Um projetista sabe que chegou à perfeição, não quando não há nada mais a
acrescentar, mas quando não há mais nada a retirar”.1

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

Um programa de trabalho oferece uma oportunidade adicional ao permitir uma


análise empírica do algoritmo subjacente. Essa análise é baseada em cronometrar o
programa em várias entradas e, em seguida, analisar os resultados obtidos.
Discutimos as vantagens e desvantagens dessa abordagem para analisar algoritmos
na Seção 2.6.
Em conclusão, vamos enfatizar novamente a principal lição do processo representado
na Figura 1.2:

Como regra, um bom algoritmo é resultado de esforço repetido e retrabalho.

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

8.Dê um exemplo de um problema diferente de calcular o máximo divisor


comum para o qual você conhece mais de um algoritmo. Qual deles é mais
simples? Qual é mais eficiente?
9.Considere o seguinte algoritmo para encontrar a distância entre os dois elementos mais
próximos em uma matriz de números.

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

Faça o máximo de melhorias possível nesta solução algorítmica para o problema.


Se necessário, você pode alterar o algoritmo completamente; caso contrário,
melhore a implementação dada.
10.Um dos livros mais influentes sobre resolução de problemas, intituladoComo
resolver[Pol57], foi escrito pelo matemático húngaro-americano George Pólya
(1887–1985). Pólya resumiu suas ideias em um resumo de quatro pontos. Encontre
esse resumo na Internet ou, melhor ainda, em seu livro e compare-o com o plano
delineado na Seção 1.2. O que eles têm em comum? Como eles são diferentes?

1.3Tipos de problemas importantes

No mar ilimitado de problemas encontrados na computação, algumas áreas têm


atraído atenção especial dos pesquisadores. Em geral, seu interesse tem sido
impulsionado pela importância prática do problema ou por algumas características
específicas que o tornam um assunto de pesquisa interessante; felizmente, essas duas
forças motivadoras se reforçam mutuamente na maioria dos casos.
Nesta seção, vamos apresentar os tipos de problemas mais importantes:
Ordenação

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

Oproblema de classificaçãoé reorganizar os itens de uma determinada lista em ordem não


decrescente. Obviamente, para que esse problema seja significativo, a natureza dos itens da lista
deve permitir tal ordenação. (Os matemáticos diriam que deve existir uma relação de ordenação
total.) Na prática, geralmente precisamos classificar listas de números, caracteres de um alfabeto,
cadeias de caracteres e, mais importante, registros semelhantes aos mantidos pelas escolas sobre
seus alunos, bibliotecas sobre suas participações e empresas sobre seus funcionários. No caso de
registros, precisamos escolher uma informação para guiar a classificação. Por exemplo, podemos
optar por classificar os registros dos alunos em ordem alfabética de nomes ou pelo número do
aluno ou pela média de notas do aluno. Essa informação especialmente escolhida é chamada de
chave. Os cientistas da computação costumam falar sobre a classificação de uma lista de chaves,
mesmo quando os itens da lista não são registros, mas, digamos, apenas números inteiros.

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.

A segunda característica notável de um algoritmo de classificação é a quantidade de


memória extra que o algoritmo requer. Diz-se que um algoritmo éno lugarse não requer
memória extra, exceto, possivelmente, por algumas unidades de memória. Existem
algoritmos de classificação importantes que estão no local e aqueles que não estão.

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

Um problema específico – o de buscar uma determinada palavra em um texto – tem


atraído atenção especial dos pesquisadores. Eles chamam issocorrespondência de string.
Vários algoritmos que exploram a natureza especial desse tipo de pesquisa foram
inventados. Apresentamos um algoritmo muito simples no Capítulo 3 e discutimos dois
algoritmos baseados em uma ideia notável de R. Boyer e J. Moore no Capítulo 7.

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

De um modo geral, os problemas combinatórios são os problemas mais difíceis da


computação, tanto do ponto de vista teórico quanto prático. Sua dificuldade decorre dos seguintes
fatos. Primeiro, o número de objetos combinatórios normalmente cresce extremamente rápido
com o tamanho do problema, atingindo magnitudes inimagináveis mesmo para instâncias de
tamanho moderado. Em segundo lugar, não há algoritmos conhecidos para resolver a maioria
desses problemas exatamente em um período de tempo aceitável. Além disso, a maioria dos
cientistas da computação acredita que tais algoritmos não existem. Essa conjectura não foi
provada nem refutada e continua sendo a questão não resolvida mais importante na ciência da
computação teórica. Discutimos esse tópico com mais detalhes na Seção 11.3.

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

4.Pontes KönigsbergO quebra-cabeça da ponte de Königsberg é universalmente


aceito como o problema que deu origem à teoria dos grafos. Foi resolvido pelo
grande matemático suíço Leonhard Euler (1707-1783). O problema questionava se
seria possível, em um único passeio, cruzar todas as sete pontes da cidade de
Königsberg exatamente uma vez e retornar ao ponto de partida. A seguir, um
esboço do rio com suas duas ilhas e sete pontes:

a.Enuncie o problema como um problema de gráfico.

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:

Encontre umcircuito hamiltoniano—um caminho que visita todos os vértices do grafo


exatamente uma vez antes de retornar ao vértice inicial—para este grafo.

6.Considere o seguinte problema: Projete um algoritmo para determinar a melhor rota


para um passageiro do metrô tomar de uma estação designada para outra em um
sistema de metrô bem desenvolvido semelhante aos de cidades como Washington, DC
e Londres, Reino Unido.
1.4Estruturas de dados fundamentais 25

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?

7. a.Reformule o problema do caixeiro-viajante em termos de objetos combinatórios.


b.Reformule o problema de coloração de grafos em termos de objetos combinatórios.

8.Considere o seguinte mapa:

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.

1.4Estruturas de dados fundamentais


Como a grande maioria dos algoritmos de interesse opera em dados, formas particulares de organização
de dados desempenham um papel crítico no projeto e na análise de algoritmos. Aestrutura de dados
pode ser definido como um esquema particular de organização de itens de dados relacionados. A
natureza dos itens de dados é ditada pelo problema em questão; eles podem variar de tipos de dados
elementares (por exemplo, números inteiros ou caracteres) a estruturas de dados (por exemplo, uma
matriz unidimensional de matrizes unidimensionais é frequentemente usada para implementar matrizes).
Existem algumas estruturas de dados que provaram ser particularmente importantes para algoritmos de
computador. Como você está, sem dúvida, familiarizado com a maioria, senão todos eles, apenas uma
rápida revisão é fornecida aqui.

Estruturas de Dados Lineares

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

são armazenados de forma contígua na memória do computador e acessíveis pela especificação


de um valor do arrayíndice(Figura 1.3).
Na maioria dos casos, o índice é um número inteiro entre 0 en−1 (como mostrado na
Figura 1.3) ou entre 1 en.Algumas linguagens de computador permitem que um índice de
matriz varie entre quaisquer dois limites inteirosbaixoealto, e alguns até permitem índices
não numéricos para especificar, por exemplo, itens de dados correspondentes aos 12 meses
do ano pelos nomes dos meses.
Todo e qualquer elemento de uma matriz pode ser acessado na mesma quantidade
constante de tempo, independentemente de onde o elemento em questão esteja localizado na
matriz. Esse recurso distingue positivamente os arrays das listas encadeadas, discutidas abaixo.
Arrays são usados para implementar uma variedade de outras estruturas de dados.
Destaque entre eles é ocorda, uma sequência de caracteres de um alfabeto terminado por
um caractere especial que indica o fim da string. Strings compostas de zeros e uns são
chamadascadeias bináriasoucadeias de bits. Strings são indispensáveis para processar
dados textuais, definir linguagens de computador e compilar programas nelas escritos e
estudar modelos computacionais abstratos. As operações que geralmente realizamos em
strings diferem daquelas que normalmente realizamos em outras matrizes (digamos,
matrizes de números). Eles incluem calcular o comprimento da string, comparar duas
strings para determinar qual delas precede a outra emlexicográfico(ou seja, alfabética)
ordem, e concatenando duas strings (formando uma string a partir de duas strings
fornecidas anexando a segunda ao final da primeira).
Alista encadeadaé uma sequência de zero ou mais elementos chamadanós, cada um
contendo dois tipos de informação: alguns dados e um ou mais links chamados ponteiros
para outros nós da lista encadeada. (Um ponteiro especial chamado “nulo” é usado para
indicar a ausência do sucessor de um nó.)lista encadeada individualmente, cada nó,
exceto o último, contém um único ponteiro para o próximo elemento (Figura 1.4).
Para acessar um nó específico de uma lista encadeada, começa-se com o primeiro nó da lista e
percorre-se a cadeia de ponteiros até que o nó específico seja alcançado. Assim, o tempo necessário para
acessar um elemento de uma lista encadeada individualmente, ao contrário de um array, depende de
onde o elemento está localizado na lista. Do lado positivo, as listas encadeadas não

Item[0] Item[1] Item[n–1]

FIGURA 1.3Matriz denelementos.

Item0 Item1 Item n–1 nulo

FIGURA 1.4Lista ligada individualmente denelementos.


1.4Estruturas de dados fundamentais 27

nulo Item0 Item1 Item n–1 nulo

FIGURA 1.5Lista duplamente ligada denelementos.

não requer nenhuma reserva preliminar da memória do computador, e as inserções e exclusões


podem ser feitas de forma bastante eficiente em uma lista encadeada, reconectando alguns
ponteiros apropriados.
Podemos explorar a flexibilidade da estrutura da lista encadeada de várias maneiras. Por
exemplo, muitas vezes é conveniente iniciar uma lista encadeada com um nó especial chamado nó
cabeçalho. Esse nó pode conter informações sobre a própria lista encadeada, como seu
comprimento atual; ele também pode conter, além de um ponteiro para o primeiro elemento, um
ponteiro para o último elemento da lista encadeada.
Outra extensão é a estrutura chamadalista duplamente ligada, em que cada nó,
exceto o primeiro e o último, contém ponteiros tanto para seu sucessor quanto para seu
predecessor (Figura 1.5).
A matriz e a lista encadeada são duas escolhas principais na representação de uma estrutura
de dados mais abstrata chamada lista linear ou simplesmente lista. Alistaé uma sequência finita
de itens de dados, ou seja, uma coleção de itens de dados dispostos em uma determinada ordem
linear. As operações básicas executadas nessa estrutura de dados são procurar, inserir e excluir
um elemento.
Dois tipos especiais de listas, pilhas e filas, são particularmente importantes. A pilhaé uma
lista na qual as inserções e exclusões podem ser feitas apenas no final. Esta extremidade é
chamada deprincipalporque uma pilha geralmente é visualizada não horizontalmente, mas
verticalmente – semelhante a uma pilha de pratos cujas “operações” ela imita muito de perto.
Como resultado, quando os elementos são adicionados (empurrados para) a uma pilha e
deletados (removidos), a estrutura opera no modo “último a entrar, primeiro a sair” (LIFO),
exatamente como uma pilha de pratos, se pudermos adicionar ou remover uma placa apenas a
partir do topo. As pilhas têm uma infinidade de aplicações; em particular, eles são indispensáveis
para implementar algoritmos recursivos.
Afila, por outro lado, é uma lista da qual os elementos são excluídos de uma extremidade da
estrutura, chamada defrente(esta operação é chamadadesenfileirar), e novos elementos são
adicionados na outra ponta, chamada detraseira(esta operação é chamadaenfileirar).
Conseqüentemente, uma fila funciona do modo “primeiro a entrar, primeiro a sair” (FIFO),
semelhante a uma fila de clientes atendidos por um único caixa em um banco. As filas também
têm muitas aplicações importantes, incluindo vários algoritmos para problemas de grafos.

Muitas aplicações importantes requerem a seleção de um item de maior prioridade entre um


conjunto de candidatos que mudam dinamicamente. Uma estrutura de dados que busca satisfazer
as necessidades de tais aplicações é chamada de fila de prioridade. AFila de prioridadeé uma
coleção de itens de dados de um universo totalmente ordenado (na maioria das vezes,
28 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.

Normalmente é conveniente rotular os vértices de um grafo ou de um dígrafo com letras,


números inteiros ou, se uma aplicação exigir isso, cadeias de caracteres (Figura 1.6). O grafo
representado na Figura 1.6a tem seis vértices e sete arestas não direcionadas:

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)

FIGURA 1.6(a) Grafo não direcionado. (b) Dígrafo.


1.4Estruturas de dados fundamentais 29

Nossa definição de grafo não proíberotações, ou arestas conectando vértices a si


mesmos. A menos que seja explicitamente declarado o contrário, consideraremos grafos
sem loops. Como nossa definição não permite arestas múltiplas entre os mesmos vértices
de um grafo não direcionado, temos a seguinte desigualdade para o número de arestas |E|
possível em um grafo não direcionado com |V|vértices e sem loops:

0≤ |E| ≤ |V|(|V| −1)/2.

(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.

Representações gráficasOs gráficos para algoritmos de computador geralmente


são representados de duas maneiras: a matriz de adjacência e as listas de adjacência.
Omatriz de adjacênciade um gráfico comnvértices é umn×nmatriz booleana com uma
linha e uma coluna para cada um dos vértices do grafo, na qual o elemento doeuª linha
e aja coluna é igual a 1 se houver uma aresta daeuo vértice para ojvértice, e igual a 0 se
não houver tal aresta. Por exemplo, a matriz de adjacência para o grafo da Figura 1.6a
é dada na Figura 1.7a.
Observe que a matriz de adjacência de um grafo não direcionado é sempre simétrica,
isto é,A[eu j] =A[j, eu] para cada 0≤eu j≤n−1 (por quê?).
Olistas de adjacênciade um grafo ou um dígrafo é uma coleção de listas encadeadas, uma
para cada vértice, que contém todos os vértices adjacentes ao vértice da lista (ou seja, todos os
vértices conectados a ele por uma aresta). Normalmente, essas listas começam com um cabeçalho
que identifica um vértice para o qual a lista é compilada. Por exemplo, a Figura 1.7b representa o
grafo da Figura 1.6a por meio de suas listas de adjacência. Em outras palavras,

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).

Gráficos PonderadosAgráfico ponderado(ou dígrafo ponderado) é um gráfico (ou


dígrafo) com números atribuídos às suas arestas. Esses números são chamadospesosou
custos. O interesse em tais gráficos é motivado por inúmeras aplicações do mundo real,
como encontrar o caminho mais curto entre dois pontos em uma rede de transporte ou
comunicação ou o problema do caixeiro viajante mencionado anteriormente.
Ambas as representações principais de um gráfico podem ser facilmente adotadas para
acomodar gráficos ponderados. Se um grafo ponderado é representado por sua matriz de
adjacência, então seu elementoA[eu j]simplesmente conterá o peso da aresta doeuº para oj
vértice se houver tal aresta e um símbolo especial, por exemplo,∞,se não houver tal aresta.
Tal matriz é chamada dematriz de pesooumatriz de custo. Essa abordagem é ilustrada na
Figura 1.8b para o gráfico ponderado na Figura 1.8a. (Para algumas aplicações, é mais
conveniente colocar 0's na diagonal principal da matriz de adjacência.) As listas de
adjacência para um grafo ponderado devem incluir em seus nós não apenas o nome de um
vértice adjacente, mas também o peso da aresta correspondente (Figura 1.8c).

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

(a) (b) (c)

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

FIGURA 1.9Gráfico que não está conectado.

No caso de um grafo direcionado, geralmente estamos interessados em caminhos


direcionados. Acaminho direcionadoé uma sequência de vértices na qual cada par consecutivo de
vértices é conectado por uma aresta direcionada do vértice listado primeiro para o vértice listado a
seguir. Por exemplo,a, c, e, fé um caminho direcionado deaparafno gráfico da Figura 1.6b.

Diz-se que um gráfico éconectadose para cada par de seus vérticesvocêevexiste um


caminho devocêparav.Se fizermos um modelo de um grafo conectado conectando algumas
bolas que representam os vértices do grafo com cordas que representam as arestas, será
uma peça única. Se um grafo não for conectado, tal modelo consistirá em várias partes
conectadas que são chamadas de componentes conectados do grafo. Formalmente, um
componente conectadoé um subgrafo conectado maximal (não expansível pela inclusão de
outro vértice e uma aresta)2de um determinado gráfico. Por exemplo, os gráficos das
Figuras 1.6a e 1.8a são conectados, enquanto o gráfico da Figura 1.9 não é, porque não há
caminho, por exemplo, deaparaf.O grafo da Figura 1.9 tem duas componentes conectadas
com vértices {a, b, c, d, e}e {f, g, h, eu}, respectivamente.

Gráficos com vários componentes conectados acontecem em aplicações do mundo real.


Um gráfico representando o sistema de rodovias interestaduais dos Estados Unidos seria
um exemplo (por quê?).
É importante saber para muitas aplicações se um grafo em consideração possui
ou não ciclos. Acicloé um caminho de comprimento positivo que começa e termina no
mesmo vértice e não atravessa a mesma aresta mais de uma vez. Por exemplo, f,h,eu,g
,fé um ciclo no gráfico da Figura 1.9. Um grafo sem ciclos é dito seracíclico. Discutimos
grafos acíclicos na próxima subseção.

á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)

FIGURA 1.10(uma árvore. (b) Floresta.

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:

|E| = |V| −1.

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

As árvores enraizadas desempenham um papel muito importante na ciência da computação,


muito mais importante do que as árvores livres; na verdade, por uma questão de brevidade, eles
são frequentemente referidos como simplesmente “árvores”. Uma aplicação óbvia das árvores é a
descrição de hierarquias, desde diretórios de arquivos até organogramas de empresas. Existem
muitos aplicativos menos óbvios, como a implementação de dicionários (veja abaixo), acesso
eficiente a conjuntos de dados muito grandes (Seção 7.4) e codificação de dados (Seção 9.4).
Conforme discutimos no Capítulo 2, as árvores também são úteis na análise de algoritmos
recursivos. Para finalizar esta lista longe de completa de aplicações de árvores, devemos
mencionar os chamadosárvores de espaço de estadoque destacam duas importantes técnicas de
projeto de algoritmos: backtracking e branch-and-bound (Seções 12.1 e 12.2).
Para qualquer vérticevem uma árvoreT,todos os vértices no caminho simples da
raiz para esse vértice são chamadosantepassadosdev.O próprio vértice geralmente é
considerado seu próprio ancestral; o conjunto de ancestrais que exclui o próprio
vértice é referido como o conjunto deancestrais adequados. Se(você, v)é a última
aresta do caminho simples da raiz ao vérticev(evocê-=v),vocêé dito ser opaidevevé
chamado decriança devocê; vértices que têm o mesmo pai são ditosirmãos. Um
vértice sem filhos é chamado defolha;um vértice com pelo menos um filho é chamado
parental. Todos os vértices para os quais um vérticevé um ancestral dizem ser
descendentesdev; o descendentes adequadosexcluir o vérticevem si. Todos os
descendentes de um vérticev com todas as arestas que os conectam formam o
subárvoredeTenraizada naquele vértice. Assim, para a árvore da Figura 1.11b, a raiz
da árvore éa; vérticesd, g, f, h, eeu são folhas e vérticesa, b, e, ecsão parentais; o pai de
béa; os filhos debsãoceg; os irmãos debsãodee; e os vértices da subárvore com raiz em
bsão {b, c, g, h, eu}.
Oprofundidadede um vérticevé o comprimento do caminho simples desde a raiz atév. O
alturade uma árvore é o comprimento do caminho simples mais longo desde a raiz até uma folha.
Por exemplo, a profundidade do vérticecda árvore na Figura 1.11b é 2, e a altura da árvore é 3.
Assim, se contarmos os níveis da árvore de cima para baixo começando com 0 para o nível da raiz,
a profundidade de um vértice é simplesmente seu nível na árvore, e a altura da árvore é o nível
máximo de seus vértices. (Você deve estar alerta para o fato de que alguns autores definem a
altura de uma árvore como o número de níveis nela; isso torna a altura de uma árvore maior em 1
do que a altura definida como o comprimento do caminho simples mais longo desde a raiz a uma
folha.)

Á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)

FIGURA 1.12(a) Árvore binária. (b) Árvore de busca binária.

5 12 nulo

nulo 1 nulo 7 nulo nulo 10 nulo

nulo 4 nulo

FIGURA 1.13Implementação padrão da árvore de busca binária na Figura 1.12b.

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

c nulo g nulo nulo f nulo c d

nulo h nulo eu nulo h g e

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.

Os conjuntos podem ser implementados em aplicativos de computador de duas maneiras. A


primeira considera apenas conjuntos que são subconjuntos de algum conjunto grandeVOCÊ,Chamou o
Conjunto universal. Se definidovocêtemnelementos, então qualquer subconjuntoSdevocêpode ser
representado por uma cadeia de bits de tamanhon,chamado devetor de bits, em que oeuo elemento é 1
se e somente se oeuº elemento devocêestá incluído no conjuntoS.Assim, para continuar com nosso
exemplo, se você= {1,2,3,4,5,6,7,8,9},entãoS= {2,3,5, 7} é representado pela cadeia de bits 011010100.
Essa forma de representar conjuntos torna possível implementar as operações de conjunto padrão muito
rapidamente, mas às custas do uso potencial de uma grande quantidade de armazenamento.

A segunda e mais comum maneira de representar um conjunto para fins de computação é


usar a estrutura de lista para indicar os elementos do conjunto. Claro, esta opção também é viável
apenas para conjuntos finitos; felizmente, ao contrário da matemática, esse é o tipo de conjunto
de que a maioria dos aplicativos de computador precisa. Observe, no entanto, os dois principais
pontos de distinção entre conjuntos e listas. Primeiro, um conjunto não pode conter elementos
idênticos; uma lista pode. Este requisito de exclusividade é às vezes contornado pela introdução de
umconjunto múltiplo, oubolsa, uma coleção não ordenada de itens que não são
necessariamente distintos. Em segundo lugar, um conjunto é uma coleção não ordenada de itens;
portanto, alterar a ordem de seus elementos não altera o conjunto. Uma lista, definida como uma
coleção ordenada de itens, é exatamente o oposto. Esta é uma distinção teórica importante, mas
felizmente não é importante para muitas aplicações. Vale ressaltar também que, se um conjunto
for representado por uma lista, dependendo da aplicação em questão, pode ser interessante
manter a lista em ordem ordenada.
Na computação, as operações que precisamos executar para um conjunto ou
multiconjunto geralmente são procurar um determinado item, adicionar um novo item
e excluir um item da coleção. Uma estrutura de dados que implementa essas três
operações é chamada dedicionário. Observe a relação entre essa estrutura de dados e
o problema de busca mencionado na Seção 1.3; obviamente, estamos lidando aqui
com a busca em um contexto dinâmico. Conseqüentemente, uma implementação
eficiente de um dicionário deve estabelecer um compromisso entre a eficiência da
busca e a eficiência das outras duas operações. Existem algumas maneiras de
implementar um dicionário. Eles variam desde o uso não sofisticado de arrays
(ordenados ou não) até técnicas muito mais sofisticadas, como hashing e árvores de
busca balanceadas, que discutiremos mais adiante neste livro.
Uma série de aplicações em computação requerem uma partição dinâmica de alguns n
-elemento definido em uma coleção de subconjuntos disjuntos. Após ser inicializado como
uma coleção densubconjuntos de um elemento, a coleção é submetida a uma sequência de
operações de união e busca misturadas. Este problema é chamado dedefinir problema
sindical. Discutimos soluções algorítmicas eficientes para esse problema na Seção 9.2,
juntamente com uma de suas aplicações importantes.
1.4Estruturas de dados fundamentais 37

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.

b.listas representadas como listas encadeadas.

3. a.Mostre a pilha após cada operação da seguinte sequência que começa


com a pilha vazia:
push(a), push(b), pop, push(c), push(d), pop

b.Mostre a fila após cada operação da seguinte sequência que começa


com a fila vazia:
enfileirar(a), enfileirar(b), desenfileirar, enfileirar(c), enfileirar(d), desenfileirar

4. a.DeixarAser a matriz de adjacência de um grafo não direcionado. Explique qual


propriedade da matriz indica que
eu.o gráfico está completo.
ii.o grafo tem um loop, ou seja, uma aresta conectando um vértice a ele mesmo.
iii.o grafo tem um vértice isolado, ou seja, um vértice sem arestas incidentes a ele.

b.Responda às mesmas perguntas para a representação da lista de adjacências.

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

6.Prove as desigualdades que suportam a altura de uma árvore binária comnvértices:

registro2n≤h≤n−1.

7.Indique como a fila de prioridade ADT pode ser implementada como


a.uma matriz (não classificada).

b.uma matriz ordenada.

c.uma árvore de busca binária.

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

10.verificação de anagramasProjete um algoritmo para verificar se duas palavras


dadas são anagramas, ou seja, se uma palavra pode ser obtida permutando as
letras da outra. Por exemplo, as palavrascháecomersão anagramas.

RESUMO

Umalgoritmoé uma sequência de instruções não ambíguas para resolver um problema em


uma quantidade finita de tempo. Uma entrada para um algoritmo especifica um instância
do problema que o algoritmo resolve.

Os algoritmos podem ser especificados em uma linguagem natural ou pseudocódigo; eles também podem
ser implementados como programas de computador.

Entre várias maneiras de classificar algoritmos, as duas principais alternativas são:


. agrupar algoritmos de acordo com os tipos de problemas que eles resolvem
. agrupar algoritmos de acordo com as técnicas de design subjacentes nas quais eles se
baseiam

Os tipos de problemas importantes são ordenação, busca, processamento de strings,


problemas gráficos, problemas combinatórios, problemas geométricos e problemas
numéricos.

Algoritmotécnicas de design(ou “estratégias” ou “paradigmas”) são abordagens gerais


para resolver problemas algoritmicamente, aplicáveis a uma variedade de
problemas de diferentes áreas da computação.
Resumo 39

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.

Um bom algoritmo geralmente é o resultado de esforços repetidos e retrabalho.

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.

Algoritmos operam em dados. Isso torna a questão da estruturação de dados crítica


para a solução eficiente de problemas algorítmicos. As estruturas de dados
elementares mais importantes são asvariedadee alista encadeada. Eles são usados
para representar estruturas de dados mais abstratas, como olista, opilha, ofila, ográfico
(via seumatriz de adjacênciaoulistas de adjacência), oárvore binária,e adefinir.

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

Você também pode gostar