Segundo Exercício-Programa

Polinômios Esparsos

Entrega: até 16 23 de abril

Dúvidas sobre este EP devem ser enviadas para o Fórum de Discussão de CCM0128

Polinômios esparsos podem ser representados eficientemente por meio de listas encadeadas. Neste exercício-programa você escreverá uma biblioteca que usa listas encadeadas para implementar o tipo abstrato de dados "polinômio". A interface da sua biblioteca deve ser o arquivo polinomio.h, cujo conteúdo é o seguinte:

#ifndef __POLINOMIO_H__

typedef struct termo *Polinomio;

Polinomio cria_monomio(double coef, int exp);
Polinomio soma(Polinomio p, Polinomio q);
Polinomio subtrai(Polinomio p, Polinomio q);
Polinomio multiplica(Polinomio p, Polinomio q);
Polinomio divide(Polinomio p, Polinomio q);
Polinomio resto(Polinomio p, Polinomio q);
Polinomio oposto(Polinomio p);
Polinomio deriva(Polinomio p);
Polinomio copia(Polinomio p);
int grau(Polinomio p);
double calcula(Polinomio p, double x);
void imprime(Polinomio p, FILE *arq);
void libera(Polinomio p);

#define __POLINOMIO_H__
#endif

Note que o arquivo polinomio.h especifica o que se pode fazer com um Polinomio (isto é, as operações sobre polinômios), mas não informa como um Polinomio é implementado. O tipo Polinomio é definido como um apontador para uma estrutura cuja definição não é parte da interface da biblioteca. A definição da estrutura termo não aparece no arquivo polinomio.h. Isso faz com que o tipo Polinomio seja abstrato. Note, ainda, que esse tipo também é de primeira classe, pois podemos ter múltiplos polinômios e podemos armazenar esses polinômios em variáveis do tipo Polinomio.

Abaixo descrevemos o que deve fazer cada uma das funções da biblioteca:

Exceto pela função libera, nenhuma das funções acima modifica algum polinômio recebido como parâmetro. Todos os polinômios devolvidos por essas funções são alocados dinâmicamente e devem ser liberados (mediante chamadas à funcão libera) quando não tiverem mais utilidade para o cliente da biblioteca. Por exemplo: se uma variável v referenciar um Polinomio, é um erro deixar que v saia de escopo sem antes executar libera(v). Também é um erro atribuir outro Polinomio a v sem antes executar libera(v). Considere o seguinte exemplo, que imprime a soma e o produto de dois polinômios dados:

    Polinomio p, q, r;
    p = ...;              /* inicializa p com algum polinômio */
    q = ...;              /* inicializa q com outro polinômio */
    v = soma(p, q);       /* inicializa v com o polinômio soma p+q */
    imprime(v, stdout);   /* imprime o polinômio soma */
    libera(v);            /* libera o polinômio soma */
    v = multiplica(p, q); /* atribui a v o polinômio produto p*q */
    imprime(v, stdout);   /* imprime o polinômio produto */

O fragmento de código acima estaria errado sem a chamada libera(v). O fragmento a seguir também está errado, pois os polinômios devolvidos pelas chamadas às funções soma e multiplica são passados diretamente à função imprime, sem serem liberados:

    Polinomio p, q, r;
    p = ...;                           /* inicializa p com algum polinômio */
    q = ...;                           /* inicializa q com outro polinômio */
    imprime(soma(p, q), stdout);       /* imprime o polinômio soma p+q */
    imprime(multiplica(p, q), stdout); /* imprime o polinômio produto p*q */

É importante entender que um cliente da biblioteca precisa chamar funções da biblioteca para obter polinômios. O cliente não tem como "fabricar" polinômios diretamente. As funções cria_monomio, soma, subtrai, multiplica, divide, resto, oposto, deriva e copia criam novos polinômios.

Implementação da Biblioteca

