Esse projeto tenta simular uma database com o uso de uma Red-Black Tree.
O software é escrito em C++ com o uso do recurso templates para a construção da árvore, proporcionando assim uma flexibilização no armazenamento dos dados.
Foi utilizado, para testes da estrutura, dados da University of Minnesota sobre filmes. Você pode ler a documentação da base de dados clicando aqui
O objetivo desse projeto é criar um programa que tente simular uma database relacional simples, com vetores associativos para simular o conceito de "primary-key" de forma rudimentar. Para a construção desse programa foi utilizado a estrutura Red-Black Tree, uma Binary Search Tree balanceada.
A Red-Black Tree foi usada já que, em caso de busca em dados desordenados, ela se sai mais eficiente que um array comum, possuindo melhor, pior e caso médio de O(Log n)
; enquanto o array ganha apenas no melhor caso que é O(1)
em busca, e ela é mais eficiente que uma Binary Search Tree convencional já que ela é balanceada.
Embora os dados da base de Minnesota estarem organizados, assim uma lista sendo a mais eficiente, nosso intuito é montar uma estrutura solida para receber qualquer dado. Para isso foi usado um recurso de linguagem chamado template.
Com o template é possível especificar um tipo com que a estrutura trabalhe, evitando a necessidade da reescrita do código para tratar cada tipo.
Um exemplo de declaração da Red-Black Tree do tipo int
:
RBTree<int> tree;
Para conseguir montar uma árvore de um tipo específico, o tipo deve possuir os operadores:
- Igualdade (
==
) - Diferença (
!=
) - Maior que (
>
) - Menor que (
<
) - Stream insertion (
<<
)
Para sobrecarregar o operador de stream é necessário definir uma função friend para a classe:
//foo.hpp
#include <iostream>
struct foo{
int bar;
friend std::ostream &operator<<(std::ostream& os,const foo &f);
}
friend std::ostream &operator<<(std::ostream& os,const foo &f){
os<<f.bar;
return os;
}
//main.cpp
RBTree<foo> tree;
Para compilar esse programa é necessário ter o gcc >= 10.2.0 ou o clang >= 11.0.1.
$ git clone https://github.com/Numb4r/RBTree-DB-simulation.git RBDB
$ cd RBDB
$ make clean && make
Para compilar com o Clang, substitua no Makefile -g++ em CXX por -clang++
Para executar esse programa é necessário baixar a base de dados da University of Minnesota. Você pode baixar clicando aqui.
Extraia para a raiz do programa. A estrutura do projeto ficará assim:
📦RBDB
┣ 📂build
┣ 📂ml-1m
┃ ┣ 📜README
┃ ┣ 📜movies.dat
┃ ┣ 📜ratings.dat
┃ ┗ 📜users.dat
┣ 📂src
┃ ┣ 📜Database.cpp
┃ ┣ 📜Database.hpp
┃ ┣ 📜RBTree.cpp
┃ ┣ 📜RBTree.hpp
┃ ┗ 📜main.cpp
┣ 📜.gitignore
┣ 📜Makefile
┗ 📜Readme.md
Você pode retirar o arquivo users.dat já que ele não e necessário. Depois disso você pode executar pelo comando:
$ make run
Lembre-se que a pasta "ml-1m" precisa estar na raiz do programa. Então se mover o executável app para outro lugar, você precisa mover a pasta "ml-1m" para o mesmo local.
O algorítimo tem como objetivo uma simulação de uma database relacional de forma rudimentar. Para isso se utiliza da estrutura de árvore rubro-preta.
A estrutura da árvore rubro-preta é uma implementação da árvore binária de busca, mas seguindo certas regras:
- Todo nó possui uma cor, vermelho ou preta.
- A raiz é preta, independentemente.
- Todos as folhas nulas são pretas.
- Se um nó é vermelho, os seus dois filhos serão pretos.
- Todo caminho de um determinado nó para qualquer um de seus nós folha descendentes contem o mesmo numero de nós pretos.
Essas regras asseguram o ponto principal das árvores rubro-negra: ** O caminho mais longo da raiz a qualquer folha não seja mais que duas vezes o caminho mais curto da raiz a qualquer outra folha naquela árvore**
Com essas características, as árvores rubro-negras são consideras árvores binárias balanceadas.
Vamos simular a inserção de 8 itens com índices numéricos: (P) = Preto. (V) = Vermelho.
7 (P)
7 (P)
\
10(V)
10 (P)
/ \
07(V) 11(V)
10 (P)
/ \
07(P) 11(P)
\
12(V)
10 (P)
/ \
07(P) 12(P)
/ \
11(V) 13(V)
10 (P)
/ \
07(P) 12(V)
/ \
11(P) 13(P)
\
14(V)
10 (P)
/ \
07(P) 12(V)
/ \
11(P) 14(P)
/ \
13(V) 15(V)
12 (P)
/ \
10(V) 14(V)
/ \ / \
07(P) 11(P) 13(P) 15(P)
\
16(V)
Como podemos perceber, além de usar cores nos nós para manipulação da árvore, ela também se faz uso de rotações simples.
O uso da estrutura no algoritmo será semelhante ao mostrado acima, já que todos os dados possuem id único de identificação.
Os resultados são os esperados da estrutura árvore binária balanceada. As considerações a seguir usam tempo como métrica a eficiência do algorítimo. Embora não seja o ideal, o tempo serve como um parâmetro razoável para um estudo do algorítimo. Para cada resultado, foram feitas 5 execuções e feito a média aritmética.
Em uma operação que executa 100 000 buscas aleatórias na árvore que armazena os filmes, com 3883 entradas, o tempo médio de execução do algorítimo é de 0.07s. Isso se da pelo fato que a busca ser da ordem de O(Log n)
, o que demostra a eficiência da estrutura Red-Black Tree nessa operação.
Na inserção dos usuários, com 6 040 entradas, foi feito uma verificação para que não ocorresse uma possível duplicata nas entradas. Isso não foi feito na inserção dos filmes, e serve apenas para fins de teste. A validação foi feita através de uma busca na árvore para verificar se o índice não esta presente. O resultado foi insatisfatório, com um tempo de execução médio de 8.66s. Caso não fosse feito a verificação, o tempo de execução médio seria de 3.04s. Com isso, se pode concluir que a melhor forma de evitar duplicatas é implementando diretamente na árvore, coisa que a estrutura base da Red Black Tree não faz.
O tempo médio total do algorítimo, com a inserção dos filme e usuários (usando a verificação e buscando todos os filmes que o usuário já assistiu) é de 1317.20s. Com isso, pode se dizer que, embora muito eficientes, árvores binárias não são bons substitutos para um banco de dados convencional. Mesmo que a verificação fosse implementada diretamente na árvore, o resultado deixa muito a desejar. Assim, a estrutura Red-Black Tree se mostrou muito eficiente que uma estrutura simples de array.