Uma árvore de busca binária (BST) é uma árvore binária (cada nó pode ter no máximo dois filhos, esquerda e direita) onde cada nó possui uma chave (um elemento de um conjunto com relação de ordem total) e, para todo nó u
, as chaves de todos os filhos na subárvore esquerda são menores que a chave de u
e as chaves de todos os filhos na subárvore direita são maiores que a chave de u
. Aqui há uma demonstração visual de como elas funcionam: https://visualgo.net/en/bst
Uma BST balanceada (pela altura) é uma estrutura de dados (ED) onde a representação dos dados é uma BST e as operações de modificação garantem que as distâncias da raiz até as folhas na árvore resultante serão limitadas por algum valor em função do número total de nós armazenados, por exemplo O(lg n)
.
Existem diversas BSTs balanceadas com O(lg n)
: AVL trees, red-black trees, treaps, splay trees, scapegoat trees... A mais fácil de implementar para competições é a treap: https://e-maxx-eng.appspot.com/data_structures/treap.html
Set é mais uma ED da STL C++, que representa um conjunto matemático. Sua implementação é feita por uma BST balanceada.
Assim como no vector, temos que indicar qual de qual tipo será nosso set.
set<int> conj;
para inserir no set, utiliza-se o método insert(). A complexidade dessa operação é O(log n)
set<int> conj;
conj.insert(1);
conj.insert(1);
conj.insert(2);
Assim como no conjunto matemático, valores duplicados não contam. Então nosso set teria os valores {1,2} após a execução desse trecho.
Para retirar elementos do set, utiliza-se o método erase(). A complexidade dessa operação é O(log n)
set<int> conj;
conj.insert(1);
conj.insert(1);
conj.erase(1);
Ao fim da execução desse trecho, o conjunto está vazio.
Pode-se verificar se um elemento está no set usando o método count(). A complexidade é O(log n)
set<int> conj;
conj.insert(1);
if(conj.count(1)){// retorna 1 se tiver o elemento
// faça algo
}
O método size retorna o tamanho do set, a complexidade é O(1)
set<int> conj;
conj.insert(1);
conj.insert(1);
conj.insert(2);
conj.insert(3);
int x = conj.size(); // x = 3
Se você quiser reinicializar o set, pode usar o método clear(). Sua complexidade é O(n)
, já que precisa-se liberar toda memória alocada.
Map é outra ED da STL C++, também implementada por uma BST balanceada. Enquanto o set é análogo à um conjunto matemático, o map é análogo a uma função, no sentido que podemos mapear o retorno de uma determinada entrada.
Para efeitos práticos, o map é bem parecido com set, mas ele permite guardar mais informações além de se o elemento está lá ou não. Maps nos permitem atrelar um dado a aquele elemento.
Mais formalmente, maps armazenam informações do tipo {chave, valor}, aonde os tipos de chave e de valor são definidos na declaração.
// meu mapa recebe chaves do tipo string e armazena valores inteiros
map<string,int> meu_mapa;
// o acesso é como o de um vetor, mas o índice tem o tipo da chave.
mapa["Deivis"] = 2;
cout << mapa["Sei la"] << endl;
// Isso vai imprimir 0, as chaves que ainda nao tem valores sao inicializados com um valor base quando são acessadas.
O método size retorna a quantidade de chaves criadas no map. No caso do map, é necessário tomar cuidado pois apenas acessar o valor de uma chave faz com que ela seja criada. A complexidade é O(1)
map<string, int> meu_mapa;
int x = meu_mapa["Deivis"];
int size = meu_mapa.size(); // size = 1
Assim como no set, usa-se count para ver se uma chave está no map ou não, a função retorna 1 caso esteja. A complexidade é O(log n)
.
map<char,int> meu_mapa;
meu_mapa['c'] = 2;
if(meu_mapa.count('c'))
puts("Meu mapa contém o char c");
Remove uma chave do map. A complexidade é O(log n)
map<char,int> meu_mapa;
meu_mapa['c'] = 2;
meu_mapa.erase('c');
if(mp.count('c'))
puts("Meu mapa contem o char c");
else
puts("Meu mapa nao contem o char c");
Limpa o map. A complexidade é O(n)
.
map<char,int> mp;
for(char i = 'd'; i < 'j'; i++){
mp[i]++;
}
for(char i = 'a'; i < 'l'; i++){
if(mp.count(i)) printf("%c pertence\n",i);
else printf("%c NAO pertence\n",i);
}
puts("");
mp.clear();
for(char i = 'a'; i < 'l'; i++){
if(mp.count(i)) printf("%c pertence\n",i);
else printf("%c NAO pertence\n",i);
}
Ás vezes é necessário percorrer os elementos no map e no set. Uma maneira de fazer isso é a seguinte:
set<int> conj;
// codigo que preenche o set
//iterator!
for(auto i = conj.begin(); i != conj.end(); i++){
printf("%d\n",*i);
}
map<int,int> mapa;
// codigo que preenche o map
//iterator!
for(auto i = mapa.begin(); i != mapa.end(); i++){
// Os *pares* chave valor do map funcionam como um pair (Duh!)
printf("%d %d\n",(*i.first),(*i).second);
}
A boa notícia é que tem um jeito um pouco mais simples.
set<int> conj;
// codigo que preenche o set
for(int elem : conj){
printf("%d\n",elem);
}
map<int,int> mapa;
// codigo que preenche o map
// p é um par de chave valor
for(auto& p : mapa){
printf("%d %d",p.first,p.second);
}
Perceba que no map é possível adicionar &
após o tipo para modificar o campo second
(valor mapeado pela chave, armazenada no campo first
).
É interessante considerar a seguinte ideia: E se usássemos um vetor para indicar se um elemento está ou não no set, ou guardar seu valor no map? (No caso de chaves inteiras). Sim, é possível.
// Max é o maior elemento que pode aparecer
// set<int> conj;
vector<bool> conj(MAX,false);
// insert
v[9] = true;
// erase
v[9] = false;
// count
if(v[9]){
// faça algo
}
// map<int,string> mapa;
vector<string> mapa(MAX);
//mesmo uso
mapa[1] = "oie";
// if(mapa.count(2))
if(mapa[2] != ""){
// faça algo
}
E a vantagem disso é que as operações de acesso e escrita ocorrem em O(1)
, e não O(log n)
. Mas para fazer isso, é necessário ter memória suficiente, caso o maior elemento for da ordem de 10^9 por exemplo, não teremos memória para tal e seria melhor usar um set ou map.
Uma função hash é qualquer função que pode ser usada para mapear dados de um conjunto de tamanho arbitrário para um conjunto de tamanho fixo.
int hash(int n){
return n % 10;
}
Antes de entrar na função, n
podia ter qualquer valor abrigado pelo tipo int
(um conjunto bem grande). Agora sabemos que ele é um inteiro no intervalo [0, 9].
Tabela hash é uma ED que faz uso dessa idéia. Cada dado é mapeado por meio da função hash, e armazenado naquela posição da tabela.
https://visualgo.net/en/hashtable
É evidente que quando se tem uma função cujo domínio é maior que o contra-domínio, dois elementos do domínio terão a mesma imagem. Ou seja, estamos propensos a ter dois elementos com o mesmo resultado na função hash.
Por essa razão, a tabela hash não guarda só um elemento numa das suas posições, e sim uma lista desses elementos.
Isso é de certa forma problemático, pois se tivermos muitas colisões, a eficiência dessa ED cai drasticamente.
Existem diversas formas de melhorar a eficiência de uma tabela hash:
- Escolher uma função hash com bom espalhamento.
hash(n) = k
é uma função hash, mas com espalhamento ruim (a imagem só possui um único elemento,k
). - Aumentar o tamanho do contra-domínio da função hash e o tamanho da tabela até um limite aceitável de uso de memória.
- Fazer vários níveis de hash, onde cada nível tem sua própria função hash. Assim que uma lista do nível
i
atinge um tamanho limite, ela é substituída por uma nova tabela hash que usa a função hash do níveli+1
.
Equivalentes a sets e maps, implementadas com tabelas hash no lugar de BSTs. Não suportam armazenamento ordenado dos elementos (limitação de tabelas hash).
unordered_set<int> conj;
unordered_map<int,int> mapa;
Os métodos e funcionalidades dessas estruturas de dados são quase em totalidade iguais aos set e map comuns, a maior mudança é relacionada a complexidade das operações.
Enquanto na implementação usando BSTs a maioria das operações tinha complexide logarítmica, na implementação usando hashtables, o caso médio tem complexidade O(1)
, mas o pior caso continua sendo O(n)
, caso hajam muitas colisões.
Equivalentes a sets e maps, permitindo elementos repetidos no set e chaves repetidas no map.
multiset<int> conj1;
multimap<int,int> mapa1;
unordered_multiset<int> conj2;
unordered_multimap<int,int> mapa2;