Cada polinômio deve ser armazenado em uma lista encadeada sem cabeça. Cada célula dessa lista deve ser do tipo struct termo e deve conter os dados de um dos termos do polinômio: o coeficiente e o expoente, além do apontador para o próximo termo. Apenas termos com coeficiente não-nulo devem estar na lista e, para que a manipulação seja mais eficiente, os termos devem aparecer na lista em ordem decrescente do expoente.

Exemplo 1: O polinômio p(x)=-x5+2x3+5x2-7 fica armazenado em uma lista de acordo com a seguinte figura.

inicio
 
 
-1 5 prox
 
2 3 prox
 
5 2 prox
 
-7 0 NULL

Em cada uma das células acima, o campo prox representa um apontador para a célula seguinte na figura. O retângulo mais à esquerda é a variável que contém o início (o endereço da primeira célula) da lista. Note a ausência dos termos correspondentes a x4 e x, que neste polinômio têm coeficientes nulos.

Exemplo 2: O polinômio nulo p(x)=0 é representado pela lista que aparece na figura abaixo.

inicio
NULL

Note que essa lista é vazia, pois o polinômio p(x)=0 não possui nenhum termo com coeficiente não nulo. Como a lista não possui nenhuma célula, a variável que indica o início da lista contém NULL.

A implementação da biblioteca (o arquivo polinomio.c) deve incluir as seguintes declarações:

#include <stdio.h>
#include <stdlib.h>
...
#include "polinomio.h"

typedef struct termo {
    double coef;
    int exp;
    struct termo *prox;
} Termo;

static Termo *lista_livre = NULL;

A estrutura Termo é usada para as células dos polinômios. Em outras palavras, cada polinômio é uma lista encadeada de Termos. Note, entretanto, que os programas que forem usar a sua biblioteca de polinômios (os clientes da biblioteca) não precisam saber nada disso. Esses programas não precisam conhecer a estrutura Termo, nem saber que internamente a biblioteca implementa polinômios como listas encadeadas sem cabeça. Para escrever um cliente é suficiente conhecer a interface da biblioteca (o arquivo polinomio.h) e saber o que fazem as funções dessa interface.

A lista livre

Sua implementação deve manter uma lista livre, com os Termos alocados via malloc mas disponíveis no momento. O apontador lista_livre aponta para o início dessa lista. Um novo Termo deve ser alocado via malloc apenas quando a lista livre estiver vazia. A função cria_monomio devolve um Polinomio constituído por uma célula recém retirada da lista livre ou recém alocada via malloc. (Ela obtém essa célula por meio de uma chamada à função aloca_termo, descrita abaixo.) A função libera insere na lista livre todas as células do polinômio referenciado por p. (Ela faz isso mediante chamadas à função libera_termo, descrita abaixo.)

Não deixe sua implementação vazar memória, ou seja, inclua na lista livre todas as células que não estiverem em um polinômio no momento.

Sua biblioteca deve conter duas funções auxiliares com os seguintes protótipos:

static Termo *aloca_termo();
static void libera_termo(Termo *p);

Essas são funções de manipulação da lista livre. Todo acesso à lista livre é feito por meio de chamadas a aloca_termo ou a libera_termo. A função aloca_termo devolve o endereço de um Termo retirado da lista livre ou (caso essa lista esteja vazia) alocado diretamente via malloc. A função libera_termo insere na lista livre o Termo cujo endereço é recebido como parâmetro. Opcionalmente você pode fazer com que libera_termo examine o comprimento da lista livre antes de inserir um Termo na lista. Se esse comprimento for menor que um valor pré-fixado, a função insere o Termo na lista livre. Caso contrário libera_termo faz uma chamada a free para devolver ao sistema a memória ocupada pelo Termo. Esse esquema evita que a lista livre cresça indefinidamente.

