Outros Containers (List, Set e Map)
Ampliando o entendimento de containers via outros TADs comuns (com Douglas Macharet)
Até Agora…
Basicamente fizemos uso de Vector
- Vamos ampliar para outros
containers
Uma visão geral do Vector e dos outros
- Na memória, o
vector
é um vetor (array
) mesmo. - Apenas encapsulou-se o fato do mesmo crescer e reduzir de tamanho
- Vamos explorar outros Containers e entender, por alto, as diferenças
List
A lista é implementada com ponteiros 😱
3 <-> 7 <-> 9 <-> 8
- Lista duplamente encadeada
- Não temos mais acesso via índice. Motivo?
- Não tem um vetor por baixo como o vector
- Cada “número” guarda ponteiros para quem vem antes e depois
- É errado dizer que o número guarda os ponteiros, mas estou simplificando.
- Na prática é um struct, vide exemplo abaixo
struct node_t {
int valor;
node_t *anterior;
node_t *próximo;
}
List (Ideia)
O código abaixo mostra a ideia da lista no CPPTutor
- Observe que estamos apenas mostrando como encadear Nó
- Observe que para caminhar temos que usar os ponteiros
- Voltaremos para uma ‘list do zero’ em aulas futuras
Exemplo de código com a List
Observe o uso do iterador para acessar os elementos
#include <iostream>
#include <list>
int main() {
std::list<int> l = {7, 5, 16, 8};
l.push_front(25);
l.push_back(13);
// iterator de uma lista de inteiros
std::list<int>::iterator ptr = l.begin();
while (ptr != l.end()) {
std::cout << *ptr << std::endl;
ptr = next(ptr);
}
return 0;
}
Iteradores
- Geralmente não acessamos elementos usando índices quando usamos uma lista encadeada
- Pode ser feito, só que é mais lento
- Uma forma de iterar uma container sem usar índices
- Todo container da biblioteca padrão oferece as funções
begin()
eend()
begin()
: Retorna um iterador pro primeiro elementend()
: Indica o fim do container (último elemento da coleção)
- Usando um “iterador”, podemos dizer:
- “me dê o próximo, me dê o próximo, me dê o próximo, …” até acabar
next(it)
ouit++
- “me dê o próximo, me dê o próximo, me dê o próximo, …” até acabar
Iteradores C++
- Se comportam como ponteiros (😱)
ptr++
anda para frente e
As duas chamadas abaixo são equivalentes
std::list<int>::iterator ptr = l.begin();
std::cout << *ptr << std::endl
ptr = next(it);
Aqui usamos aritmética de ponteiros, mas pode fazer com next se achar mais simples
std::list<int>::iterator it = l.begin();
std::cout << *(it++) << std::endl
Pequeno Desvio (o auto)
#include <iostream>
#include <list>
int main() {
std::list<int> l = {7, 5, 16, 8};
l.push_front(25);
l.push_back(13);
auto ptr = l.begin();
while (ptr != l.end()) {
std::cout << *ptr << std::endl;
ptr = next(ptr);
}
return 0;
}
- No exemplo acima eu fiz uso de um tipo estranho
auto
(?) - Na prática, eu estou dizendo: ei C++, sou preguiçoso
- Digite o tipo para mim
- Se for possível, será feito
- O exemplo abaixo funciona pois:
l.begin()
sempre retorna um tipostd::list<int>::iterator
- Assim o compilador sabe inferir o tipo
next(it)
sempre retorna um ponteiro para um número na lista- Assim o compilador sabe inferir o tipo
Mais Auto
Aqui o compilador sabe o retorno da função, o auto funciona
double funcao() {
return 0.0
}
int main() {
auto var = funcao(); //função retorna int, então var é int
return 0;
}
Aqui teremos um erro de compilação
- Não temos como saber o tipo de `var
int main() {
auto var;
return 0;
}
List vs Vector
- Quando queremos uma sequência de elementos, podemos escolher entre vector e list
- A não ser que tenha um motivo, use vector.
- desempenho melhor no geral
- Caso faça deleções e inserções em vários locais, use
list
- Com
std::list<int> l = {7, 5, 16, 8};
l.push_front(0)
vira{0, 7, 5, 16, 8}
l.push_back(32)
vira{0, 7, 5, 16, 8, 32}
- Com
- O vector não suporta push_front
Containers Associativos (ou Conjutos sem Repetição)
- Elementos não possuem ordem específica
Projetados para suportar o acesso direto aos elementos usando chaves pré-determinadas
- Chave
- Usada internamente para guardar em ordem
- Operações
insert()
erase()
find()
count()
Set de forma Abstrata
- Guarda uma coleção de elementos distintos
Set em C++
- Dados armazenados (ordenados) em uma Árvores Binárias de Busca
- Já explico
- Comparáveis de acordo com algum critério
- Números sempre são comparáveis
- TADs são mais complexos
- Tem que usar um comparator, explicado mais afrente.
Exemplo com Set
#include <iostream>
#include <set>
int main() {
std::set<int> s;
for(int i = 1; i <= 10; i++) {
s.insert(i);
}
std::cout << "(" << s.size() << ")" << std::endl;
for (int e : s) {
std::cout << e << std::endl;
}
s.insert(7);
std::cout << "(" << s.size() << ")" << std::endl;
for (int e : s) {
std::cout << e << std::endl;
}
for(int i = 2; i <= 10; i += 2) {
s.erase(i);
}
std::cout << "(" << s.size() << ")" << std::endl;
for (int e : s) {
std::cout << e << std::endl;
}
return 0;
}
Árvores Binárias de Busca
Binary Search Tree – BST
- Invariantes:
- O filho da esquerda é menor ou igual ao nó
- O filho da direita é maior do que o nó
- Consequências:
- Elementos na esquerda são menores
- Elementos na direita são maiores
Pra qualquer nó:
[ Esquerda ] < [ Nó ] < [ Direita ]
8
/ \
/ \
3 10
/ \ \
/ \ \
2 4 15
Y.samadzadeh, CC BY-SA 4.0, via Wikimedia Commons
Inserção
A inserção é feita via um algoritmo recursivo
- Abaixo temos uma versão simplificada do mesmo
- Na verdade como estamos evitando ponteiros ao máximo por enquanto, pode ser vista como mais complicada :-)
- Sim, tive que usar ponteiros para esse exemplo, mas sem
new
edelete
- Qual a ideia?
- Sempre que for menor, tente inserir mais para esquerda
- Se for maior, mais para a direita
- Use recursividade para descer na árvore
Y.samadzadeh, CC BY-SA 4.0, via Wikimedia Commons
BST (Ideia)
O BST abaixo mostra a ideia da lista no CPPTutor
- Observe que estamos apenas mostrando como inserir um elemento
- Chamada recursiva que desce até a posição correta, estilo o gif acima
- A
BST
é assunto de Estruturas de Dados
Busca
A grande vantagem da árvore é buscar mais rápido.
- Como seria um algoritmo para achar um elemento em um vector?
- Percorre TODOS os elementos até achar.
- Observe abaixo como a árvore é mais rápida.
- Em cada passo eu pulo metade dos elementos
- n/2 várias vezes é
log(n)
- 16 / 2 = 8; 8 / 2 = 4; 4 / 2 = 2; 2 / 2 = 1.
- log2(16) = 4
- 4 divisões acima
- Em
log2(n)
passos acho um elemento - No vector eu tenho que percorrer todos os elementos,
A.gholamzade, CC BY-SA 4.0, via Wikimedia Commons
Map
- Um mapa armazena pares (chave, valor) chamados itens
- Chaves e valores podem ser de qualquer tipo
- Elemento e Valor são sinônimos
- A chave é utilizada para achar um elemento rapidamente
- Estruturas especiais são usadas para que a pesquisa seja rápida
- Diz-se portanto que um mapa “mapeia chaves para valores”
- O mapa pode ser mantido ordenado ou não (com respeito às chaves)
- Em C++ implementada como BST
- Também pode ser uma Tabela Hash
- Assunto de Estruturas de Dados
Exemplo Map
- Observe aqui o iterator mais complicado
- Tem que me indicar a chave e o valor
- Acesso com
->
pois se comporta como ponteiro
#include <iostream>
#include <string>
#include <map>
int main() {
std::map<int,std::string> telefones;
telefones.insert({2017123456, "Joao"}); // uma forma de inserir
telefones[2016123456] = "Maria"; // outras formas
telefones[2018123456] = "Carlos";
telefones[2015123456] = "Jose";
telefones[2014123456] = "Joana";
auto pair;
while(pair != telefones.end()) {
std::cout << pair->first << ": " << pair->second << std::endl;
pair = next(pair);
}
return 0;
}
Outros Maps/Sets
- Por padrão, maps/sets são implementados como árvores binárias de busca
- Existem versões unordered_*
- Mais eficazes na prática
- Porém, não conseguimos ordenar as chaves
- Iterador em um map/set
- Sempre em ordem
Comparators (Avançado, podem pular inicialmente)
- C++ sabe comparar números e strings
- Não sabe comparar seus TADs
- Para tal, precisamos criar um
functor comparator
- nome feio para um
struct
com uma única função:- comparar
- nome feio para um
struct nome_do_comparator {
bool operator()(Tad t1, Tad t2) {
// compara
}
}
Comparators (exemplo de uso)
// O comparator sempre verificar se é <. Com < podemos criar >, == e != . Note que:
// p1 < p2 <--> p2 > p1
// p1 >= p2 <--> !(p1 < p2)
// p1 == p2 <--> !(p1 < p2) && !(p2 < p1)
// p1 != p2 <--> !(p1 == p2)
struct compara_pessoa_f {
bool operator()(const Pessoa& p1, const Pessoa& p2) {
return p1.get_idade() < p2.get_idade();
}
};
int main() {
std::set<Pessoa, compara_pessoa_f> pessoas;
pessoas.insert(Pessoa("Ana", 18));
pessoas.insert(Pessoa("Pedro", 19));
pessoas.insert(Pessoa("Ana", 18));
// apenas uma ana no set
for (Pessoa p : pessoas)
std::cout << p.get_nome() << std::endl;
return 0;
}
Por fim…
- Vimos muitas estruturas de dados. Você pode explicar quando usamos cada uma?
vector
- indexação por inteiros
- inserção no final
set
- recuperação rápida de itens
- saída ordenada
map
- quando necessário para construir tabelas