São requisitos para essa aula o conhecimento de:
Materiais complementares:
O Saco ou Bolsa (do inglês Bag) é um Tipo Abstrato de Dado (TAD) que serve para agregar elementos de um tipo pré-definido (estrutura homogênea).
Em um saco de produtos, por exemplo:
O Saco é também conhecido como multiset e alguns autores não consideram operações de remoção. ^[De acordo com NIST “An unordered collection of values that may have duplicates”.]
Sacos são estruturas fundamentais na própria computação, como o conceito de multiset
. Diferente de um set
, não se exige que elementos sejam únicos.
Em Python, a classe Counter
representa um tipo de saco. Em Java
, a interface Bag representa um saco. Em C++, pode-se considerar containers como std::list
e std::vector
.
Um Saco é um tipo abstrato de dado (TAD) que consiste de 4 operações básicas:
Note que o Saco não tem conceito de indexação nem uma ordem específica de percurso / ordenação dos elementos. Elementos podem ser repetidos.
Veja operações: adiciona('c'); adiciona('a'); adiciona('b'); adiciona('a');
Podemos responder as perguntas: ‘a’ existe? ‘d’ existe?
Quais estruturas de dados podem ser usadas para implementar as operações de um Saco?
Apresentaremos inicialmente a implementação do TAD Saco através de Listas Encadeadas, embora também possa ser feito através de Arrays.
O conceito de saco somente requer suas operações básicas. Como consideramos um saco genérica (saco de inteiro, char, etc), definimos um conceito genérico chamado SacoTAD
:
template <typename Agregado>
concept bool
SacoTAD = requires(Agregado a, typename Agregado::Tipo t,
typename Agregado::ItTipo it) {
// requer operação 'adiciona' sobre tipo 't'
{a.adiciona(t)};
// requer operação 'itera' (retorna 'it')
{a.itera()};
// requer operação 'busca' sobre tipo 't' (retorna 'it')
{a.busca(t)};
// requer operação 'remove'
{a.remove(it)};
};
Antes de completar as funções pendentes, utilizaremos um SacoTAD
:
int main () {
SacoTAD s = ...; // alguma implementação
s.cria();
s.adiciona('c');
s.adiciona('a');
s.adiciona('b');
s.adiciona('a');
printf("%d\n", s.busca('a').terminou()); // 0
printf("%d\n", s.busca('d').terminou()); // 1
for (auto it = s.itera(); !it.terminou(); it.proximo())
printf("%c\n", it.atual()); // a b a c (em qual ordem?)
s.remove(s.busca('b'));
s.libera();
...
As listas encadeadas possibilitam a inclusão de elementos em quantidade arbitrária (limitada à memória do computador), através de nós de encadeamento.
Veja operações: adiciona('c'); adiciona('a'); adiciona('b'); adiciona('a');
Consideraremos uma lista encadeada de caracteres. Para tal, definimos um agregado que contenha elementos, e outro que represente um iterador.
class NoEnc1
{
public:
char dado;
NoEnc1* proximo;
};
class IteradorNoEnc1
{
public:
NoEnc1* no; // no atual
char atual(); // inglês 'current'
bool terminou(); // inglês 'isDone'
void proximo(); // inglês 'next'
};
class ListaEnc1
{
public:
typedef char Tipo;
typedef IteradorNoEnc1 ItTipo;
NoEnc1* inicio; // inicio da lista
int N; // num. de elementos na lista
void cria(); // inicializa agregado
void libera(); // finaliza agregado
void adiciona(Tipo dado);
ItTipo itera();
ItTipo busca(Tipo dado);
void remove(ItTipo it);
};
// verifica se agregado ListaEnc1 satisfaz conceito SacoTAD
static_assert(SacoTAD<ListaEnc1>);
Antes de completar as funções pendentes, utilizaremos a ListaEnc1
:
int main () {
ListaEnc1 l;
l.cria();
l.adiciona('c');
l.adiciona('a');
l.adiciona('b');
l.adiciona('a');
printf("%d\n", l.busca('a').terminou());
printf("%d\n", l.busca('d').terminou());
for(auto it = l.itera(); !it.terminou(); it.proximo()) {
printf("%c\n", it.atual());
}
l.libera();
...
Verifique as impressões em tela: 0 1 A B A C
A operação cria
inicializa a estrutura para uso, e a função libera
desaloca os recursos dinâmicos.
class ListaEnc1 {
...
void cria() {
this->N = 0;
}
void libera() {
while (this->inicio != 0) {
auto* prox = this->inicio->proximo;
delete this->inicio;
this->inicio = prox;
}
this->N = 0;
}
...
}
A operação adiciona
em uma lista encadeada adiciona um novo elemento na “cabeça” da lista (no início).
class ListaEnc1 {
...
void adiciona(char dado) {
auto* no = new NoEnc1{.dado = dado, .proximo = inicio};
this->inicio = no;
this->N++; // N = N + 1
}
...
}
Podemos definir um tipo abstrato para o iterador, denominado IteradorTAD
.
template <typename Agregado>
concept bool
#endif
IteradorTAD = requires(Agregado a)
{
// requer operação 'terminou' (retorna booleano)
{a.terminou()};
// requer operação 'atual' (retorna elemento)
{a.atual()};
// requer operação 'proximo'
{a.proximo()};
};
As operações do iterador utilizam um marcador/sentinela onde o ponteiro atual vale zero quando não existe mais um próximo.
class IteradorNoEnc1 {
NoEnc1* no;
char atual() {
return this->no->dado;
}
bool terminou() {
return this->no == 0;
}
void proximo() {
this->no = this->no->proximo;
}
}
// verifica se agregado IteradorNoEnc1 satisfaz conceito IteradorTAD
static_assert(IteradorTAD<IteradorNoEnc1>);
A operação itera em uma lista encadeada retorna o iterador da lista, posicionado no começo.
class ListaEnc1 {
...
IteradorNoEnc1 itera() {
IteradorNoEnc1 it{.no = this->inicio};
return it;
}
...
}
A operação busca em uma lista encadeada utiliza o iterador da lista, posicionado no começo, para encontrar o elemento, retornando o iterador atualizado na posição correspondente.
class ListaEnc1 {
...
IteradorNoEnc1 busca(char dado) {
auto it = this->itera();
while (!it.terminou())
{
if (it.atual() == dado)
return it;
it.proximo();
}
return it;
}
...
}
Questão: O que acontece quando nenhum elemento é encontrado? Qual o valor de ‘it’ correspondente?
A busca pode ser feita de forma recursiva também:
IteradorNoEnc1 buscarec(IteradorNoEnc1 it, char dado)
{
if(it.terminou() || it.atual() == dado)
return it;
else
{
it.proximo();
return buscarec(it, dado);
}
}
A remoção de um elemento ocorre a partir de um iterador.
class ListaEnc1 {
...
void remove(IteradorNoEnc1 it)
{
auto *prox = it.no->proximo;
(*it.no) = (*it.no->proximo); // sobrescrita
delete prox;
this->N--; // N = N - 1
}
...
}
A implementação de listas encadeadas tipicamente permite acesso bidirecional, com um ponteiro anterior
além de um proximo
.
Este tipo de lista é chamado de Lista Duplamente Encadeada.
class NoDuploEnc1
{
public:
char dado;
NoDuploEnc1* anterior;
NoDuploEnc1* proximo;
};
Desafio: Implemente as operações da lista!
Dica: para simplificar operações nos extremos da lista, pode-se utilizar um nó sentinela de Cabeça e Cauda.
É possível transformar a implementação da ListaEnc1
para uso em for do tipo range, em C++. Exemplo:
int main() {
SacoTAD s = ListaEnc1();
s.cria();
s.adiciona('c');
s.adiciona('a');
s.adiciona('b');
s.adiciona('a');
printf("%d\n", s.busca('a').terminou()); // 0
printf("%d\n", s.busca('d').terminou()); // 1
printf("%d\n", buscarec(s.itera(), 'd').terminou()); // 1
for (auto x : s)
printf("%c\n", x); // a b a c
...
Por padrão, a linguagem C++ exige os métodos begin()
e end()
para inicio e fim do processo de iteração, no agregado ListaEnc1
.
Por outro lado, os métodos terminou()
, proximo()
e atual()
são substituídos pelos operadores ==
, ++
e *
, no agregado IteradorNoEnc1
.
class ListaEnc1
{
...
// iterador de início
IteradorNoEnc1 begin() {
return itera();
}
// sentinela de final da iteração
IteradorNoEnc1 end() {
return IteradorNoEnc1{.no = 0};
}
};
Tópico Avançado: os métodos terminou()
, proximo()
e atual()
são substituídos pelos operadores ==
, ++
e *
, no agregado IteradorNoEnc1
.
class IteradorNoEnc1
{
public:
NoEnc1* no; // elemento atual
... // demais métodos omitidos
char operator*() { return atual(); }
bool operator!=(const IteradorNoEnc1& other) {
return no != other.no;
}
void operator++() { proximo(); }
};
Fim parte de implementações de listas.
Os vetores podem ser utilizados como sacos, basta incluir os elementos sequencialmente à medida que forem adicionados.
Veja operações: adiciona('c'); adiciona('a'); adiciona('b'); adiciona('a');
\(| c | a | b | a |\) \(0 \; 1 \; 2 \; 3\)
Consideraremos um vetor de caracteres pré-alocado com capacidade MAX_N
.
constexpr int MAX_N = 10000;
class SacoVetor1 {
public:
typedef char Tipo;
typedef IteradorVetor1 ItTipo;
Tipo elementos[MAX_N]; // elementos
int N; // num. de elementos na lista
void cria(); // inicializa agregado
void libera(); // finaliza agregado
void adiciona(Tipo dado);
ItTipo itera();
ItTipo busca(Tipo dado);
void remove(ItTipo it);
};
Antes de completar as funções pendentes, utilizaremos a SacoVetor1
:
int main () {
SacoTAD s = SacoVetor1();
s.cria();
s.adiciona('c');
s.adiciona('a');
s.adiciona('b');
s.adiciona('a');
printf("%d\n", s.busca('a').terminou());
printf("%d\n", s.busca('d').terminou());
for(auto it = s.itera(); !it.terminou(); it.proximo()) {
printf("%c\n", it.atual()); // c a b a (nesta ordem)
}
l.libera();
...
A operação cria
inicializa a estrutura para uso, e a função libera
não desaloca nada.
class SacoVetor1 {
...
void cria() {
this->N = 0;
}
void libera() {
this->N = 0;
}
...
}
A operação adiciona
no final do vetor.
class SacoVetor1 {
...
void adiciona(char dado) {
this->elementos[N] = dado;
this->N++; // N = N + 1
}
...
}
Precisamos ainda de uma definição de iterador.
class IteradorVetor1
{
public:
char* elemento; // elemento atual
char* sentinela; // elemento sentinela "final"
char atual() { return *this->elemento };
bool terminou() { return elemento == sentinela; }
void proximo() { this->elemento++; }
};
// verifica se agregado satisfaz conceito IteradorTAD
static_assert(IteradorTAD<IteradorVetor1>);
// verifica se agregado SacoVetor1 satisfaz conceito SacoTAD
static_assert(SacoTAD<SacoVetor1>);
A operação itera adiciona a posição atual (vetor decai a um ponteiro), bem como uma posição sentinela final.
class SacoVetor1 {
...
IteradorVetor1 itera() {
IteradorVetor1 it{
.elemento = this->elementos,
.sentinela = this->elementos + N};
return it;
}
...
}
A operação busca em um vetor faz um percurso direto (alternativa sem utilizar o iterador).
class SacoVetor1 {
...
IteradorVetor1 busca(char dado) {
for (int i = 0; i < N; i++)
if (elementos[i] == dado)
return IteradorVetor1{
.elemento = this->elementos + i,
.sentinela = this->elementos + N};
return IteradorVetor1{
.elemento = this->elementos + N,
.sentinela = this->elementos + N};
}
...
}
A busca pode ser feita de forma recursiva também (utilizando o iterador):
IteradorVetor1 buscarec(IteradorVetor1 it, char dado)
{
if(it.terminou() || it.atual() == dado)
return it;
else
{
it.proximo();
return buscarec(it, dado);
}
}
A remoção de um elemento exige realocação/cópia dos elementos posteriores.
class SacoVetor1 {
...
void remove(IteradorVetor1 it)
{
auto* i = it.elemento;
while (i != (this->elementos + N)) {
(*i) = *(i + 1);
i++;
}
this->N--; // N = N - 1
}
...
}
Fim parte de implementações.
Além da bibliografia do curso, recomendamos para esse tópico:
Em especial, agradeço aos colegas que elaboraram bons materiais, como o prof. Fabiano Oliveira (IME-UERJ), e o prof. Jayme Szwarcfiter cujos conceitos formam o cerne desses slides.
Estendo os agradecimentos aos demais colegas que colaboraram com a elaboração do material do curso de Pesquisa Operacional, que abriu caminho para verificação prática dessa tecnologia de slides.
Esse material de curso só é possível graças aos inúmeros projetos de código-aberto que são necessários a ele, incluindo:
Agradecimento especial a empresas que suportam projetos livres envolvidos nesse curso:
Esses slides foram escritos utilizando pandoc, segundo o tutorial ilectures:
Exceto expressamente mencionado (com as devidas ressalvas ao material cedido por colegas), a licença será Creative Commons.
Licença: CC-BY 4.0 2020
Igor Machado Coelho