Note que as demais partes da biblioteca nunca precisarão chamar malloc ou free para alocar ou liberar termos. A motivação para se usar uma lista livre é precisamente a economia nas chamadas a malloc e free, pois essas funções são mais demoradas que as funções aloca_termo e libera_termo. (Por que malloc e free não podem ser tão eficientes quanto aloca_termo e libera_termo?)

Requisitos adicionais sobre a implementação da biblioteca

Programa Cliente

Além da implementação das funções da biblioteca, você deverá entregar um programa principal que use essas funções. O programa principal é uma calculadora que mantém um conjunto de 23 variáveis identificadas pelas letras minúsculas de 'a' a 'w'. Os valores dessas variáveis são polinômios. O usuário da calculadora pode efetuar operações envolvendo essas variáveis.

Este é um exemplo de uso da calculadora:

a?
a(x) = 0 
p?  
p(x) = 0 
p: 3 4 5 -1 3 1 2  
p(x) = 4x^5 - x^3 + x^2 
q: 4 2 2 3 4 -1 0 1 3 
q(x) = 3x^4 + x^3 + 2x^2 - 1 
r=p+q  
r(x) = 4x^5 + 3x^4 + 3x^2 - 1 
s=p*q 
s(x) = 12x^9 + 4x^8 + 5x^7 + 2x^6 - 5x^5 + 2x^4 + x^3 - x^2 
t=s-r  
t(x) = 12x^9 + 4x^8 + 5x^7 + 2x^6 - 9x^5 - x^4 + x^3 - 4x^2 + 1 
u=r' 
u(x) = 20x^4 + 12x^3 + 6x 
u=~u  
u(x) = -20x^4 - 12x^3 - 6x 
v=t/p 
v(x) = 3x^4 + x^3 + 2x^2 - 2 
w=t%p 
w(x) = -3x^4 - x^3 - 2x^2 + 1 
a=~t 
a(x) = -12x^9 - 4x^8 - 5x^7 - 2x^6 + 9x^5 + x^4 - x^3 + 4x^2 - 1 
b=~t+p*v+w 
b(x) = 0 
f=p 
f(x) = 4x^5 - x^3 + x^2 
g=~(f+q)%(r'-r/q+v'*v) 
g(x) = -4x^5 - 3x^4 - 3x^2 + 1 
h=g+(f-(p+q)%(p-q))''*((p+q)'%~(p-q)')/(r+f) 
h(x) = -4x^5 - 3x^4 - 3x^2 + 240x - 245 
h(1.2345) 
h(1.2345) = 28.2717 
t? 
t(x) = 12x^9 + 4x^8 + 5x^7 + 2x^6 - 9x^5 - x^4 + x^3 - 4x^2 + 1 
t(1)  
t(1) = 11 
t(123.456)  
t(123.456) = 8.0163e+19 
p=p 
p(x) = 4x^5 - x^3 + x^2 
p=p*p  
p(x) = 16x^10 - 8x^8 + 8x^7 + x^6 - 2x^5 + x^4 
p(2)  
p(2) = 15376 
p=p*p  
p(x) = 256x^20 - 256x^18 + 256x^17 + 96x^16 - 192x^15 + 80x^14 + 48x^13 - 47x^12 + 12x^11 + 6x^10 - 4x^9 + x^8 
p(2) 
p(2) = 2.36421e+08 

As linhas em negrito pertencem ao arquivo de entrada (ou podem ter sido digitadas pelo usuário, de modo interativo). As demais linhas são geradas pela calculadora e formam a saída do programa.

Cada linha da entrada contém um comando para a calculadora. Todo comando começa com uma letra minúscula de 'a' a 'w'. Essa letra identifica uma variável da calculadora. O comando "v?" (uma varíavel da calculadora seguida do caractere "?") é um comando de consulta que imprime o polinômio associado à variável especificada. No início da sua execução, a calculadora inicializa com polinômios nulos todas as variáveis de 'a' a 'w'. (Seu programa deve fazer isso!) Por esse motivo, a consulta "a?" imprime a informação "a(x) = 0" (a é um polinômio nulo). A primeira das consultas "p?" imprime informação análoga.

