Documente Academic
Documente Profesional
Documente Cultură
COMPLEXIDADE
GUIÃO DAS
AULAS PRÁTICAS
• Programação Avançada usando C, António Adrego da Rocha, FCA Editora de Informática, 2006.
AULAS 1 E 2
Calcule o número de operações aritméticas (somas ou produtos) executadas pelos algoritmos seguintes:
• Cálculo do factorial n!
• Cálculo do número de Fibonacci F(n) usando a versão repetitiva simplificada (ver 1.4 − Números de
Fibonacci, páginas 7-9)
∑
n
• Cálculo do quadrado usando a soma de sucessivos números ímpares n 2 = i =1
iésimo ímpar
∑
n
• Cálculo da soma de quadrados sucessivos SomaQua = i =1
i2
∑
n
• Cálculo da soma de potências sucessivas SomaPot =
i =1
xi
• Soma de matrizes
• Produto de matrizes
AULAS 3 E 4
Para perceber a implementação de tipos abstractos de dados na linguagem C, deve começar por ler o
capítulo 3, analisando a implementação do Tipo Abstracto de Dados COMPLEXO.
O Tipo Abstracto de Dados VECTOR é constituído pelo ficheiro de interface vector.h e pelo ficheiro
de implementação vector.c e implementa a criação de vectores e de operações sobre vectores. O TAD
tem capacidade de múltipla instanciação e usa um controlo centralizado de erros.
Para armazenar as componentes de um vector é usado um array, permitindo assim, que os algoritmos
matemáticos das operações sobre vectores sejam facilmente implementáveis sobre esta estrutura de
dados indexada. A figura apresenta o armazenamento de um vector com 5 componentes num array.
(v4,v3,v2,v1,v0) = (2.5,1.0,0.0,3.0,5.5)
Comece por ler com atenção os ficheiros e de seguida compile o módulo, usando para o efeito o
comando cc -c vector.c, sendo que cc é um alias do compilador da linguagem C programado da
seguinte maneira gcc -ansi -Wall. A compilação deve gerar o ficheiro objecto vector.o. De seguida
compile e teste as aplicações tvector.c e svector.c, que testam as operações do módulo vector. O
primeiro programa é uma aplicação simples, enquanto que o segundo é uma aplicação gráfica. Não se
esqueça que para compilar as aplicações, tem que mencionar o ficheiro objecto do TAD (vector.o) no
comando de compilação. Também é fornecida a makefile mkvector, para compilar o módulo e as
aplicações. Teste convenientemente toda a funcionalidade do TAD.
Para armazenar os coeficientes de um polinómio é usado um array, permitindo assim, que os algoritmos
matemáticos das operações sobre polinómios sejam facilmente implementáveis sobre esta estrutura de
dados indexada. Tenha em atenção que um polinómio de grau N tem N+1 coeficientes. A figura ilustra
o armazenamento de um polinómio de grau 4 num array.
3.5x4 + 2.5x3 + x2 + 4.5
• Pretende-se desenvolver o Tipo Abstracto de Dados MATRIZ, usando como estrutura de dados de
suporte um array bidimensional para armazenar os seus elementos inteiros. O TAD deve ter
capacidade de múltipla instanciação e controlo centralizado de erros.
5 GUIÃO DAS AULAS PRÁTICAS DE ALGORITMOS
São também fornecidas as aplicações tmatriz.c e smatriz.c e a makefile mkmatriz, para compilar o
módulo e as aplicações. Teste convenientemente toda a funcionalidade do TAD.
Sugestão: Para poder manipular as matrizes com acesso indexado do tipo Matriz[i][j], implemente na
memória dinâmica uma estrutura de dados matricial, como se indica na figura. Os excertos de código
necessários para criar e destruir uma sequência bidimensional com NL×NC elementos inteiros são
apresentados na Figura 2.5 (página 39).
PtMatriz
0
int **PtMatriz; 1
.
.
.
NL-1
0 1 2 . . . NC-1
Para armazenar os elementos de conjunto deve ser usada uma lista biligada, mantendo os seus
elementos sempre ordenados. Desta forma optimiza-se os algoritmos associados às operações habituais
sobre conjuntos. A figura apresenta o armazenamento do conjunto {A, K, L, X} numa lista biligada.
cabeça
do PtSeg PtSeg PtSeg PtSeg
conjunto
PtAnt PtAnt PtAnt PtAnt
PtEle PtEle PtEle PtEle
Sugestão: Para se familiarizar com listas biligadas e os seus algoritmos de manipulação, comece por ler
o item 2.4.2 – Listas biligadas (páginas 48-53).
GUIÃO DAS AULAS PRÁTICAS DE ALGORITMOS E COMPLEXIDADE 6
AULAS 5 E 6
• Pretende-se determinar experimentalmente a complexidade do algoritmo de pesquisa sequencia.
Faça a simulação dos algoritmos para sequências com N = 2K−1 números inteiros pares e determine o
número médio de comparações efectuadas para os dois casos médios da pesquisa.
Após a simulação dos algoritmos faça a análise teórica dos casos médios dos algoritmos de pesquisa
para arrays com 2K – 1 elementos. Compare os resultados experimentais com os resultados teóricos,
para sequências com N = 2K−1 elementos (com 10 ≤ K ≤ 20).
Para calcular o número de comparações, pode utilizar uma variável de duração permanente em
conjunção com a função de comparação pretendida. Para esse efeito, encapsula-se a comparação dentro
da função CmpSearchCount, que, além de efectuar a comparação pretendida, também contabiliza o
número de vezes que é invocada. O tipo de comparação desejado é indicado pelo parâmetro ptipo,
usando o identificador: EQUAL (==); BIGGER (>); BEQUAL (>=); LESSER (<); LEQUAL (<=) e NEQUAL
(!=). O modo de actuação é indicado pelo parâmetro pmodo, usando o identificador: INIC para
inicializar a variável contadora; NORM para executar a função e incrementar o valor da variável contadora;
e REP para reportar o valor da variável contadora. O modo de actuação é indicado pelo parâmetro
pmodo, usando o identificador: INIC para inicializar a variável contadora; NORM para executar a função e
incrementar o valor da variável contadora; e REP para reportar o valor da variável contadora.
GUIÃO DAS AULAS PRÁTICAS DE ALGORITMOS E COMPLEXIDADE 8
unsigned int CmpSearchCount (int *x, int *y, int ptipo, int pmodo)
{
static unsigned int cont; /* contagem do número de comparações */
AULAS 7 E 8
Implemente os seguintes algoritmos recursivos e calcule o número de operações aritméticas (somas ou
produtos) executadas:
1, se n = 0
1º método → x n = x × x n -1 2º método → x n ( ) 2
= x n/2 , se n é par
( ) 2
x × x n/2 , se n é ímpar
0, se n = 0
• Cálculo do número de Fibonacci F(n) = 1, se n = 1
F(n − 1) + F(n − 2), se n ≥ 2
∑
n
• Cálculo da soma de potências sucessivas SomaPot = i =1
xi usando o segundo método do cálculo
recursivo da potência
1, se n = 1
• Cálculo do número triangular T(n) =
T(n − 1) + n, se n > 1
1, se n = 1
• Cálculo do número quadrático Q(n) =
Q(n − 1) + 2n − 1, se n > 1
• Construa uma função recursiva para implementar a função C(n), definida pela seguinte relação de
recorrência.
1 , se n = 0
C(n) =
n −1
2 × C(i) + n ,
n ∑
caso contrário
i =0
Construa um programa para simular a execução da função C(n), que permita determinar o seu tempo
de execução, usando para esse efeito o módulo crono (crono.h e crono.c). Efectue a análise empírica
da complexidade da função implementada, construindo uma tabela com tempos de execução da função
para diferentes valores de n. Qual é a ordem de complexidade da função recursiva?
GUIÃO DAS AULAS PRÁTICAS DE ALGORITMOS E COMPLEXIDADE 10
Uma forma de resolver problemas recursivos de maneira a evitar o cálculo repetido de valores, consiste
em calcular os valores de baixo para cima, ou seja, de C(0) para C(n) e utilizar um array para manter os
valores entretanto calculados. Este método designa-se por programação dinâmica e reduz o tempo de
cálculo à custa da utilização de mais memória para armazenar valores intermédios.
Usando a técnica de programação dinâmica, construa uma função repetitiva para implementar a função
C(n) e efectue uma análise empírica da sua complexidade. Qual é a ordem de complexidade desta
implementação da função?
Analisando a solução anterior, desenvolva uma função repetitiva que não necessite de utilizar o array e
efectue uma análise empírica da sua complexidade. Qual é a ordem de complexidade desta
implementação da função?
Finalmente, faça a análise formal das três implementações da função C(n) e confirme as ordens de
complexidade obtidas experimentalmente.
FILAS E PILHAS
Para se familiarizar com as memórias fila e pilha sugere-se a leitura do Capítulo 7 – Filas e pilhas.
Estude o funcionamento das implementações dinâmicas, baseadas em listas ligadas e analise as
implementações dinâmicas genéricas.
A memória fila (Queue), cuja funcionalidade é especificada pelo ficheiro de interface queue.h e
implementada pelo ficheiro de implementação queue.c, é uma memória abstracta, com capacidade de
múltipla instanciação. Comece por compreender a sua funcionalidade e depois acrescente-lhe a função
QueueHead, que copia o elemento que se encontra à cabeça da fila, sem contudo o retirar da fila.
Acrescente também a função QueueEmpty, que determina se a fila está ou não vazia.
A memória pilha (Stack), cuja funcionalidade é especificada pelo ficheiro de interface stack.h e
implementada pelo ficheiro de implementação stack.c, é uma memória abstracta, com capacidade de
múltipla instanciação. Comece por compreender a sua funcionalidade e depois acrescente-lhe a função
StackTop, que copia o elemento que se encontra no topo da pilha, sem contudo o retirar da pilha.
Acrescente também a função StackEmpty, que determina se a pilha está ou não vazia.
Compile os módulos e teste a sua funcionalidade, usando nomeadamente o programa capicua.c que
determina se uma sequência de caracteres é um palíndromo. Um palíndromo é uma palavra que se lê da
mesma maneira, quer seja da esquerda para a direita, quer seja da direita para esquerda. Ou seja, é uma
palavra que normalmente se designa por capicua.
Implemente uma memória fila duplamente terminada (Double-Ended Queue – Deque), também
designada por fila dupla, dinâmica genérica cuja funcionalidade é especificada pelo ficheiro de interface
deque.h.. Uma fila dupla é uma fila que permite inserir e retirar elementos, quer da cabeça, quer da
cauda. A fila dupla implementa as seguintes operações: criar uma fila dupla DequeCreate; destruir uma
fila dupla DequeDestroy; colocar um novo elemento na cabeça da fila dupla DequePush; retirar um
elemento da cabeça da fila dupla DequePop; colocar um novo elemento na cauda da fila dupla
DequeInject; retirar um elemento da cauda da fila dupla DequeEject; e determinar se uma fila dupla
está ou não vazia DequeEmpty. Tenha em atenção que precisa de usar como estrutura de dados de
suporte uma lista biligada, de forma a permitir retirar elementos das duas extremidades da fila dupla.
11 GUIÃO DAS AULAS PRÁTICAS DE ALGORITMOS
AULA 9
O Tipo Abstracto de Dados Árvore Binária de Pesquisa (ABP) é constituído pelo ficheiro de interface
abp.h e pelo ficheiro de implementação abp.c e implementa a manipulação de árvores binárias de
pesquisa, com capacidade para armazenar números inteiros. O TAD tem capacidade de múltipla
instanciação e usa um controlo centralizado de erros.
Para testar o TAD é fornecido o programa tabp.c e a makefile mkabp. Comece por testar
convenientemente toda funcionalidade do TAD, usando para esse efeito os ficheiros de árvores
disponibilizados. Também é fornecido o programa irabp.c que simula a inserção e a remoção de
elementos na árvore, fazendo a sua visualização hierárquica na horizontal, após cada operação. Simule o
programa para as sequências de elementos dos ficheiros.
• Uma função recursiva para obter um ponteiro para o nó do menor elemento armazenado na
árvore. A função deve ter o seguinte protótipo:
PtABP ABPMinNode (PtABP pabp);
• Uma função repetitiva para obter um ponteiro para o nó do maior elemento armazenado na
árvore. A função deve ter o seguinte protótipo:
PtABP ABPMaxNode (PtABP pabp);
• Uma função para obter o elemento armazenado na árvore, dado um ponteiro para o seu nó. A
função deve ter o seguinte protótipo:
int ABPElement (PtABP pnode);
• Uma função recursiva para determinar a soma dos elementos armazenados na árvore. A função
deve ter o seguinte protótipo:
int ABPTotalSum (PtABP pabp);
• Uma função recursiva para determinar o número de elementos ímpares armazenados na árvore. A
função deve ter o seguinte protótipo:
unsigned int ABPOddCount (PtABP pabp);
• Uma função repetitiva para determinar a soma dos elementos pares armazenados na árvore. A
função deve ter o seguinte protótipo:
unsigned int ABPEvenSum (PtABP pabp);
• Uma função para determinar a soma dos elementos armazenados na árvore, com número de ordem
ímpar. Ou seja, a soma do primeiro, terceiro, quinto, sétimo, etc. menores números inteiros
armazenados na árvore. A função deve ter o seguinte protótipo:
int ABPOddOrderSum (PtABP pabp);
GUIÃO DAS AULAS PRÁTICAS DE ALGORITMOS E COMPLEXIDADE 12
Escreva um programa, chamado por exemplo testeabp.c que permita testar estas operações. O
programa constrói uma árvore binária de pesquisa a partir de um ficheiro, cujo nome é passado como
argumento na linha de comando e depois apresenta no monitor, o número de nós e a altura da árvore
criada. De seguida, o programa indica também: o valor do menor elemento armazenado na árvore; o
valor do maior elemento armazenado na árvore; a soma dos elementos da árvore; a contagem dos
números múltiplos de 3 da árvore; o número de números ímpares da árvore; a soma dos elementos
pares da árvore; e a soma dos elementos com número de ordem ímpar da árvore.
AULA 10
O Tipo Abstracto de Dados Árvore Adelson-Velskii Landis (AVL) é constituído pelo ficheiro de
interface avl.h e pelo ficheiro de implementação avl.c e implementa a manipulação de árvores AVL,
com capacidade para armazenar números inteiros. O TAD tem capacidade de múltipla instanciação e
usa um controlo centralizado de erros.
Para testar o TAD é fornecido o programa tavl.c e a makefile mkavl. Comece por testar
convenientemente toda funcionalidade do TAD, usando para esse efeito os mesmos ficheiros de
árvores disponibilizados para a árvore ABP.
Também é fornecido o programa iravl.c que simula a inserção e a remoção de elementos na árvore,
fazendo a sua visualização hierárquica na horizontal, após cada operação. Simule o programa para as
sequências de elementos dos ficheiros. Compare as simulações com as da árvore ABP.
13 GUIÃO DAS AULAS PRÁTICAS DE ALGORITMOS
AULA 11
Comece por ler o Capítulo 9 – Filas com prioridade, mais concretamente o item 9.5 – Fila com
prioridade com amontoado (páginas 374-377), para se familiarizar com a implementação de uma fila
com prioridade baseada num amontoado binário (binary heap) e os respectivos algoritmos.
O Tipo Abstracto de Dados Fila com Prioridade, que é constituído pelo ficheiro de interface pqueue.h
e pelo ficheiro de implementação pqueue.c, implementa a manipulação de filas com prioridade
orientada aos máximos, com capacidade para armazenar números inteiros. O TAD tem capacidade de
múltipla instanciação e as operações devolvem um código de erro relativo à execução da operação.
Para testar o TAD são fornecidos os programas tpqueue.c e spqueue.c e a makefile mkpqueue.
Comece por testar convenientemente toda funcionalidade do TAD, com excepção das operações
PQueueIncrease e PQueueDecrease que estão incompletas. Finalmente, complete a funcionalidade
destas duas operações e teste-as convenientemente.
TPC
Para simular o algoritmo do caminho mais curto de Dijkstra usa-se habitualmente uma fila com
prioridade implementada com um amontoado binário e organizada com prioridade orientada aos
mínimos, que armazene elementos estruturados do tipo VERTICE, sendo a prioridade dos elementos
estabelecida pelo campo TCost.
/* definição de um elemento da fila com prioridade */
typedef struct dijkstra
{
unsigned int Vertice; /* vértice */
int TCost; /* custo do caminho até ao vértice */
} VERTICE;
Crie uma fila com prioridade para elementos estruturados do tipo VERTICE, cuja funcionalidade é
descrita no ficheiro de interface pqueue_dijkstra.h e usando o esqueleto do ficheiro de
implementação pqueue_dijkstra.c. Implemente as seguintes operações:
Para testar a funcionalidade desta fila com prioridade pode usar o programa teste1.c, cuja execução é
apresentada no ficheiro teste1.txt. Pode também usar o programa teste2.c, que simula o algoritmo de
Dijkstra para o digrafo apresentado na página 388 (sendo o custo infinito representado pelo valor 100),
e cuja execução é apresentada no ficheiro teste2.txt.
GUIÃO DAS AULAS PRÁTICAS DE ALGORITMOS E COMPLEXIDADE 14
AULAS 12 E 13
Comece por ler o Capítulo 10 – Grafos (ver 10.3 − Implementação do Grafo, 10.4 − Caracterização do
Grafo e 10.5 − Digrafo dinâmico, páginas 397-411). Estude o funcionamento da implementação do
digrafo dinâmico e analise a sua implementação, para se familiarizar com os algoritmos.
O Tipo Abstracto de Dados DIGRAFO é constituído pelo ficheiro de interface digrafo.h e pelo
ficheiro de implementação digrafo.c e implementa a manipulação de digrafos dinâmicos. Para testar o
TAD é fornecido o programa de simulação gráfica sdigrafo.c e a makefile mkdigrafo.
Comece por testar convenientemente toda funcionalidade do TAD. Depois, acrescente-lhe a seguinte
funcionalidade:
• Verificar se o nó ni é uma “fonte”, i.e., se tem associados um ou mais arcos emergentes, mas não
tem nenhum arco incidente. A função deve ter o seguinte protótipo:
int DigraphSource (PtDigraph pdigraph, unsigned int pvertice);
• Verificar se o nó ni é um “sumidouro”, i.e., se tem associados um ou mais arcos incidentes, mas não
tem nenhum arco emergente. A função deve ter o seguinte protótipo:
int DigraphSink (PtDigraph pdigraph, unsigned int pvertice).
• Acrescente ao TAD digrafo o algoritmo de Dijkstra, que é fornecido no ficheiro dijkstra.c. Para
implementar este algoritmo precisa de uma fila com prioridade, orientada aos mínimos, para
elementos estruturados do tipo VERTICE (pqueue_dijkstra).