Algoritmos e Estruturas de Dados

Transcrição

Algoritmos e Estruturas de Dados
�
�
�
MATEUS�CONRAD�BARCELLOS�DA�COSTA�
Algoritmos e Estruturas de
Dados
M.�B.�Costa���Algoritmos�e�Estruturas�de�Dados
[ Serra, ES ]
[ 2008 ]
2
M.�B.�Costa���Algoritmos�e�Estruturas�de�Dados
Referências utilizadas na elaboração deste material
1. LISKOV B. Data Abstraction and Hiararchy. In OOPSLA’87:
Conference on Object Oriented Programming Systems Languages and
Applications. Addendum to the proceedings on Object-oriented
programming systems, languages and applications, 1987.
2. TENENBAUM A. M. Data Structs using C. Prentice Hall Int.
Editions, 1990.
3. ZIVIANI N. Projeto de Algoritmos. Segunda Edição. Pioneira, 2003.
3
M.�B.�Costa���Algoritmos�e�Estruturas�de�Dados
Meu nome é Mateus Barcellos Costa, sou professor e pesquisador do Ifes
desde 2005. Atuo na área de Engenharia de Software e em disciplinas de
Programação. Se você quiser mais informações sobre mim e sobre os
trabalhos que desenvolvo, pode
visitar minha página pessoal em
http://www.sr.cefetes.br/~mcosta.
Produzi o material que ora lhe apresento como instrumento básico para o
estudo da disciplina de Técnicas de Programação Avançada. Nesta disciplina
iremos aprofundar nossos conhecimentos em Programação de Computadores
usando uma linguagem imperativa. Exemplos destas linguagens são C e
Pascal. Iremos adotar a linguagem C, em nossos exemplos e
implementações. No entanto, é preciso que você saiba que os conceitos
estudados aqui vão além da linguagem e podem ser aplicados em diversos
cenários, na programação e na Engenharia de Software como um todo.
4
M.�B.�Costa���Algoritmos�e�Estruturas�de�Dados
Sumário
1�
2�
Abstração de dados ...................................................................................... 7�
1.1�
Introdução ............................................................................................ 7�
1.2�
Conceitos de abstração de dados .......................................................... 9�
1.2.1�
Abstração em Computação........................................................... 9�
1.2.2�
Abstração de Procedimentos ...................................................... 11�
1.2.3�
Tipos Abstratos de Dados .......................................................... 13�
1.2.4�
Implementação de Tipos Abstratos de Dados ............................ 19�
1.2.5�
Avaliação de Implementações de Tipos Abstratos de Dados ..... 20�
Tipos Abstratos de Dados Fundamentais ................................................... 25�
2.1�
2.1.1�
Especificação do TAD Pilha ...................................................... 26�
2.1.2�
Implementação de Pilhas em Arranjos ....................................... 28�
2.2�
4�
Filas .................................................................................................... 31�
2.2.1�
Especificação do TAD FILA...................................................... 32�
2.2.2�
Implementação de Filas em arranjos com deslocamento ........... 35�
2.2.3�
Implementação de Filas com Arranjos circulares ...................... 37�
2.3�
3�
Pilhas .................................................................................................. 25�
Implementação de TADS com alocação dinâmica de memória ......... 40�
2.3.1�
Revisão de Alocação Dinâmica de Memória ............................. 40�
2.3.2�
Implementação do TAD Pilha .................................................... 47�
2.3.3�
Implementação do TAD Fila ...................................................... 50�
Listas e Árvores.......................................................................................... 55�
3.1�
Listas Circulares ................................................................................. 55�
3.2�
Lista Circular Duplamente encadeada ................................................ 64�
3.3�
Árvores Binárias................................................................................. 69�
3.3.1�
Árvore Binária de Pesquisa ........................................................ 70�
3.3.2�
O TAD Árvore Binária de Pesquisa ........................................... 71�
3.3.3�
Implementação do TAD árvore Binária de Pesquisa ................. 72�
Pesquisa em Memória Primária.................................................................. 83�
4.1�
Pesquisa Sequencial ........................................................................... 85�
4.1.1�
Implementação da Pesquisa Seqüencial ..................................... 86�
4.1.2�
Tempo de execução de algoritmos ............................................. 87�
4.2�
Pesquisa Binária ................................................................................. 89�
4.3�
Tabelas Hash ...................................................................................... 92�
4.3.1�
Operações de Inserção e Pesquisa em Tabelas Hash................. 94�
5
M.�B.�Costa���Algoritmos�e�Estruturas�de�Dados
5�
4.3.2�
Tratamento de Colisões .............................................................. 96�
4.3.3�
Tratamento de Colisão usando endereçamento Aberto .............. 98�
Ordenação em Memória Primária ............................................................ 100�
5.1�
Conceitos Básicos de ordenação ...................................................... 100�
5.1.1�
5.2�
Operações Típicas de processos de Ordenação ........................ 100�
Métodos de Ordenação ..................................................................... 101�
5.2.1�
Ordenação por Seleção ............................................................. 101�
5.2.2�
Método da Inserção .................................................................. 103�
5.2.3�
Método da Bolha ...................................................................... 104�
5.2.4�
Desempenho dos métodos de Seleção, Inserção e Bolha ........ 105�
5.2.5�
Método de Shell ....................................................................... 106�
5.2.6�
O Método Quicksort ................................................................. 109�
6
M.�B.�Costa���Algoritmos�e�Estruturas�de�Dados
1
ABSTRAÇÃO DE DADOS
Olá! Neste Capítulo iremos discutir e aprender sobre Abstração de Dados.
Abstração de Dados é uma técnica de programação que visa simplificar o
desenvolvimento de programas. Sua aplicação pode se dar no
desenvolvimento de programas menores. Mas podemos afirmar que seria
impossível o desenvolvimento de sistemas que temos hoje, com milhões de
linhas de código, sem o uso de abstração de dados.
1.1 INTRODUÇÃO
Um programa de computador desenvolvido para atender alguma finalidade
prática é, normalmente, um artefato complexo. Por esse motivo, a atividade de
desenvolvimento desses artefatos, a programação de computadores, está entre as
atividades mais complexas desempenhadas pelo homem. Se você cursou
disciplinas introdutórias de programação, pode estar se questionando: � Ora,
desenvolver um programa não é tão complexo assim! Basta compreender o problema,
suas entradas e suas saídas e construir a solução usando uma linguagem de
programação. Simples não? Não! A história da programação tem dado provas
que programar não é tão simples assim.
Figura 1.1: O Gap Semântico.
Programar é uma atividade complexa por diversos aspectos, tanto de cunho
teórico quanto prático. Dentre estes aspectos destacamos os seguintes:
1. Programar um computador significa desenvolver programas de
computadores (formais e precisos) para atender a finalidades práticas
definidas em termos de conceitos do mundo real (informais e
imprecisos). Existe um abismo entre o mundo dos problemas reais e o
mundo das soluções. Esse abismo é chamado na computação de gap
semântico. Transpor o abismo é um dos desafios centrais da
programação de computadores e da Engenharia de Software como um
7
M.�B.�Costa���Algoritmos�e�Estruturas�de�Dados
todo. A Figura 1.1 é uma alegoria que busca mostrar a função do
desenvolvedor de software: Transpor o abismo entre o mundo informal
(dos problemas) e o mundo formal (das soluções computacionais).
Nessa transposição existem muitos desafios e perigos que podem
dificultar a trajetória do desenvolvedor.
2. Muitas vezes, em disciplinas iniciais de programação, deparamo-nos
com o desenvolvimento de programas mais simples, de cunho didático.
Por exemplo, programas para calcular médias ou somatórios. Em outros
momentos fazemos programas para aprender a usar um certo
mecanismo, por exemplo, apontadores. Aqui, estamos nos referindo a
programas de computadores para ajudar pessoas a resolverem problemas
do mundo real, problemas grandes e complexos! Exemplos desses
problemas incluem:
a. Gerenciar as operações financeiras de uma empresa;
b. Controlar uma aeronave;
c. Controlar os trens de uma malha ferroviária;
d. Produzir edições diárias de um jornal;
e. Gerenciar o processo de produção de um filme.
3. Problemas como esses não são simples de se resolver.
Conseqüentemente, programas voltados para essas finalidades são
complexos, levam muito tempo para ficar prontos, têm de ser
desenvolvidos por uma equipe de pessoas, utilizando diversas
tecnologias e seguindo um processo de desenvolvimento sistemático.
4. Para atender às funcionalidades esperadas, um programa deve
apresentar um conjunto de características que juntas vão tornar o
programa efetivamente útil e determinarão a qualidade do mesmo.
Essas características são as seguintes:
a. Um programa deve estar correto, livre de erros;
b. Um programa deve ser robusto. Um programa robusto ou
sistema robusto é aquele que consegue funcionar, mesmo que
precariamente, diante de uma adversidade. Por exemplo,
suponha que um programa precise dos dados x, y e z para
realizar uma tarefa. Se este for robusto, na falta de um dos
dados, o programa pode tentar realizar o processamento possível
com a ausência dado;
c. Um programa deve ser eficiente. A eficiência de um programa
está relacionada ao seu tempo de execução (eficiência na
execução) ou ao espaço em memória (eficiência na ocupação)
de que necessita para executar (área de dados). Para um
problema há infinitas soluções (programas). Quanto menores
esses valores mais eficiente o programa. Em computação podese verificar se uma solução é ótima (mais eficiente possível)
para um problema;
d. Um programa deve ser compatível com outros programas;
e. Um programa deve ser fácil de usar;
8
M.�B.�Costa���Algoritmos�e�Estruturas�de�Dados
f.
Um programa deve ser portável, podendo funcionar em
diferentes plataformas ou sistemas operacionais;
g. Um programa deve ser íntegro, ou seja, deve evitar que os dados
sejam corrompidos ou violados;
h. O processamento realizado pelo programa deve ser verificável;
i.
O programa ou partes dele devem poder ser utilizados em outros
cenários diferentes daquele para o qual o programa foi
originalmente desenvolvido.
5. Por último, devemos considerar a atividade de programação como
atividade econômica. Assim como outras atividades econômicas, o
desenvolvimento de software é regido por leis de mercado que impõem
severas exigências aos desenvolvedores. De acordo com essas leis, não
basta apenas desenvolver um programa que atenda à finalidade
esperada. Esses programas devem ser desenvolvidos dentro dos prazos
e custos estabelecidos. Além disso, o programa precisa ter outras
características importantes que permitam a sua evolução. Essas
características são chamadas de fatores internos. São eles:
a. Facilidade de manutenção;
b. Facilidade de evolução;
c. Fácililade de entendimento;
d. Modularidade.
Pelos motivos discutidos acima, o desenvolvimento de programas requer a
aplicação de princípios, métodos e técnicas que diminuam a complexidade desse
desenvolvimento. A Abstração de Dados envolve uma série de conceitos e
princípios para esse fim. A seguir discutiremos esses conceitos.
Atividade 1
Nesta introdução foram levantados cinco aspectos que tornam o
desenvolvimento de software uma tarefa difícil. Para atacar esses
aspectos e tornar o desenvolvimento de software mais simples são
consideradas três dimensões: Processo de desenvolvimento, Pessoas
(partes interessadas: clientes, desenvolvedores) e Tecnologias de
desenvolvimento. Releia os cinco motivos e tente escrever um texto
resumido, estabelecendo uma relação entre esses 5 motivos e essas
três dimensões.
1.2 CONCEITOS DE ABSTRAÇÃO DE DADOS
1.2.1 Abstração em Computação
A abstração é um dos conceitos mais importantes da computação. Sem o uso
deste conceito podemos afirmar que a evolução apresentada pela computação
teria sido mais lenta.
Segundo o dicionário Michaelis, temos a seguinte definição para a palavra
Abstração:
9
M.�B.�Costa���Algoritmos�e�Estruturas�de�Dados
1. O ato ou efeito de abstrair. Consideração das qualidades independentemente
dos objetos a que pertencem. Abstrair: Considerar um dos caracteres de um
objeto separadamente;2. Excluir, prescindir de.
A finalidade principal de uso de abstração em qualquer área do conhecimento é
colocarmos foco em um subconjunto dos aspectos de um sistema ou processo
complexo. Considere, por exemplo, o processo de pintar um quadro. A Figura
1.2 mostra, à esquerda um esquema inicial mostrando as linhas principais do
quadro que se encontra do lado direito (A Virgem, o Menino e Sant’Ana,
Leonardo Da Vinci).
Figura 1.2: Abstração na Pintura.
Observe que, no desenho, as proporções, os detalhes das posturas e feições são
já determinados. Esse processo ajuda o pintor pois, no momento da idealização
do quadro, ele não precisa se preocupar com outros aspectos complexos, como
cores, nuances de sombras e reflexos, para definir a estrutura e a sua aparência
geral. Podemos dizer então que o desenho à esquerda é uma abstração do
quadro.
Em computação, abstração também possui finalidades semelhantes. Em
programação, especificamente, esse conceito está presente quase o tempo todo,
seja nas construções existentes nas linguagens, seja nos métodos de
programação empregados.
Uma das principais abstrações das linguagens de programação é o conceito de
variável. Uma variável é um conceito abstrato que esconde diversos aspectos
técnicos que não interessam ao programador. Quando declaramos uma variável
em um programa, essa declaração implica em “coisas” que não irão interferir no
seu desenvolvimento. Por exemplo, por trás de uma variável do tipo inteiro em
C, (ex. int x;), estão “escondidas” as características físicas do armazenamento da
variável em memória, a saber:
-
A forma de representação de números inteiros usando base 2 (por
exemplo, complemento de 2);
-
O padrão usado pelo hardware (ex. little endian, big endian);
-
O número de bytes que uma variável do tipo inteiro ocupa;
-
O endereço da variável na memória principal;
-
O conjunto de bits responsável por armazenar o sinal do número inteiro.
10
M.�B.�Costa���Algoritmos�e�Estruturas�de�Dados
Para o programador em C, geralmente, nenhuma dessas informações é
importante. O que o programador deseja é utilizar a variável realizando as
operações permitidas aos números inteiros (operações aritméticas e
comparações), atribuir, recuperar e modificar o valor contido na variável.
Assim podemos dizer que uma variável permite ao programador abstrair-se
de detalhes que não interessam e não irão influenciar no comportamento do
programa.
Atividade 2
Os itens 1,2 e 3 são todos abstrações. Para cada um deles
descreva os aspectos que estão sendo escondidos e os aspectos
que realmente importam ao programador:
1. O comando
if(){
..
}
else{
..
}
2. um arquivo
3. a função scanf
1.2.2 Abstração de Procedimentos
Podemos afirmar que a grande maioria dos elementos de linguagem utilizados
em uma linguagem de programação de alto nível são abstrações. Essas
abstrações facilitaram o desenvolvimento de programas mais complexos e
sofisticados, evitando que programadores precisassem manipular bits e
endereços de memória e interagir diretamente com o sistema operacional.
Uma outra abstração de programação importante é a Abstração de
Procedimento.
Abstração de Procedimento
A abstração de procedimento permite que o programador crie, ele mesmo, sua
“forma de abstrair”, utilizando os comandos disponíveis na linguagem. A
possibilidade de criar procedimentos permite ao programador criar
funcionalidades mais abstratas que “escondem” a seqüência de operações
necessária para a realização de uma tarefa complexa.
Por exemplo, sabemos que na linguagem C não existe um comando que seja
capaz de ordenar um vetor de inteiros em ordem crescente. Seria muito bom que
pudéssemos contar com esse comando, certo? Mas, como não temos esse
comando, iremos criar uma função (abstração de procedimento) que realize essa
tarefa para nós. A função ordena na Figura 1.3 é essa abstração.
11
M.�B.�Costa���Algoritmos�e�Estruturas�de�Dados
void ordena(int a[], int tamanho) {
int i, j, valor;
for(i = 1; i < tamanho; ++i) {
value = a[i];
for (j = i - 1; j >= 0 && a[j] > value; --j){
a[j + 1] = a[j];
}
a[j+1] = valor;
}
}
Figura 1.3: Função ordena: Cria uma abstração do procedimento de ordenação.
Após a implementação da função ordena, quando o programador precisar
ordenar um vetor, basta que ele invoque a função, passando os parâmetros
necessários. Ou seja, ao usar o procedimento, o programador irá se preocupar
apenas com o que a função faz e não mais com os detalhes de sua
implementação.
Uma abstração de procedimento deve realizar uma tarefa (por exemplo, ordenar
um vetor de inteiros) que deve ser independente da forma como este vai ser
implementado. O programador deve antes de tudo ver o procedimento como
uma caixa preta, cuja especificação deve conter três elementos básicos (Figura
1.4):
�
Entrada: O conjunto de dados de entrada necessário;
�
Saída: O conjunto de dados produzido pelo procedimento;
�
Função: A descrição do que o procedimento faz.
ENTRADA
FUNÇÃO
SAÍDA
Figura 1.4: Os elementos considerados na definição de um procedimento.
Na especificação do procedimento, o programador não deve estar preocupado
com a implementação, mas sim com o comportamento (função) do mesmo.
Qualquer implementação que realize a função especificada irá servir como
solução . Para o caso da ordenação, veremos adiante neste curso que existem
diferentes métodos de ordenar um vetor. O método utilizado na função ordena
se chama ordenação por inserção. A implementação interna poderia ser
substituída por qualquer outro método de ordenação. A própria linguagem C
oferece em sua biblioteca padrão stdlib, uma função chamada qsort, que
pode ser usada para ordenar vetores. Essa função utiliza um outro método de
ordenação muito conhecido e também muito rápido, chamado de Quick Sort.
12
M.�B.�Costa���Algoritmos�e�Estruturas�de�Dados
Atividade 3:
1. Suponha que você tenha disponíveis as seguintes funções
em C:
�
�
�
�
Int CalculaMedia(int *notas);
Void DeterminaMaiorEMenorNotas(int *v, int
* maior, int *menor);
Void leNotas(int *notas);
Void
MostraResultados(int
media,int
maiorNota, int menorNota);
Utilizando essas funções, desenvolva um programa em C
que leia as notas de 5 turmas e para cada uma delas,
imprima a média, a maior e a menor nota.
1.2.3 Tipos Abstratos de Dados
A abstração de dados visa aos mesmos objetivos que a abstração de
procedimentos, mas com relação às estruturas de dados utilizadas nos
programas. A abstração de dados visa criar novos tipos de dados definidos em
temos de seu comportamento. Esses novos tipos são chamados de Tipos
Abstratos de Dados - TAD.
É muito importante que você perceba que um tipo abstrato de dados não se
resume a uma nova estrutura de dados. Vamos então discutir um pouco sobre
essa diferença.
Primeiramente, uma estrutura de dados pode ser definida simplesmente como
uma variável capaz de armazenar mais de um valor simultaneamente. Assim, um
vetor, uma matriz ou um registro (struct em C) são exemplos de estruturas de
dados. A combinação desses elementos em estruturas mais complexas dá origem
a outras estruturas de dados. A Figura 1.5 apresenta as declarações de struct
ponto e struct reta, como exemplos de tipos de estruturas de dado.
struct ponto{
float x, y;
};
struct reta {
struct ponto a,b;
};
Figura 1.5: Estruturas de Dados ponto e reta.
Ao definir a struct ponto, passamos a contar com mais uma alternativa
para definição de tipos de variáveis e, conseqüentemente, para a composição de
novos tipos de estruturas. Assim a struct reta foi definida como uma
composição de duas variáveis do tipo struct ponto. Essas estruturas
podem ser usadas para declarar tanto variáveis como argumentos de funções.
Em C temos ainda a cláusula typedef, que permite definir o nome do novo
tipo. Nesse caso, não precisamos usar a palavra reservada struct na
declaração de variáveis do tipo definido. Na Figura 1.6 temos o uso de
13
M.�B.�Costa���Algoritmos�e�Estruturas�de�Dados
typedef. Veja que na definição do tipo Reta, o nome Ponto é usado sem a
palavra reservada struct.
typedef struct {
float x, y;
}Ponto;
typedef struct {
Ponto a,b;
}Reta;
Figura 1.6: Definição dos tipo Reta e Ponto.
Estrutura de Dados versus Tipo Abstrato de Dados
A definição de uma estrutura de dados se preocupa em demonstrar como o
objeto é representado na memória de um computador (representação). Nessa
definição são considerados aspectos do tipo: quais as informações que serão
armazenadas ali e qual a quantidade destas informações. A definição de um
Tipo Abstrato de Dados segue uma abordagem diferente. Essa definição é
feita em termos das operações que podem ser realizadas sobre o tipo.
A definição de um Tipo Abstrato de Dado (TAD) é chamada de especificação
e consiste, basicamente, em definir as operações relacionadas ao tipo.
Dizemos que essas operações definem o comportamento do TAD.
Vamos aplicar o conceito de TAD considerando os objetos Reta e Ponto. Um
passo importante na definição do TAD, que já ajuda na programação de uma
maneira geral, é que não conseguimos defini-lo sem conhecermos a sua
finalidade e o contexto no qual será usado. Em nosso exemplo precisamos
saber para quê? nós queremos um ponto e uma reta. Geralmente essa
informação é conseguida a partir do enunciado do problema. Assim, vamos
supor a seguinte descrição para o nosso problema envolvendo retas e pontos:
Problema A. É necessário um programa de computador que realize operações
geométricas envolvendo pontos e retas localizadas no plano cartesiano. O
programa deve permitir: calcular a distância do ponto até a origem do plano
cartesiano; calcular a distância entre dois pontos; dada a representação da reta
por dois pontos, calcular o ângulo de inclinação da reta, fornecer os
parâmetros a e b correspondentes a equação da reta ax + b. Determinar a
distância de uma reta a um ponto. Evidentemente, o programa deve permitir a
leitura e impressão de pontos e retas conforme a necessidade das operações.
Com base na descrição acima podemos identificar a necessidade do TAD ponto
e do TAD Reta, bem como suas operações. Essas operações aparecem
sublinhadas no texto do problema e são apresentadas nas Figuras 1.7 e 1.8.
Operações TAD Ponto:
-
Calcular Distância do ponto até
a Origem do plano cartesiano
Calcular Distância
14 até outro
ponto
Ler um ponto
Imprimir um ponto
M.�B.�Costa���Algoritmos�e�Estruturas�de�Dados
Figura 1.7: operações do TAD Ponto para o problema A.
Operações TAD Reta:
-
Calcular o ângulo de inclinação
da reta
Calcular os parâmetros a e b
Calcular a Distância da reta a
até um ponto
Ler uma reta representada por
dois pontos
Imprimir uma reta representada
por dois pontos
-
Figura 1.8: operações do TAD Reta para o problema A.
Agora, considere que estejamos desenvolvendo um programa para o problema
abaixo:
Problema B. É necessário um programa de computador para apresentar
graficamente figuras geométricas formadas por pontos e retas, usando o
monitor do computador como plano. O programa deve permitir: ler um ponto,
plotar um ponto na tela, ligar dois pontos por um segmento de reta; dada uma
reta passando por esse ponto, deslocar um outro ponto com base na equação
dessa reta; dada uma reta representada por dois pontos, plotar esta reta no
monitor; dada uma reta e um valor d, criar uma reta paralela à reta dada a uma
distancia d da reta.
As operações necessárias aos TAD Ponto e Reta neste problema são
apresentadas nas Figuras 1.9 e 1.10.
Operações TAD Ponto:
-
Ler um ponto
Plotar um ponto
Ligar dois pontos
Deslocar um ponto
Figura 1.9: operações do TAD Ponto para o problema B.
Operações TAD Reta:
-
Ler uma Reta 15
Plotar uma Reta
Criar Reta Paralela
M.�B.�Costa���Algoritmos�e�Estruturas�de�Dados
Figura 1.10: operações do TAD Reta para o problema B.
Note que nos dois problemas foram definidos os tipos Ponto e Reta. No
entanto, a abstração necessária difere de um problema para o outro,
interferindo na definição das operações. Embora existam essas diferenças,
iremos sempre tentar criar abstrações mais genéricas o quanto for possível.
Quanto mais genérico um TAD, maior o um número de problemas em que
esse poderá ser aplicado.
Embora as operações não informem nada a respeito da representação interna
de um TAD, elas fornecem ao programador tudo o que ele precisa para
manipular o TAD. As operações de um TAD especificam a sua interface com
o restante do programa. Isso significa que Qualquer ação relacionada ao
TAD deve ser feita mediante uma de suas operações.
Temos como resultado prático que o programador, ao usar o TAD, não vai
precisar se preocupar com sua implementação interna . Iremos agora analisar
esse aspecto considerando os TAD Ponto e Reta e o problema A.
Na implementação, ponto e reta foram definidos e implementados, originando
dois módulos independentes: o módulo Ponto e o módulo Reta. Cada módulo em
C é normalmente implementado por dois arquivos: o arquivo .h e o arquivo .c.
No arquivo .c teremos a implementação das operações do TAD e no arquivo .h
teremos a especificação da interface do TAD. A Figura 1.11 e a Figura 1.12
apresentam as interfaces dos módulos Ponto e Reta, respectivamente.
// interface do Tad Ponto: ponto.h
typedef struct ponto {
int x, int y;
}Ponto;
void lerPonto(Ponto *);
void distanciaOrigem(Ponto);
void distancia2Pontos(Ponto,Ponto);
Figura 1.11: Interface do TAD Ponto.
// interface do Tad Reta: Reta.h
typedef struct reta {
struct ponto a,b;
} Reta;
void lerReta(Reta *);
void distanciaRetaPonto(Reta,Ponto);
void InclinacaoReta(Reta);
void equacaoReta(Reta);
16
M.�B.�Costa���Algoritmos�e�Estruturas�de�Dados
Figura 1.12: Interface do TAD Reta.
Com a implementação dos TADs Ponto e Reta concluídas e devidamente
testadas, qualquer outro módulo do programa poderá utilizar esses tipos por
meio das operações definidas para os mesmos. Quando um módulo A utiliza
um módulo B em programação, dizemos que o módulo A é cliente do módulo
B. Essa relação de “clientela” entre os módulos (ou TADs) de um programa
pode ser representada graficamente por meio de um diagrama de estrutura
modular - DEM.
Diagrama de Estrutura Modular
Um diagrama de estrutura modular é formado por retângulos e linhas
direcionadas relacionando os retângulos. Cada retângulo representa um
módulo. As linhas direcionadas significam “cliente de” e indicam o
acionamento de operações contidas no módulo apontado pelo módulo cliente.
Esses digramas também são chamados diagramas hierárquicos pois
apresentam a hierarquia dos módulos, iniciando por um módulo que inicia o
programa e aciona os demais módulos.
Como exemplo, suponha que tenhamos também, junto aos módulos Ponto e
Reta, um módulo chamado principal. Esse módulo é cliente dos módulos Ponto e
Reta. A Figura 1.13 a seguir ilustra o DEM deste programa. O módulo que
inicia o programa é o módulo principal (e deve conter uma função main() ). Ele
aciona as operações tanto do módulo ponto quanto do módulo reta. Reta, por sua
vez, também é cliente do módulo ponto.
Ponto
Reta
Principal
Figura 1.13: DEM com módulos Ponto Reta e Principal.
A Figura 1.14 ilustra a implementação de um módulo Principal. Note que a
única forma de acesso aos TADs Ponto e Reta é por meio das operações
definidas em suas respectivas interfaces.
Use diagramas de estrutura modular sempre que for iniciar um novo projeto.
Defina os TADs e estabeleça o relacionamento entre eles por meio de DEMs.
Junto às linhas, você pode especificar as operações do TAD que são
acionadas pelo cliente. Isso vai ajudar você na especificação dos TADs.
Atividade 4
Implementar as operações do TadPonto e do TadReta considerando o
17
M.�B.�Costa���Algoritmos�e�Estruturas�de�Dados
enunciado do problema 1, da Seção 1.2.3 do texto, e desenvolver um
programa usando a função principal (do quadro a seguir) de forma a testar as
operações.
void principal() {
char opcao;
Ponto p1,p2;
Reta r1;
printf("1- Calcular Distância até a origem.\n");
printf("2- Calcular Distância entre pontos.\n");
printf("3- Calcular Distância de uma reta a um
ponto.\n");
printf("4- Determinar inclinação da reta.\n");
printf("5- Determinar equação da reta.\n");
printf("0- Sair.\n");
printf("Informe a opção Desejada: ");
do{
opocao = getchar();
switch(opcao){
case '1': lerPonto(&p1);
distanciaOrigem(p1);
break;
case '2': lerPonto(&p1);
lerPonto(&p2);
distancia2Pontos(p1,p2);
break;
case '3': lerReta(&r1);
lerPonto(&p1);
distanciaRetaPonto(r1,p1);
break;
case '4': lerReta(&r1);
inclinacaoReta(r1);
break;
case '5': lerReta(&r1);
equacaoReta(r1);
break;
default: printf("Informe uma Opção Válida.");
}
} while(opcao!='0');
}
int main(){
principal();
return 0;
}
Figura 1.14: Módulo Principal para o Problema A.
Até o momento, em nosso exemplo envolvendo Ponto e Reta, não abordamos
a questão de como as operações serão implementadas propositalmente. É
que até a parte que apresentamos do desenvolvimento não precisamos saber
mesmo. Você por acaso lembra como se calcula a distância entre dois
18
M.�B.�Costa���Algoritmos�e�Estruturas�de�Dados
pontos? E a distância entre uma reta e um ponto? Pois bem, o importante é
que você tenha compreendido a discussão feita e o exemplo dado, mesmo
sem saber responder essas duas perguntas. Assim espero!
1.2.4 Implementação de Tipos Abstratos de Dados
Um dos principais benefícios da abstração de dados é separar o
comportamento do TAD, especificado por meio da definição de suas
operações, da sua implementação. Em nosso exemplo, o que definimos a
respeito dos TAD Ponto e Reta foi o seu comportamento, as suas operações.
Nesta seção, discutiremos melhor como separar em um projeto a definição do
comportamento e da implementação de um TAD.
O projeto completo de um TAD consiste de dois passos:
1. Especificação - Consiste na especificação do comportamento do TAD;
2. Implementação – Implementação das operações e estruturas de dados.
Especificação
A especificação de um TAD descreve o que TAD faz, mas omite informações
sobre como o mesmo é implementado. Por omitir detalhes de implementação,
uma única especificação permite muitas implementações diferentes.
A especificação é feita por meio da definição das operações do TAD. Para
detalhar melhor cada uma destas operações, devemos estabelecer, para cada
operação, dois elementos:
-
Pré-condições: definem as condições necessárias para que a operação
possa ser realizada. Por exemplo, suponha que desejamos especificar o
TAD Conjunto com a operação listarConjunto. Uma pré-condição
para essa operação é que o Conjunto não esteja vazio.
-
Pós-condições: definem o estado do TAD após a execução da
operação,Por exemplo, suponha a operação inserirElemento no
TAD conjunto. Uma pós-condição para essa operação seria: elementos
no conjunto = elementos no conjunto + novo elemento. A Figura 1.15
ilustra a definição do TAD conjunto.
TAD Conjunto:
Operações:
inserirElemento
-
Pré-condições: conjunto cheio == FALSO
-
Pós Condições: elementos no conjunto
elementos do conjunto + novo elemento
listarConjunto
-
19
Pré-Condições: conjuntoVazio == FALSO
-
Pós-Condições:
não há
=
M.�B.�Costa���Algoritmos�e�Estruturas�de�Dados
Figura 1.15: Especificação do TAD Conjunto.
Implementação
Um TAD é implementado por um módulo de um programa. Uma única
especificação permite diferentes implementações para um mesmo TAD. Uma
implementação está correta se esta provê o comportamento especificado para o
TAD. Implementações corretas podem diferir uma da outra, por exemplo, em
termos do algoritmo ou da estrutura de dados que elas usam. Essas diferenças
interferem na eficiência (desempenho em tempo de execução, ou ocupação de
espaço) apresentado pelo TAD para realização das operações.
Encapsulamento
Para uma abstração funcionar corretamente, a sua implementação deve ser
encapsulada. Se a implementação for encapsulada, nenhum outro módulo do
programa vai depender de detalhes de implementação do TAD.
Encapsulamento garante que módulos do programa podem ser implementados
e re-implementados independentemente, sem afetar os outros módulos do
programa.
O encapsulamento geralmente é conseguido por meio da separação da interface
e da implementação do módulo. Conforme já vimos anteriormente, em C a
implementação de um TAD por meio de um módulo consiste em duas partes: a
especificação da interface do módulo por meio do arquivo header ( com
extensão .h) e da implementação das operações por meio de um arquivo com
extensão .c.
1.2.5 Avaliação de Implementações de Tipos Abstratos de
Dados
É fundamental para um programador saber criticar e avaliar a qualidade de
uma implementação! Uma implementação baseada em Abstração de
Dados é um indicativo de boa qualidade. Nesta Seção, discutiremos
elementos que permitem que você verifique se uma implementação
realmente está de acordo com essa técnica.
Embora a linguagem C não ofereça um mecanismo que impeça realmente que o
programador tenha acesso à estrutura interna do TAD, esta é uma regra
fundamental e deve ser respeitada pelo programador. Esta regra ou característica
de TADs é o encapsulamento, discutido na seção anterior. Dizemos que um
TAD é encapsulado por suas operações no sentido de que a estrutura interna do
TAD fica preservada e invisível ao restante do programa. Violar essa regra
significa não usar corretamente a Abstração de Dados.
Localidade
O maior benefício do encapsulamento chama-se princípio da Localidade.
20
M.�B.�Costa���Algoritmos�e�Estruturas�de�Dados
Esse princípio permite que um programa seja implementado, entendido e
modificado um módulo de cada vez. A localidade aumenta a qualidade do
software que está sendo desenvolvido.
Dentre os benefícios oriundos do princípio da localidade temos:
1. O programador de uma abstração sabe o que é necessário pelo que está
descrito na especificação. Dessa forma, ele não precisa interagir com
programadores de outros módulos (ou, pelo menos, essa interação vai
ser bem limitada).
2. De forma análoga, o programador de um módulo que usa a abstração
sabe o que esperar desta abstração, apenas pelo comportamento descrito
em sua especificação.
Uma ferramenta que pode contribuir para esse entendimento é a
documentação do TAD. Ou seja, a explicação sobre o seu funcionamento e
sobre como utilizá-lo. Procure sempre fazer uma boa documentação do TAD.
Essa documentação pode vir como um documento à parte que acompanha o
módulo.
3. É necessário apenas o raciocínio local (por módulo), para saber o que o
programa faz e se está fazendo a coisa certa. Para estudar e compreender
o programa, podemos dividi-lo em módulos, e analisar um módulo de
cada vez. Em cada caso, preocupamo-nos em saber se o módulo faz o
que é suposto que faça. Ou seja, se ele cumpre o que está na
especificação.
Pode-se assim limitar a atenção para um módulo, ignorando tanto os
módulos usados por este quanto os que o utilizam. Os módulos que
utilizam o módulo estudado podem ser ignorados porque dependem
apenas de sua especificação e não da sua implementação. Os módulos
utilizados são ignorados, raciocinando-se sobre o que eles fazem
utilizando apenas sua especificação em vez de sua codificação. Com
isso, tem-se uma grande economia de esforço, dado que as
especificações são muito menores que as implementações. Observandose apenas as especificações evita-se também um efeito cascata. Por
exemplo, se tivermos que olhar o código do módulo que utilizamos,
teremos que olhar também o código dos módulos que são utilizados
pelos módulos que utilizamos e assim por diante.
4. Finalmente, a modificação do programa pode ser feita módulo por
módulo. Se uma abstração particular necessita ser reimplementada para
prover um melhor desempenho, corrigir um erro ou prover uma nova
funcionalidade, o módulo implementado anteriormente pode ser trocado
por uma nova implementação sem afetar os outros módulos.
Prototipagem
Localidade também provê uma base firme para a prototipação ou
prototipagem. Um protótipo é uma implementação inicial, rudimentar e
incompleta de um programa a ser desenvolvido. Se mantivermos o
21
M.�B.�Costa���Algoritmos�e�Estruturas�de�Dados
princípio da localidade no desenvolvimento do protótipo, essa
implementação inicial pode ir sendo completada ou substituída por
implementações melhores sem grande esforço nem re-trabalho.
Localidade também provê suporte para evolução. Abstrações podem ser
utilizadas nesse caso para encapsular modificações potenciais no
programa. Por exemplo, suponha que desejamos um programa para ser
executado em diferentes máquinas. Podemos tratar esse problema
inventando abstrações que escondam as diferenças entre as máquinas de
forma que, para mover o programa para uma máquina diferente, apenas
essas abstrações precisem ser reimplementadas. Um bom princípio de
projeto é pensar sobre modificações esperadas e organizar o
desenvolvimento utilizando abstrações que encapsulem as mudanças.
Domínio da complexidade
Os benefícios da localidade são particularmente importantes para a
abstração de dados. Estruturas de dados são muitas vezes complicadas e a
visão mais abstrata mais simples provida pela especificação torna o resto
do programa mais simples. Ainda, mudanças nas estruturas de
armazenamento são uma das principais formas de evolução de programas.
Portanto, os efeitos dessas mudanças devem ser minimizados encapsulando
essas estruturas de dados em abstrações de dados.
Se avaliarmos um programa segundo o critério da abstração de dados,
devemos observar os seguintes fatores:
1. Os TADs estão realmente encapsulados?
a. O entendimento de cada módulo do programa independe dos
demais módulos?
b. O efeito cascata é evitado na depuração?
c. É possível reimplementar um módulo sem afetar os outros?
2. Potenciais mudanças foram previstas no projeto do TAD?
3. Os TADs oferecem abstrações suficientemente simples das estruturas de
dados que encapsulam?
Atividade 5
Os códigos a seguir especificam as interfaces de dois módulos:
Aluno e Turma.
/**************** aluno.h *******************/
typedef struct aluno{
char nome[30];
int matricula;
int cdgcurso;
22
M.�B.�Costa���Algoritmos�e�Estruturas�de�Dados
} Aluno;
void leAluno(Aluno *);
void imprimeAluno(Aluno);
void AlteraAluno(Aluno *);
/******************* turma.h *****************/
#include "aluno.h"
#define MAXTURMA 100
typedef struct turma {
int nalunos;
Aluno alunos[MAXTURMA];
}Turma;
void insereAluno(Turma *,Aluno);
/*insere o aluno passado como paramentro na
turma */
void localizaAluno(Turma *, char
*); /* localiza um aluno na turma pelo nome */
void imprimeTurma(Turma);
void atualizaAlunoDaTurma(Turma
*, char *);
Agora, suponha que tenhamos o seguinte módulo principal,
utilizando esses módulos:
/************ Modulo Principal **************/
#include "turma.h"
Turma turma1;
void principal(){
int opcao,i;
aluno a;
char nome[30];
do{
scanf("%d",&opcao);
switch(opcao){
case 1: /* cadastrar aluno */
lealuno(&a);
insereAluno(&turma1,a);
break;
}
case 2: /* imprimir os dados de um aluno
cujo nome é xxx*/
scanf("%s",nome);
a= localizaAluno(&turma1, nome);
printf("%s - %d - %d",a.nome,
a.matricula,a.cdgcurso);
break;
case 3: /* imprimir turma */
for (i=0;i<turma.nalunos;i++)
imprimeAluno(turma.alunos[i]);
}
break;
case 4: /* alterar dados de um aluno */
scanf("%s",nome);
atualizaAlunoDaTurma(&turma1,
nome);
23
M.�B.�Costa���Algoritmos�e�Estruturas�de�Dados
break;
}
}
}
Tarefas:
a) Critique a implementação do módulo acima com base nos
critérios de avaliação de TADs discutidos acima.
b)
Faça
uma
implementação
da
operação
atualizaAlunoDaTurma, respeitando os princípios de
programação baseada em tipos abstratos de dados.
Dijkstra (1930 – 2002) foi, sem dúvida, um dos cientistas que mais
contribuíram para o desenvolvimento da Programação de Computadores.
Terminamos este capítulo com uma frase dele, que resume o conceito de
abstração:
“O propósito da abstração não é ser vaga, mas sim, criar um novo nível
semântico no qual ela possa ser absolutamente precisa”.
Edsger. W. Dijkstra, The Humble Programmer (O Programador Pobre),
Communications of the ACM, 15(10), 1972.
Reflita sobre isso!
24