Um comando da forma "v: n <lista de n pares>" atribui à variável v o polinômio cujos termos não nulos são especificados por uma lista de n pares de números. Em cada um desses pares de números, o primeiro elemento é o coeficiente do termo (um número real) e o segundo elemento é o expoente do termo (um número inteiro não negativo). Assim, o comando "p: 3 4 5 -1 3 1 2" atribui à variável p um polinômio com três termos não nulos (o polinômio 4x5 - x3 + x2). Em resposta a esse comando, a calculadora imprime o polinômio atribuído à variável p (a informação p(x) = 4x^5 - x^3 + x^2). Note que a lista de pares não precisa ser fornecida em ordem decrescente de expoente. No comando "q: 4 2 2 3 4 -1 0 1 3" os pares são fornecidos numa ordem arbitrária, mas os termos do polinômio atribuído à variável q aparecem em ordem decrescente de expoente (q(x) = 3x^4 + x^3 + 2x^2 - 1).

Um comando da forma "v=<expressão>" atribui à variável v o polinômio resultante da avaliação da expressão, que deve ser bem formada e pode conter os seguintes elementos:

Todos os operadores binários são infixos. O operador "~" é prefixo e o "'" é posfixo. Todos os operadores podem ser aplicados diretamente a variáveis ou a resultados de operações já efetuadas.

Em resposta a um comando da forma "v=<expressão>", a calculadora imprime o polinômio atribuído à variável v. O exemplo acima contém vários comandos desse tipo. Note que a mesma variável pode aparecer nos dois lados da atribuição ("p=p" e "p=p*p" são comandos válidos).

Finalmente, um comando da forma "v(<um número real>)" faz a calculadora imprimir o valor do polinômio v (mais precisamente, o valor do polinômio associado à variável v) sobre o número real especificado. Em outras palavras, o programa calcula e imprime v(x) para o valor de x especificado.

Precedência e Associatividade dos Operadores da Calculadora

A precedência determina qual operador "precede" o outro, ou seja, qual deve ser efetuado primeiro. Esta é ordem de precedência dos operadores aceitos pela calculadora:

  1. "'"
  2. "~"
  3. "*", "/" e "%" (empatados, ou seja, com a mesma precedência)
  4. "+" e "-" (empatados, ou seja, com a mesma precedência)

Por exemplo, na expressão

    a+b*c-d'*~e

o primeiro "*" será efetuado antes do "+", o "'" e o "~" antes do segundo "*", que precederá o "-".

A associatividade dos operadores binários é sempre da esquerda para a direita. Ou seja, na expressão

    a+b-c+d

o primeiro "+" é efetuado, depois o "-" e finalmente o segundo "+ ".

Implementação do Programa Cliente

Note que o segundo caractere de cada comando ('?', ':', '=' ou '(') define o tipo do comando. O primeiro caractere especifica a variável afetada pelo comando. Você pode usar esses fatos para simplificar a implementação do seu programa cliente.

Implementação das Variáveis da Calculadora

Para implementar as variáveis da calculadora, utilize um vetor com 23 posições (uma para cada letra minúscula de 'a' a 'w'), que contenha em cada posição uma referência para um polinômio.

Mais especificamente, seu programa cliente deve manter um vetor

    Polinomio var[23]; /* O tipo Polinomio é o definido pela biblioteca. */

de modo que

Em outras palavras, as 23 variáveis da calculadora (a, b, c, ..., w) são implementadas pelas 23 posições do vetor var (var[0], var[1], var[2], ..., var[22]). Dessa forma pode-se ter acesso a um polinômio de modo bem eficiente, a partir da letra minúscula que identifica o polinômio.

No início da execução do programa, todas as posições do vetor var devem ser inicializadas com polinômios nulos. Para obter polinômios nulos, faça chamadas à função cria_monomio passando o valor zero no parâmetro coef.

