Sacos
Igor Machado Coelho
29/03/2021
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. 1
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');
[inicio→][a∣→][b∣→][a∣→][c∣→0]
Consideraremos uma lista encadeada de caracteres. Para tal, definimos um agregado que contenha elementos, e outro que represente um iterador.
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.
A operação adiciona
em uma lista encadeada adiciona um novo elemento na “cabeça” da lista (no início).
Podemos definir um tipo abstrato para o iterador, denominado IteradorTAD
.
As operações do iterador utilizam um marcador/sentinela onde o ponteiro atual vale zero quando não existe mais um próximo.
A operação itera em uma lista encadeada retorna o iterador da lista, posicionado no começo.
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:
A remoção de um elemento ocorre a partir de um iterador.
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.
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
.
Tópico Avançado: os métodos terminou()
, proximo()
e atual()
são substituídos pelos operadores ==
, ++
e *
, no agregado IteradorNoEnc1
.
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∣ 0123
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.
A operação adiciona
no final do vetor.
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.
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):
A remoção de um elemento exige realocação/cópia dos elementos posteriores.
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