Coleção de problemas, soluções e implementação de algumas estruturas de dados usando C++
Problema 1 ----------------------------------------------------------------------------------------------------------------
Escreva um programa para contar a frequência de palavras em um texto. Seu programa deve usar a função main abaixo para ler a entrada padrão, e imprimir resultados:
int main() {
WordCounter *wc = new WordCounter(100); string s; cin >> s; while (s != ".") { wc->addWord(s); cin >> s; } wc->print();
return 0; }
Para facilitar seu trabalho, a implementação da classe WordCounter segue abaixo. Para terminar esse exercício, você precisa completar os arquivos WordCounter.hpp e WordCounter.cpp. O construtor já está implementado. Você deve implementar os métodos que estão faltando (addWord(), print() e ~WordCounter()). A saída do problema corresponde a lista de palavras e suas frequências. As frequências e as palavras devem ser separadas por um único espaço em branco (" "). Além disso, as palavras devem ser apresentadas em ordem alfabética.
Note que a classe WordCounter utiliza a classe Word, que também já está implementada para você. O principal objetivo desse exercício é observar a comunicação entre objetos de diferentes classes. Você pode incluir novos métodos e atributos nas classes WordCounter e Word se julgar necessário.
#ifndef WORDCOUNT
#define WORDCOUNT
#include
#include
#include
#include
#include "Word.hpp"
using namespace std;
class WordCounter {
public:
Word *words;
int size = 0;
WordCounter(int num_words);
~WordCounter();
void addWord(string w);
void print() const;
};
#endif
Alguns exemplos de uso podem ser vistos logo abaixo:
$ cat t0.txt uma frase outra frase mais uma frase . $ ./a.out < t0.txt frase 3 mais 1 outra 1 uma 2
$ cat t1.txt a a a b c a b b c a a b a a b c . $ ./a.out < t2.txt a 8 b 5 c 3
$ cat t2.txt 1 2 3 4 2 1 4 3 2 1 . $ ./a.out < t2.txt 1 3 2 3 3 2 4 2
Atenção: você deve enviar os arquivos main.cpp, WordCounter.hpp, WordCounter.cpp, Word.hpp e Word.cpp.
Problema 2 ---------------------------------------------------------------------------------------------------------
Neste exercício você deverá completar as classes List e Node. A classe List representa uma lista encadeada de inteiros. Cada elo dessa lista é uma instância da classe Node:
class Node { private: int _data; Node * _next; public: Node(int data, Node* next); void setData(int data); void setNext(Node next); /* Retorna o dado armazenado no nó. @retorna um inteiro. / int getData() const; /* Retorna o próximo nó. / Node getNext() const; };
Parte da implementação da classe list é dada abaixo:
class List { public: List();
/** Essa função insere um elemento no começo da lista.
*/
void insert(int value);
/** Essa função remove o primeiro nó da lista, e retorna o dado armazenado nele.
@retorna um inteiro armazenado no primeiro nó da lista.
*/
int remove();
/** Esse método indica se a lista está vazia.
@retorna verdadeiro se a lista está vazia, e falso caso contrário.
*/
int isEmpty();
/** Informa o número de elementos da lista.
@retorna um inteiro n, onde n é o número de nó da lista.
*/
unsigned size() const;
/** Retorna o elemento do meio da lista.
Se a lista possui 2*n ou 2*n + 1 elementos, então o elemento do meio é
o que está no índice n, assumindo que o primeiro índice é 0.
@retorna o elemento do meio da lista.
*/
int middle() const;
/** Retorna o último elemento da lista.
@retorna o inteiro armazenado no último nó da lista.
*/
int last() const;
/** Esse método move o primeiro nó da lista para última posição.
Em outras palavras, rotate() + last() == head.
*/
void rotate();
private: Node *head; /// Ponteiro para o primeiro elemento da lista. };
Você deverá completar todas as funções que possuem declaração mais não possuem corpo. Fique à vontade para criar funções auxiliares (e privadas) e atributos dentro das classes List e Node. Tenha em mente que o principal objetivo desse exercício é aplicar o princípio de encapsulamento. Você deve enviar os arquivos .cpp e .hpp para as classes List e Node e o arquivo main.cpp. Para testar sua implementação, utilize a função main abaixo:
int main(int argc, char** argv) { int input; std::cin >> input; List *list = new List(); while (input != 0) { list->insert(input); std::cin >> input; } std::cout << "s:" << list->size() << ", "; std::cout << "m:" << list->middle() << ", "; std::cout << "l:" << list->last() << ", "; std::cout << "r:" << list->remove() << ", "; std::cout << "m:" << list->middle() << ", "; std::cout << "l:" << list->last() << ", "; list->rotate(); std::cout << "m:" << list->middle() << ", "; std::cout << "l:" << list->last() << std::endl; return 0; }
Alguns exemplos de uso podem ser vistos logo abaixo:
$ cat t0.txt 2 3 5 7 11 0 $ ./a.out < t0.txt s:5, m:5, l:2, r:11, m:3, l:2, m:2, l:7
$ cat t1.txt 2 3 5 7 11 13 17 19 23 29 31 37 0 $ ./a.out < t1.txt s:12, m:13, l:2, r:37, m:13, l:2, m:11, l:31
$ cat t2.txt 1 2 3 4 5 6 7 8 9 0 $ ./a.out < t2.txt s:9, m:5, l:1, r:9, m:4, l:1, m:3, l:8
Problema 3 ----------------------------------------------------------------------------------------------------------------
Neste exercício você irá implementar uma função genérica GetMaxDefault(T a, T b, T c). Essa função recebe três argumentos, e retorna o maior entre os dois primeiros, ou o terceiro argumento, caso não exista um maior entre os dois primeiros. A função opera sobre tipos que reconhecem uma ordem total ou parcial. Uma ordem, estabelecida sobre um conjunto, é dita total caso, dados dois elementos diferentes do conjunto, sempre for possível definir o maior deles. A ordem é dita parcial quando isso não for possível sempre. Por exemplo, o conjunto dos números inteiros define uma ordem total: dados dois números diferentes, sempre há um maior. Por outro lado, um conjunto potência, ordenado por inclusão, define uma ordem parcial. Se um subconjunto A contiver um subconjunto B, então A é dito maior que B, e de modo análogo, se um subconjunto B contiver um subconjunto A, então B é dito maior que A. Porém, é possível que nem A esteja em B, nem B esteja em A. Nesse caso, a ordem entre A e B não é definida. Quando a ordem parcial entre os dois primeiros elements, a e b, não estiver definida, sua função GetMaxDefault(a, b, c) deve retornar o terceiro elemento, c. Para lhe ajudar, a declaração de GetMaxDefault é dada abaixo:
template T GetMaxDefault (T a, T b, T dflt) { // TODO: implement the code that is missing here. }
A função GetMaxDefault deve operar sobre tipos que reconheçam o operador de comparação "maior que ou igual" (>=). Esse operador está definido para todos os tipos primitivos da linguagem C++. Porém, ele não é definido para tipos compostos como classes e structs. Nesse exercício, você deverá implementar esse operador para dois tipos, a saber: Interval e BitSet. Parte da implementação de Interval pode ser vista abaixo. Note que um intervalo I1 somente é maior que outro intervalo I2 se I1 contém I2 inteiramente. Por exemplo, [1, 5] é maior que [2, 4], porém [1, 5] não é maior que [3, 6], pois o primeiro interval não contém o segundo:
struct Interval { Interval(int left, int right): _l(left), _r(right) {} const int _l; const int _r; friend std::ostream & operator<<(std::ostream& os, const Interval& i) { os << '(' << i._l << ", " << i._r << ')'; return os; } /**
- \brief compares two intervals to see if the first is greater than the
- second. An interval (a1, a2) is greater than or equal to another interval
- (b1, b2) if a1 <= b1 and a2 >= b2. */ // TODO: implement the operator >= };
O segundo tipo que deve ser implementado é um conjunto de bits. Esse conjunto representa elementos como índices em um mapa de bits. Cada conjunto tem capacidade para armazenar os elementos de 1 até 32. Existem 2^32 conjuntos possíveis com esses elementos. Cada conjunto é definido pela representação binária de um número inteiro. Por exemplo, o número três, em binário, é escrito assim: 0...0011. Esse número representa o conjunto formado pelos números 1 e 2 (o primeiro e o segundo bits ativos). Similarmente, o número 10 possui a seguinte representação binária: 0...01010. Esse número representa o conjunto {2, 4}, pois o segundo e o quarto bits estão ativos. Abaixo, é dada parte da implementação de BitSet:
struct BitSet { BitSet(unsigned value): _set(value) {} const unsigned _set; friend std::ostream & operator<<(std::ostream& os, const BitSet& i) { const int limit = sizeof(unsigned) * 8; os << '<'; for (int aux = 0; aux < limit; aux++) { unsigned mask = 1 << aux; if (i._set & mask) { os << "|"; } else { os << "-"; } } os << '>'; return os; } /**
- \brief compares two bit sets. A bit set b1 is greater than or equal to
- another bit set b2 if b1 contains all the bits in b2. */ // TODO: implement the operador >= } };
Note que tanto BitSet quanto Interval possuem a implementação do operador de streaming (<<). Assim, é possível imprimir valores desses tipos diretamente via std::cout ou std::cerr, por exemplo. Você deverá testar seu código com a função main abaixo. Essa função lê bit sets (via número inteiros positivos que os representam) ou intervalos da entrada padrão. O primeiro caracter de cada linha indica o tipo que deverá ser lido:
#include #include "GetMax.h" BitSet* readBitSet() { unsigned set; std::cin >> set; return new BitSet(set); } Interval* readInterval() { int left, right; std::cin >> left >> right; return new Interval(left, right); } void testBitSet() { BitSet *b1 = readBitSet(); BitSet *b2 = readBitSet(); BitSet *dflt = new BitSet(0); std::cout << GetMaxDefault(*b1, *b2, *dflt) << std::endl; delete b1; delete b2; } void testInterval() { Interval *i1 = readInterval(); Interval *i2 = readInterval(); Interval *dflt = new Interval(0, 0); std::cout << GetMaxDefault(*i1, *i2, *dflt) << std::endl; delete i1; delete i2; } int main () { char data; while (std::cin >> data) { switch (data) { case 'i': testInterval(); break; case 'b': testBitSet(); break; default: std::cerr << "Invalid type\n"; } } return 0; }
Abaixo são mostrados alguns testes. Use-os como exemplo. Eles são parte do conjunto de testes que será usado para corrigir esse exercício.
$> X=0; cat t$X.txt ; echo "***"; ./a.out < t$X.txt i 2 20 3 15 i 2 4 3 5 i -1 1 0 0
(2, 20) (0, 0) (-1, 1)
$> X=1; cat t$X.txt ; echo "***"; ./a.out < t$X.txt b 7 3 b 6 3 b 15 7 b 12 8
<|||-----------------------------> <--------------------------------> <||||----------------------------> <--||---------------------------->
$> X=2; cat t$X.txt ; echo "***"; ./a.out < t$X.txt i 2 20 3 15 b 52428 34952 i 2 4 3 5 b 34952 33928
(2, 20) <--||--||--||--||----------------> (0, 0) <-------------------------------->
$> X=3; cat t$X.txt ; echo "***"; ./a.out < t$X.txt i 1 3 1 2
(1, 3)
$> X=4; cat t$X.txt ; echo "***"; ./a.out < t$X.txt b 24 24 b 28 24 b 24 28
<---||---------------------------> <--|||---------------------------> <--|||--------------------------->
Problema 4 -----------------------------------------------------------------------------------------------------------
Neste exercício você deverá implementar quatro operadores sobre a class Point, cuja declaração pode ser vista logo abaixo:
class Point { public: Point(double xx = 0, double yy = 0): x(xx), y(yy) {} double getX() const { return x; } double getY() const { return y; } private: double x; double y; }
Os quatro operadores que precisam ser implementados são:
O operador de leitura de stream << O operador de escrita de stream >> O operador de adição + O operador de atribuição com adição +=
A declaração de tais operadores pode ser vista logo abaixo:
class Point { public: ... friend std::ostream & operator << (std::ostream &out, const Point &c); friend std::istream & operator >> (std::istream &in, Point &c); Point operator + (const Point &p); Point& operator += (const Point &p); ... };
Use a função main abaixo para testar sua implementação:
#include #include #include "Point.h" void readPoints(std::vector &vec) { Point p; while (std::cin >> p) { vec.push_back(p); } } void printPoints(std::vector &vec) { for (Point p: vec) { std::cout << p << " "; } std::cout << std::endl; } Point sumAssPoints(std::vector &vec) { Point pSum; for (Point p: vec) { pSum += p; } return pSum; } Point sumBinPoints(std::vector &vec) { Point pSum; for (Point p: vec) { pSum = pSum + p; } return pSum; } int main() { std::vector vec; char testCase; std::cin >> testCase; readPoints(vec); if (testCase == 'a') { std::cout << "Sum = " << sumAssPoints(vec) << std::endl; } else if (testCase == 'b') { std::cout << "Sum = " << sumBinPoints(vec) << std::endl; } printPoints(vec); }
Abaixo são mostrados alguns testes que você pode usar como exemplo:
input a 2.0 3.1 -1.0 4.0 -1.5 -2.3 2.5 -1.2 output Sum = (2, 3.6) (2, 3.1) (-1, 4) (-1.5, -2.3) (2.5, -1.2)
input b 1.0 3.0 -2.0 -1.0 output Sum = (-1, 2) (1, 3) (-2, -1)
input p 2 3 -2 4 1 5.5 -3 -1 output (2, 3) (-2, 4) (1, 5.5) (-3, -1)
Problema 5 ----------------------------------------------------------------------------------------------------
Neste exercício você deverá implementar um arranjo circular. Um arranjo circular é uma estrutura de dados com capacidade finita, e duas operações, add e get. A primeira operação adiciona um elemento ao arranjo, e a segunda retorna o mais antigo elemento inserido nele. Use a declaração abaixo para entender como funciona essa estrutura de dados:
#ifndef RING_ARRAY_H #define RING_ARRAY_H
/**
-
\brief this class represents a ring array, that is, a circular array. The
-
array has a fixed capacity, and it is possible to insert elements in it
-
until it becomes full. Any attempt to insert more elements in a filled array
-
will abort the program. / template <class T, unsigned N> class RingArray { public: RingArray(): _first(0), _last(0) {} /*
- \brief This method adds a new value into the array. If the array is full, * then this method stops the program. After inserting an element in the
- array, the number of elements stored there increases by one.
- \param value the element that must be inserted. */ void add(T value);
/**
- \brief This method returns the oldest element stored in the array. After
- returning an element, that element is removed from the array. Thus, the
- number of elements in the array decreases by one. If we try to retrieve
- an element from an empty array, then this method aborts the program. */ T get();
/**
- This method is true if the array contains N-1 elements.
- \return true if the array contains N-1 elements. */ bool isFull() const;
/**
- This method is true if the array contains zero elements.
- \return true if the array is empty. */ bool isEmpty() const;
private: unsigned _first; ///< The index of the oldest element in the array. unsigned _last; ///< The index of the next empty spot in the array. T buf[N]; ///< The buffer that stores the data in the array. };
#endif
Para testar seu programa, utilize o arquivo de testes abaixo. Note que a parte de leitura de dados já está definida. Tudo o que você precisa fazer é prover implementações para os métodos da classe RingArray. Lembre-se que sua implementação precisa tratar situações de erro. Você pode usar a função fer_assert, que está implementada para você. Caso prefira usar outra função, não imprima mensagens de erro na saída de erro (err), ou o sistema de avaliação não vai conseguir capturá-las:
#include #include "RingArray.h" void fer_assert(const bool expr, const char* msg) { if (!expr) { std::cout << msg << std::endl; exit(1); } } template <class T, unsigned N> void RingArray<T, N>::add(T value) { // TODO: implement this method. } template <class T, unsigned N> T RingArray<T, N>::get() { // TODO: implement this method. } template <class T, unsigned N> bool RingArray<T, N>::isFull() const { // TODO: implement this method. } template <class T, unsigned N> bool RingArray<T, N>::isEmpty() const { // TODO: implement this method. } template void test_add_then_get() { RingArray<T, 8> r; T value; while (std::cin >> value) { r.add(value); } while (!r.isEmpty()) { std::cout << r.get() << std::endl; } } template void test_add_or_get() { RingArray<T, 8> r; T value; char c; while (std::cin >> c) { if (c == '+') { std::cin >> value; r.add(value); } else if (c == '-') { std::cout << r.get() << std::endl; } else { std::cout << "Invalid operation\n"; } } } int main () { char data; while (std::cin >> data) { switch (data) { case 'd': test_add_then_get(); break; case 's': test_add_then_getstd::string(); break; case 'D': test_add_or_get(); break; case 'S': test_add_or_getstd::string(); break; default: std::cout << "Invalid type\n"; } } return 0; }
Abaixo são mostrados alguns exemplos de testes. Seu programa deve produzir as mesmas saídas que esses exemplos. Não se preocupe em ler a entrada, pois isso já é feito pelo programa de testes:
$ X=0; echo "s o mar e ana, mariana" > t$X.txt ; ./a.out < t$X.txt o mar e ana, mariana
$> X=1; echo "d 1.0 2.3 2.1 -23.5" > t$X.txt ; ./a.out < t$X.txt 1 2.3 2.1 -23.5
$> X=2; echo "s a b c d e f g h" > t$X.txt ; ./a.out < t$X.txt Erro: anel cheio.
$> X=3; echo "D + 1.0 + 2.0 + 3.0 + 4.0 - + 5.0 + 6.0 + 7.0 - - - - - + 8.0 - -" > t$X.txt ; ./a.out < t$X.txt 1 2 3 4 5 6 7 8
$> X=4; echo "D + 1.0 - -" > t$X.txt ; ./a.out < t$X.txt 1 Erro: anel vazio.
Problema 6 ----------------------------------------------------------------------------------------------------
Neste exercício aprenderemos sobre mais um padrão de projetos: a Cadeia de Responsabilidade, ou Chain of Responsibility (CR), como originalmente chamado. Uma CR é um padrão bem parecido com o Decorator, que já vimos em um exercício anterior. Porém, em vez de todos os objetos da cadeia realizarem uma ação sobre o dado, na CR, somente um objeto perfaz tal ação.
Neste exercício, nós implementaremos um pequeno interpretador de instruções. Esse interpretador manipula uma máquina virtual de pilha. Essa máquina virtual é como um computador, porém, toda a memória que ela usa é uma pilha. As instruções permitem manipular os dados no topo da pilha. Nossa máquina reconhecerá somente cinco instruções:
pop: remove o dado no topo da pilha. push n: coloca o valor 'n' no topo da pilha. add: adiciona os dois valores no topo da pilha, remove-os, e coloca o valor resultante no topo da pilha. mul: multiplica os dois valores no topo da pilha, remove-os, e coloca o valor resultante no topo da pilha. print: imprime na saída padrão o valor no topo da pilha.
Para testar seu programa, utilize a função de teste abaixo:
#include #include #include "Handler.h" Instruction* readNextInstruction() { std::string msg; if (std::cin >> msg) { int value = 0; if (msg == "push") { std::cin >> value; } return new Instruction(msg, value); } return NULL; } int main() { std::stack *stack = new std::stack(); Handler *handler = new HandlerAdd(NULL, stack); handler = new HandlerMul(handler, stack); handler = new HandlerPop(handler, stack); handler = new HandlerPrint(handler, stack); handler = new HandlerPush(handler, stack); Instruction *inst; while ((inst = readNextInstruction())) { handler->handle(inst); delete inst; } delete stack; delete handler; return 0; }
Sua tarefa, neste exercício, será completar a implementação dos "Manipuladores de Instrução" (handlers). Um modelo descrevendo a estrutura de manipuladores pode ser vista abaixo:
As declarações de todas as funções já estão feitas para você. Você terá tão somente que implementar as funções handle de cada manipulador. A título de exemplo, abaixo pode ser vista a implementação da função handle da classe HandlerAdd:
void HandlerAdd::handle(Instruction *inst) { if (inst->msg.compare("add") == 0) { fer_assert(_stack->size() >= 2, "Erro: poucos argumentos na pilha."); const int v1 = _stack->top(); _stack->pop(); const int v2 = _stack->top(); _stack->pop(); const int vv = v1 + v2; _stack->push(vv); } else { fer_assert(_next != NULL, "Erro: comando desconhecido."); _next->handle(inst); } }
Note que estamos realizando o tratamento de erro com a função fer_assert. Essa função é mostrada logo abaixo:
void fer_assert(const bool expr, const char* msg) { if (!expr) { std::cout << msg << std::endl; exit(1); } }
Você pode usar qualquer outra função para realizar o tratamento de erro. Contudo, você deve imprimir mensagens de erro na saída padrão, não na saída de erro, para que a correção automática possa funcionar. Caso um erro aconteça, seu programa deve imprimir uma mensagem, e então interromper o programa. Os dois tipos de mensagem de erro possíveis são (Lembre-se de imprimir um ponto no final da mensagem de erro):
Erro: poucos argumentos na pilha. Essa mensagem é disparada quando a pilha não contém argumentos suficientes para a realização de alguma operação. Erro: comando desconhecido. Essa mensagem é disparada quando não existe nenhum manipulador para tratar um determinado tipo de instrução.
A declaração dos manipuladores e da classe de instrução está disponível em um dos arquivos que fazem parte do trabalho: Handler.h. Abaixo são dados alguns testes. Procure usá-los como exemplo para entender o funcionamento da máquina de pilha:
=== input === push 13 push 29 add print pop === output === 42
=== input === push 2 push 2 mul print push 2 mul print push 2 mul print push 2 mul print === output === 4 8 16 32
=== input === push 2 push 3 push 4 push 5 mul print mul print mul print === output === 20 60 120
=== input === push 2 print push 3 print pop add === output === 2 3 Erro: poucos argumentos na pilha.
=== input === push 2 print push 3 print add print === output === 2 3 5
Problema 7 -------------------------------------------------------------------------------------------------------------------------------------
Implemente no arquivo Stack.cpp a classe Stack (pilha de números inteiros) declarada no arquivo Stack.h. Sua implementação deve passar em todos os testes definidos no arquivo Main.cpp.