Avaliação das Expressões

De longe a maneira mais fácil para se avaliar uma expressão arbitrária contendo operadores infixos e subexpressões delimitadas por parênteses consiste destas duas etapas:

  1. Converta a expressão original para a forma posfixa.
  2. Avalie a expressão posfixa.

Sua calculadora deve usar esse método. A conversão da expressão original para a forma posfixa pode ser feita de modo similar ao mostrado nas notas de aula sobre pilhas. A avaliação da expressão posfixa deve ser feita da maneira natural, usando de uma pilha de operandos e resultados de operações.

Leitura dos Dados

Tome o seguinte cuidado com a leitura dos dados no seu programa cliente: leia cada linha da entrada até o último caractere da linha (o caractere '\n' que termina toda linha), incluindo esse caractere. Se você não fizer isso, quando você estiver querendo ler o primeiro caractere da próxima linha você acabará pegando o '\n' da linha anterior (ou até mesmo algum caractere que apareceu antes desse '\n', como por exemplo um espaço presente ao final da linha anterior). Para não ter esse tipo de problema, faça seu cliente ler toda linha até o fim, passando para a linha seguinte só depois de ler um '\n'!

Este esqueleto de programa cliente inclui uma função auxiliar (descarta_resto_da_linha) que lê e descarta todos os caracteres ainda não lidos da linha atual. Ele contém também um fragmento da função main com o laço de leitura da entrada. Esse fragmento mostra uma chamada a descarta_resto_da_linha, efetuada quando o cliente encontra uma linha cujo primeiro caractere não é uma variável válida. Faça o seu programa cliente chamar descarta_resto_da_linha sempre que a leitura dos caracteres de um comando não chegar ao '\n' que termina a linha: depois de ler o último caractere útil do comando, descarte o resto da linha da entrada.

Note que o nosso esqueleto de programa cliente tem mais uma função auxiliar (le_polinomio), esta ainda não implementada. Implemente a função le_polinomio e use-a no tratamento do comando ":"!

Organização do Programa Cliente

Além de conter a função principal (main) da calculadora, o arquivo cliente.c deve ter pelo menos as seguintes funções:

Argumentos na Linha de Comando

Seu programa cliente deve tratar os seguintes argumentos na linha de comando:

    ./cliente [-d] [arq-entrada] [arq-saida]
A linha de comando acima pressupõe que o nome do arquivo executável é cliente.

Os argumentos aceitos pelo programa têm estes significados:

Exemplos:

    ./cliente entrada.txt saida.txt
    ./cliente -d entrada.txt saida.txt
    ./cliente entrada.txt 
    ./cliente -d entrada.txt 
    ./cliente
    ./cliente -d

O tratamento dado a cada argumento é determinado pela posição do argumento na linha de comando. O primeiro argumento diferente de "-d" é o nome do arquivo de entrada. O argumento seguinte é o nome do arquivo de saída. Pode ocorrer omissão de argumentos:

Arquivos Exemplo

Neste diretório você encontra os arquivos polinomio.h e esqueleto-cliente.c, um exemplo pré-compilado de biblioteca (arquivo polinomio.o) e um exemplo executável de programa cliente (arquivo cliente), bem como o arquivo de entrada com os exemplos de uso da calculadora apresentados nesta página. O diretório contém também arquivos mallocc.h e mallocc.o, pois a biblioteca polinomio.o usa a função mallocc.

Seu programa cliente deve funcionar com o arquivo polinomio.o fornecido. Use-o para testar seu cliente. Note que você pode escolher se começa implementando a biblioteca ou se começa pelo cliente. Caso você opte por começar pelo cliente, o arquivo polinomio.o permitirá que você rode o cliente mesmo antes da sua biblioteca estar pronta.

Observações


Last modified: Wed Apr 24 19:19:26 BRT 2013