Skip to content

PedroLouback/Comparasion-of-tree-structures

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

20 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Trabalho sobre Comparação das estruturas em árvore

Linguagem C++ requirement ISO

Comparação de diferentes estruturas com diferentes tamanhos de entrada

Conteúdos

ApresentaçãoIntroduçãoEstruturas e suas implementaçõesResultados e AnálisesConclusãoBibliotecasCompilação e ExecuçãoReferênciasAutor


Apresentação

Foi proposto pelo professor Michel Pires da Silva da matéria de Arquitetura e Estruturas de Dados 2 do 4º Período do curso de Engenharia de Computação um trabalho relacionado ao conteúdo passado em suas aulas teóricas que são Estrutura de Dados Árvore, onde foi introduzido uma comparação de tempo entre diversas implementações sob diferentes volumes de dados em três tipos de árvores sendo elas, Árvore Binária de Pesquisa, Árvore AVL e Árvore RedBlack e outras diferentes estruturas sendo vectors e MAPs. Portanto foi proposto o seguinte desafio:

Chegou o momento de compararmos tais estruturas, observando seu comportamento sob diferentes volumes de dados. Para tanto, elabore arquivos que contenham 500 , 5000 , 50.000 , 500.000 entradas numéricas do tipo ponto flutuante. Para tanto, tente evitar repetições de valores em grande escala para que possamos ter uma estrutura profunda. Considere produzir os menores a partir dos maiores volumes de forma randômica. Feito a isso, vamos testar os seguintes processos:

1) - Qual o tempo gasto com pesquisa para um arquivo de entrada com 10.000 consultas. Considere como consulta a busca de um determinado número na estrutura escolhida. Para cada pesquisa, é preciso remover o elemento da árvore, retornando esse número para o usuário. Então, considere o processo de remoção como parte final do processo.

2) - Qual o tempo necessário para montar a estrutura em memória.

3) - Qual o ganho dessas estruturas ao compará-las a vetores ordenados e aplicados à pesquisa binária. Qual o tempo gasto com a ordenação do vetor? Foi possível produzi-lo em memória?

4) - Adotando como estrutura o map e unorder_map do C++, há ganhos maiores nesse processo de pesquisa?

Elabore um relatório detalhando a implementação dessas estruturas, funcionamento da aplicação, exemplo de resultado, modo de compilação e conclusões. Considere essa última seção como uma discussão de quando adotar cada estrutura acima citada e o por quê de tal escolha. Para toda essa discussão, apresentar gráficos que demonstrem os resultados obtidos durante o processo de análise.

Com isso, foi desenvolvido um algoritmo baseado na proposta, onde foi possível obter as seguintes conclusões presentes em Conclusão!


Introdução

Para introduzir o problema proposto primeiramente foi necessário a criação dos cinco diferentes arquivos sendo quatro deles contendo os diferentes tamanhos de entrada do tipo float e um contendo 10.000 entradas do tipo float utilizado para consultas, sendo possível encontra-los na pasta src/files para verificação. A criação dos arquivos foi necessária apenas uma vez para manter o padrão de testes, após isso foi removido as funções de criações.

Após a criação dos arquivos foi necessário a implementação dos diferentes tipos de Estrutura de Dados Árvore onde foram utilizadas através de repositórios feitos pelo professor Michel Pires da Silva, sendo eles, Árvore Binária de Pesquisa, Árvore AVL e Árvore RedBlack onde na última estrutura foi necessária a implementação do método de remoção onde foi encontrado o mesmo no livro do [1]Cormem em forma de pseudocódigo, sendo feita apenas algumas alterações no código principal para que fosse possível ser feita a implementação da remoção sem nenhum problema. Sendo feita essas implementações foi necessário implementar a utilização de vectors e estruturas do tipo MAPs encontrando informações em [2] para uma implementação sem erros;

Estruturas e suas implementações

• Árvore Binária de Pesquisa

O que é uma Árvore Binária de Pesquisa?

Uma Árvore Binária de Pesquisa é organizada, como o nome sugere, em uma árvore binária como mostra a Figura 1. Sendo uma estrutura baseada em nós, onde todos os nós da subárvore esquerda possuem um valor númerico interior ao nó raiz e todos os nós da subárvore direita possuem um valor superior ao nó raiz, sendo essa a forma padrão. Essa estrutura possue o objetivo de estruturar os dados de forma a permitir Busca Binária. Esse tipo de estrutura suporta muitas operações de conjuntos dinâmicos, incluindo busca, mínimo, máximo, antecessor, sucessor, inserção e remoção, possibilitando seu uso como um dicionário e também como uma fila de prioridades.

Figura 1: Árvores Binárias de Pesquisa. (a)Uma árvore binária de pesquisa com seis nós e altura 2. (b)Uma árvore de busca binária menos eficiente, com altura 4, contendo as mesmas chaves de (a).

Custos de uma Árvore Binária de Pesquisa?

As operações básicas em um árvore binária de pesquisa demoram um tempo proporcional á altura da árvore. No caso de uma árvore binária completa com n nós, tais operações são executados no tempo $O(log{}{n})$ do pior caso. Porém, se a árvore é uma cadeia linear de n nós, as mesmas operações demoram o tempo $O(n)$ do pior caso. Segue uma tabela composta pela complexidade de tempo em Notação big O dos algoritmos básicos que compõem a Árvore Binária de Pesquisa:

Algoritmo Caso Médio Pior Caso
Espaço $O(n)$ $O(n)$
Busca $O(log{n})$ $O(n)$
Inserção $O(log{n})$ $O(n)$
Remoção $O(log{n})$ $O(n)$

Tabela 1: Complexidade de uma Árvore Binária de Pesquisa em notação big O.

  • Logo a árvore binária de busca é de pouca utilidade para ser aplicada em problemas de busca em geral, surgindo então o interesse em árvores balanceadas, cuja altura seja $O(log{}{n})$ no pior caso.

Operação de Busca

A operação de busca por um valor específico implementada no algoritmo através de uma função recursiva como é possível visualizar no pseudocódigo abaixo que foi usado como base para implementação:

TreeSearch(x,k)
1 if x == NULL ou k == x.chave
2   return x
3 if k < x.chave
4   return TreeSearch(x.esquerda, k)
5 else return TreeSearch(x.direita, k)

Explicação: A busca começa examinando o nó raiz. Se a árvore está vazia, o valor procurado não pode existir na árvore. Caso contrário, se o valor é igual a raiz a busca foi bem sucedida. Se o valor é menor do que a raiz, a busca segue pela subárvore esquerda. Igualmente caso o valor é maior do que a raiz, a busca segue pela subárvore direita. Se tornando um processo recursivo até encontrar o valor requerido. Se o valor na for encontrado até a busca chegar na subárvore nula é concluido que o valor não está presente na árvore

Operação de Inserção

A operação de inserção é utilizado o procedimento presente na função _insertTree() onde o mesmo toma um nó z para o qual z.chave = v, z.esquerda=NULL e z.direita=NULL, e modifica T e alguns dos atributos de z de modo tal que insere z em uma posição adequada na árvore, sendo possível visualizar no pseudocódigo abaixo:

insertTree(T,z)
1 y = NULL
2 x = T.raiz
3 while x != NULL
4   y = x
5   if z.chave < x.chave
6       x = x.esquerda
7   else x = x.direita
8 z.p = y
9 if y = NULL
10  T.raiz = z    //a árvore T era vazia
11 else if z.chave < y.chave
12  y.esquerda = z
13 else y.direita = z

Explicação: A fim de introduzir um nó novo na árvore, seu valor é primeiro comparado com o valor da raiz. Se seu valor for menor que a raiz, é comparado então com o valor do filho da esquerda da raiz. Se seu valor for maior, está comparado com o filho da direita da raiz. Este processo continua até que o nó novo esteja comparado com um nó da folha, e então adiciona-se o filho da direita ou esquerda, dependendo de seu valor. Segue abaixo na Figura 2 um exemplo de inserção na Árvore Binária de Pesquisa:

Figura 2: Exemplo de uma inserção do valor 13 em uma árvore binária de pesquisa. Os nós sombreados emtommais claro indicamo caminho simples da raiz até a posição emque o item é inserido. A linha tracejada indica a ligação que é acrescentada à árvore para inserir o item.

Operação de Remoção

A exclusão de um nó na árvore binária de pesquisa é um processo mais complexo. Para excluir um nó de uma árvore binária de pesquisa, levando em consideração três casos distintos para a exclusão, sendo eles:

Caso 1 - Remoção na folha: A exclusão na folha é a mais simples, batando apenas removê-lo da árvore como no exemplo da figura abaixo:

Figura 3: Remoção do valor 40 presente em uma folha.

Caso 2 - Remoção de nó com um filho: Ao excluir um nó que possui um filho, o filho vai subir para a posição do pai.

Figura 4: Remoção do valor 90 presente em um nó com um filho.

Caso 3 - Remoção de nó com dois filhos: Neste caso, há duas opções para ser operado. A primeira é a possibilidade de substituir o valor do nó a ser retirado pelo valor sucessor (o nó mais á esquerda da subárvore direita) ou pelo valor antecessor (o nó mais á direita da subárvore esquerda), sendo feita então a remoção do nó sucessor ou antecessor.

Figura 5: Remoção do valor 30 presente em um nó com dois filho.

A função antecessor foi utilizada para que houvesse uma implementação sem erros onde a mesma é composta por:

antecessor(T,x)
1 if x.direita != NULL
2 return antecessor(x.direita)
3 x = T
4 T = T.esquerda
5 free x
RemoveTree(T,x)
1 y = T.raiz
2 if y == NULL
3   return
4 if x.chave < y.chave
5     RemoveTree(y.esquerda, x)
6 if x.chave > y.chave
7     RemoveTree(y.direita, x)
8 if y.direita == NULL{
9     aux = y
10     y = y.esquerda
11     free aux
12 }
13 if y.esquerda == NULL
14     antecessor(y.esquerda, y)
15 aux = y
16 y = y.direita
17 free aux
  • T representa uma estrutura de árvore e x um nó

Testes utilizando Árvore Binária de Pesquisa

Após ser feita a implementação de todos os algoritmos básicos presentes em uma Árvore Binária de Pesquisa foi possível realizar os testes propostos em Apresentação obtendo então os seguintes resultados informados na tabela a seguir:

Tabela 2: Tempos médios de comparações para pesquisa, remoção e motagem da estrutura de Árvore Binária de Pesquisa.

Onde os mesmos são fornecidos pelo programa desenvolvido da seguinte forma após a sua compilação:

Figura 6: Tempos encontrados após execução para diferentes processos implementados na Árvore Binária de Pesquisa.


• Árvore AVL

O que é uma Árvore AVL?

Uma árvore AVL é uma árvore de busca binária de altura balanceada, ou seja, uma árvore balanceada são as árvores que minimizam o número de comparações efetuadas no pior caso para uma busca com chaves de probabilidades de ocorrências identidas, Sendo necessária a reconstrução da árvore para seu estado ideal a cada operação sobre seus nós (inserção ou remoção). A sua diferença de alturas das subárvores á esquerda e á direita de x é no máximo de uma unidade.

Figura 7: Demonstração de uma Árvore AVL aproximadamente balanceada, uma árvore desbalanceada após remoções e a sua correção resultando em uma Árvore AVL completamente balanceada.

Complexidade de uma Árvore AVL

Todas as operações básicas em uma Árvore AVL possuem complexidade $O(log{n})$ e ocupa espaço $O(n)$, sendo n o número de nós da árvore, essa condição provém da sua característica de balanceamento.

Algoritmo Caso Médio Pior Caso
Espaço $O(n)$ $O(n)$
Busca $O(log{n})$ $O(log{n})$
Inserção $O(log{n})$ $O(log{n})$
Remoção $O(log{n})$ $O(log{n})$

Tabela 3: Complexidade de uma Árvore AVL em notação big O.

  • Logo a árvore AVL é caracterizada por sua eficiência em buscas com chaves de probabilidades de ocorrências idênticas, reduzindo o número médio de comparações necessária para localizar qualquer elemento da árvore em 39 vezes menos.

Operação de Busca

A operação de busca por um valor específico implementada no algoritmo através de uma função recursiva como é possível visualizar no pseudocódigo abaixo que foi usado como base para implementação, sendo a mesma utilizada em Árvore Binária de Pesquisa:

TreeSearch(x,k)
1 if x == NULL ou k == x.chave
2   return x
3 if k < x.chave
4   return TreeSearch(x.esquerda, k)
5 else return TreeSearch(x.direita, k)

Explicação: A busca começa examinando o nó raiz. Se a árvore está vazia, o valor procurado não pode existir na árvore. Caso contrário, se o valor é igual a raiz a busca foi bem sucedida. Se o valor é menor do que a raiz, a busca segue pela subárvore esquerda. Igualmente caso o valor é maior do que a raiz, a busca segue pela subárvore direita. Se tornando um processo recursivo até encontrar o valor requerido. Se o valor na for encontrado até a busca chegar na subárvore nula é concluido que o valor não está presente na árvore

Operação de Inserção

A operação de inserção é utilizado o procedimento presente na função _insertTreeAVL() onde é necessário uma busca pelo elemento inserido nesta mesma árvore. Após a busca o local correto para a inserção do nó será em uma subárvore vazia de uma folha da árvore. Depois de inserido o nó, a altura do nó pai e de todos os nós acima deve ser atualizada. Em seguida o algoritmo de rotação simples ou dupla deve ser acionado para o primeiro nó pai desregulado

insertTreeAVL(T,z)
1 if T == NULL
2   new(T)
3   T.item = z
4   T.peso = 0
5   T.esquerda = NULL
6   T.direita = NULL
7   else if z.chave = T.item.chave
8       insertTreeAVL(T.esquerda, z)
9       if (getPeso(T.esquerda) - getPeso(T.direita) = 2)
10          if z.chave < T.esquerda.item.chave
11              rotacaoSimplesDireita(T)
12          else
13              rotacaoDuplaEsquerdaDireita(T)
14      else if z.chave > T.item.chave
15          insertTreeAVL(T.direita, z)
16          if (getPeso(T.direita) - getPeso(T.esquerda) = 2)
17              if z.chave > T.direita.item.chave
18                  rotacaoSimplesEsquerda(T)
19              else
20                  rotacaoDuplaDireitaEsquerda(T)
21 T.peso = PesoMax(getPeso(T.esquerda),getPeso(T.direita))+1

A inserção demonstrada utilizou de quatro diferentes funções que foram criadas para auxiliar o balanceamento na inserção, sendo essas as 4 diferentes rotações possíveis de visualizar nos exemplos gráficos e respectivos pseudocódigos abaixo:

Figura 8: Exemplo de uma rotação simples à direita.

rotacaoSimplesDireita(t)
1 aux = t.left;
2 t.left = aux.right;
3 aux.right = t;
4 t.peso = PesoMax(getPeso(t.left), getPeso(t.right)) + 1;
5 aux.peso = PesoMax(getPeso(aux.left), t.peso) + 1;
6 t = aux;
rotacaoSimplesEsquerda(t)
1 aux = t.right;
2 t.right = aux.left;
3 aux.left = t;
4 t.peso = PesoMax(getPeso(t.esquerdo), getPeso(t.direito)) + 1;
5 aux.peso = PesoMax(getPeso(auxˆ .direito), t.peso) + 1;
6 raiz = aux;

Figura 9: Exemplo de uma rotação dupla a esquerda/direita.

rotacaoDuplaEsquerdaDireita(t)
1 rotacaoSimplesEsquerda(t.left);
2 rotacaoSimplesDireita(t);
rotacaoDuplaDireitaEsquerda(t)
1 rotacaoSimplesDireita(t.right);
2 rotacaoSimplesEsquerda(t);
  • t representa uma estrutura de árvore e z um nó.

Operação de Remoção

O primeiro passo para remover uma chave na árvore AVL consiste em realizar uma busca binária a partir do nó raiz. Caso a busca encerre em uma subárvore vaiza, então a chave não está na árvore e a remoção não pode ser realizado. Caso a busca encerre em um nó a remoção poderá ser realizada obedecendo os seguintes casos

Caso 1 - Remoção na folha: A exclusão na folha é a mais simples, batando apenas removê-lo da árvore como no exemplo da figura abaixo:

Caso 2 - Remoção de nó com um filho: Ao excluir um nó que possui um filho, o filho vai subir para a posição do pai, bastando apontar o nó pai para a única subárvore e excluir o nó.

Caso 3 - Remoção de nó com dois filhos: Neste caso é necessário localizar o nó antecessor ou sucessor, que sempre será um nó folha ou possuirá apenas uma subárvore, copiar a chave do nó antecessor ou sucessor para o nó que deseja excluir e excluir o nó antecessor ou sucessor a partir da respectiva subárvore do nó que deseja excluir.

RemoveTree(T,F,x)
1 y = T.raiz
2 if y == NULL
3   return
4 if x.chave < y.chave
5     RemoveTree(y.esquerda,T,x)
6 if x.chave > y.chave
7     RemoveTree(y.direita,T,x)
8 if y.direita == NULL{
9     aux = y
10     y = y.esquerda
11     free aux
12     rebalenceTree(f) //Função que verifica os pesos da árvore e balanceia novamente se necessário
13 }
14 if y.esquerda != NULL{
15     antecessor(y.esquerda, y)
16     rebalenceTree(T)
17     rebalenceTree(F)
18 }        
19 aux = y
20 y = y.direita
21 free aux
22 rebalenceTree(T)
23 rebalenceTree(F)
  • T e F representam uma estrutura de árvore e x um nó.

Figura 10: Exemplo de uma remoção balanceada na Árvore AVL.

Testes utilizando Árvore AVL

Após ser feita a implementação de todos os algoritmos básicos presentes em uma Árvore AVL foi possível realizar os testes propostos em Apresentação obtendo então os seguintes resultados informados na tabela a seguir:

Tabela 4: Tempos médios de comparações para pesquisa, remoção e motagem da estrutura de Árvore AVL.

Onde os mesmos são fornecidos pelo programa desenvolvido da seguinte forma após a sua compilação:

Figura 11: Tempos encontrados após execução do programa como saída para diferentes processos implementados na Árvore AVL.


• Árvore RedBlack

O que é uma Árvore RedBlack?

As Árvores RedBlack são um dos muitos esquemas de árvores de busca que são "balanceadas" de modo a garantir que operações básicas de conjuntos dinâmicos demorem o tempo $O(log{n})$ no pior caso. Consistindo em uma árvore de busca binária com um bit extra de armazenamento por nó, sendo esse bit sua cor, podendo ser vermelha ou preta. Restringindo as cores dos nós em qualquer caminho simples da raiz até uma folha, as árvores vermelho-preto asseguram que o comprimento de nenhum desses caminhos seja maior que duas vezes o de qualquer outro, de modo que a árvore é aproximadamente balanceada. A Árvore RedBlack satisfaz as seguintes propriedades para manter seu balanceamento: 1. Todo nó é vermelho ou preto; 2. A raiz é preta; 3. Toda folha (NULL) é preta; 4. Se um nó é vermelho, então os seus filhos são pretos; 5. Para todo nó, todos os caminhos até as folhas apresentam o mesmo número de nós pretos 6. Para cada nova inserção, o nó criado começa com a cor vermelha

Figura 12: Exemplo de uma árvore RedBlack.

Complexidade de uma Árvore RedBlack

Todas as operações básicas em uma Árvore RB possuem complexidade $O(log{n})$ e ocupa espaço $O(n)$, sendo n o número de nós da árvore, essa condição provém da sua característica de balanceamento.

Algoritmo Caso Médio Pior Caso
Espaço $O(n)$ $O(n)$
Busca $O(log{n})$ $O(log{n})$
Inserção $O(log{n})$ $O(log{n})$
Remoção $O(log{n})$ $O(log{n})$

Tabela 5: Complexidade de uma Árvore RedBlack em notação big O.

  • Árvores Red Black são usadas na maioria das bibliotecas de linguagens como map, multimap, multset em C++ e é caracterizada por ser utilizada em aplicações de tempo real (críticas)

Operação de Busca

A operação de busca por um valor específico implementada no algoritmo através de uma função iterativa como é possível visualizar no pseudocódigo abaixo que foi usado como base para implementação:

RBTreeSearch(T,x)
1 if T == NULL
2   return
3 atual = T
4 while atual != NULL
5   if x == atual.valor
6       return
7   if x > atua.valor
8       atual = atual.direita
9   else
10      atual = atual.esquerda
  • T representa uma estrutura de árvore e x um valor do tipo flutuante.

Operação de Inserção

A operação de inserção é utilizado o procedimento presente na função _insertTreeRB(), sendo considerado uma operação complicada porque pode ferir alguma propriedade deste tipo de árvore, podendo ser implementadas de forma bastante parecida com as respectivas operações nas Árvores Binárias de Pesquisa, bastanto apenas modificar as cores dos nós para que as propriedades de Árvores RedBlack sejam satisfeitas, utilizando uma função secundária RBInsertFixUp() para restauras essas propriedades na inserção.

insertTreeRB(T,z)
1 y = T.nil
2 x = T.raiz
3 while x != T.nil
4   y = x
5   if z.chave < x.chave
6       x = x.esquerda
7   else x = x.diretia
8 z.p = y
9 if y == T.nil
10  T.raiz = z
11 else if z.chave < x.chave
12     y.esquerda = z
13 else y.direita = z
14 z.esquerda = T.nil
15 z.direita = T.nil
16 z.cor = VERMELHO
17 RBInsertFixUp(T,z)
RBInsertFixUp(T,z)
1 while z.p.cor == VERMELHO
2   if z.p == z.p.p.esquerda
3       y = z.p.p.direita
4       if y.cor == VERMELHO             
5           z.p.cor = PRETO                //caso 1
6           y.cor = PRETO                  //caso 1
7           z.p.p.cor = VERMELHO           //caso 1
8           z = z.p.p                      //caso 1
9       else if z = z.p.direita
10          z = z.p                        //caso 2
11          rotacaoSimplesEsquerda(T, z)   //caso 2
12     z.p.cor = PRETO                     //caso 3
13     z.p.p = VERMELHO                    //caso 3
14     rotacaoSimplesDireita(T, z.p.p)     //caso 3
15     else (igual à cláusala then com "direita" e "esquerda" trocadas)
16 T.raiz.cor = PRETO
  • T representa uma estrutura de árvore e z um nó.

Explicação: A operação de inserção em uma Árvore RedBlack começa por uma busca da posição onde o novo nó deve ser inserido, partindo-se da raiz em direção aos nós que possuam o valor mais próximo do qual vai ser inserido, onde é feita a verificação de acordo com seus respectivos casos:

Caso 1: Caso esta inserção seja feita em uma árvore vazia, basta alterar a cor do nó para preto, satisfazendo assim a propriedade 2;

Caso 2: Ao inserir x, se o tio de x é vermelho, é necessário fazer a recoloração de seu a, t e p;

Obs1: Se o pai de a é vermelho, o rebalanceamento tem que ser feito novamente.
Obs2: Se a é raiz, então ele deve ser preto!

Caso 3: Suponha que o tio do elemento inserido é preto. Neste caso, para manter o critério 4 é preciso fazer rotações envolvendo a, t, p e x.
– Há 4 subcasos que correspondem às 4 rotações possíveis:

Caso 3a: Rotação à direita;

Caso 3b: Rotação à esquerda;

Caso 3c: Rotação dupla esquerda, podendo ser visto como um caso 3a seguido do caso 3b;

Caso 3d: Rotação dupla direita, podendo ser visto como um caso 3b seguido do caso 3a;

(OBS: Lembrando que a Árvore RedBlack não possui explicitamente rotações duplas, os exemplos acima de rotações duplas são constituidos de duas rotações simples onde resulta em uma rotação dupla implicitamente)

Operação de Remoção

A remoção nas árvores RedBlacks se inicia com uma etapa de busca e remoção como nas Árvores Binárias de Pesquisa convecionais. Então se alguma propriedade da árvore for violada, a árvore deve ser rebalanceada, sendo um procedimento um pouco mais complicado que inserir um nó.

O procedimento para eliminar um nó de uma árvore RedBlack é baseado no procedimento remove_RBTree(), onde primeiramente é necessário a utilização da sub-função RBTransplant() que é chamada na função de remoção como é possível visualizar nos respectivos pseudocódigos abaixo:

RBTransplant(T,u,v)
1 if u.p == T.nil
2   T.raiz = v
3 else if u == u.p.esquerda
4    u.p.esquerda = v
5 else u.p.direita = v
6 v.p = u.p

Explicação: Essa sub-função faz com que seja substituido uma subárvore como um filho de seu pai por uma outra subárvore e quando é feita essa substituição, a subárvore enraizada no nó u pela subárvore enraizado no nó v, o pai do nó u tornase o pai do nó v, e o pai de u acaba ficando com v como seu filho adequado.

remove_RBTree(T,z)
1 y = z
2 y.cor.original = y.cor
3 if z.esquerda == T.nil
4     x = z.direita
5     RBTransplant(T,z,z.direita)
6 else if z.direita == T.nil
7     x = z.esquerda
8     RBTransplant(T,z,z.esquerda)
9 else y = Tree-Minimum(z.direita)
10     y.cor.original = y.cor
11     x = y.direita
12     if y.p == z
13         x.p = y
14     else RBTransplant(T,y,y.direita)
15         y.direita = z.direita
16         y.direita.p = y
17     RBTransplant(T,z,y)
18     y.esquerda = z.esquerda
19     y.esquerda.p = y
20     y.cor = z.cor
21 if y.cor.original == PRETO
22     RBDeleteFixUp(T,x)
  • T representa uma estrutura de árvore e u, v e z um nó.

Após ser feita a remoção do nó z, remove_RBTree() chama um procedimento auxiliar RBDeleteFixUp(), que muda as cores e executa rotações para restaurar as propriedades da árvore RedBlack, sendo necessário observar em uma remoção os seguintes casos para que se mantenha em conformidade com suas propriedades:

Caso 1: É feita a verificação se o pai do nó não é nulo, se for vai para o segundo caso

Caso 2: Se o nó e seu pai forem pretos e seu irmão for vermelho, o pai deve ser pintado de vermelho e o irmão de preto e então se o nó for filho esquerdo, faz a rotação à esquerda de seu pai e vai pro próximo caso, se for filho direito, rotaciona o pai à direita e vai pro próximo caso

Caso 3: Nesse caso, se o pai do nó, o irmão, o filho esquerdo e direito do irmão forem todos pretos, pinta o irmão de vermelho e volte para o primeiro caso com o pai do nó, se não forem vai pro próximo caso.

Caso 4: Se o irmão e o filho esquerdo e direito do irmão forem pretos e o pai do nó for vermelho, deve pintar o irmão de vermelho e o pai do nó de preto, se não deve prosseguir para o próximo caso.

Caso 5: Se o nó for filho esquerdo e o filho direito do irmão for preto deverá pintar o irmão de vermelho e o filho esquerdo do irmão de preto e aí sim rotacionar à direita o irmão, mas se o nó for filho direito deverá pintar o irmão de vermelho e o filho direito do irmão de preto e então rotacionar para esquerda o irmão, indo para o último caso

Caso 6: Ao chegar nesse último caso deverá pintar o pai do nó de preto, caso o nó seja filho esquerdo, pinta o filho direito do irmão do nó de preto e rotaciona o pai do nó para a esquerda, se o nó for filho direito, pinta o filho esquerdo do irmão de preto e rotaciona o pai para direita.

Ao sair do encadeamento de casos, poderá ser feita a remoção do nó naturalemnte

Testes utilizando Árvore RedBlack

Após ser feita a implementação de todos os algoritmos básicos presentes em uma Árvore RedBlack foi possível realizar os testes propostos em Apresentação obtendo então os seguintes resultados informados na tabela a seguir:

Tabela 6: Tempos médios de comparações para pesquisa, remoção e motagem da estrutura de Árvore RedBlack.

Onde os mesmos são fornecidos pelo programa desenvolvido da seguinte forma após a sua compilação:

Figura 13: Tempos encontrados após execução do programa como saída para diferentes processos implementados na Árvore RedBlack.


• Pesquisa Binária

O que é uma Pesquisa Binária?

A busca/pesquisa binária é um algoritmo que implementa o paradigma Divisão e Conquista para encontrar um elemento em um vetor ordenado. Uma analogia ao funcionamento desse algoritmo seria a busca de uma palavra em dicionário.

Divisão e Conquista: Consiste em uma técnica de dividr um problema maior recursivamente em problemas menores até que o problema possa ser resolvido diretamente

Ou seja, a busca binária funciona dividindo repetidamente pela metade a porção da lista que deve conter o item, até reduzir as localizações possíveis a apenas uma.

Figura 14: Exemplo de uma busca binária ao elemento 4 de um vetor ordenado.

Complexidade de uma Pesquisa Binária

Considerando um array com N elementos, o tempo de execução de uma Pesquisa Binária é:

Complexidade Caso Situação
$O(1)$ Melhor caso O elemente procurado está no meio do array
$O(log{n})$ Pior caso O elemento não existe
$O(log{n})$ Caso médio O elemento está em uma posição aleatoria

Tabela 7: Complexidade de uma Pesquisa Binária em notação big O.

  • Para se ter uma ideia da sua vantagem, em um array contendo N = 1000 elementos, no pior caso a Busca Sequencial irá executar 1000 comparações, já a Busca Binária irá executar apenas 10 comparações

Sua implementação no algoritmo

Primeiramente para a pesquisa binária ser funcional foi necessário realizar a ordenação dos 4 vectors que continha os diferentes tamanhos de entradas, onde foi utilizado a função sort() se tratando de uma função em STL (Standard Template Library) incluida na biblioteca algorithm presente na linguagem C++ onde é necessário apenas passar o inicio (begin) e fim (end) da estrutura que é requerida a ordenação, sendo possível ver a sua implementação no código desenvolvido para os teste abaixo:

Internamente essa função utiliza IntroSort, que é uma combinação de QuickSort, HeapSort e InsertionSort.

Figura 15: Exemplo de implementação da função sort() no algoritmo desenvolvido onde **vector_500entries** se trata de um vector com 500 números flutuantes.

A utilização da Pesquisa Binária no algoritmo foi necessário após ser feita a proposta em Apresentação onde foi necessário realizar comparações de tempo com as estruturas mencionadas anteriormente e vetores ordenados aplicados à pesquisa binária, analisando o tempo de ordenação dos respectivos vetores com entradas de 500, 5.000, 50.000 e 500.000 números flutuantes e o tempo gasto utilizando a Pesquisa Binária nesses vetores

Sua implementação no algoritmo foi feita utilizando uma função presente dentro da biblioteca bits/stdc++.h que está presente no C++ onde é chamada a função binary_search(start_ptr, end_ptr, num) que retorna true se o elemento estiver presente na estrutura pesquisada, senão retorna false. A variável start_ptr contpem o ponto inicial da busca binária e end_ptr mantém a posição final do espaço de busac binária e num é o valor a ser encontrado, sendo possível de visualizar um exemplo gráfico abaixo da implementação da função no código que foi desenvolvido para testes:

Figura 16: Exemplo de implementação da pesquisa binária no algoritmo desenvolvido onde **vector_500entries** se trata de um vector com 500 números flutuantes e **query_numbers** um vector contendo 10.000 números flutuantes para a pesquisa.

Testes utilizando Pesquisa Binária com vetores ordenados

Após ser feita a implementação de todos os algoritmos básicos presentes em uma Árvore RedBlack foi possível realizar os testes propostos em Apresentação obtendo então os seguintes resultados informados na tabela a seguir:

Tabela 8: Tempos médios de comparações para ordenação de vetores com diferentes entradas e a pesquisa binária aplicada à eles.

Onde os mesmos são fornecidos pelo programa desenvolvido da seguinte forma após a sua compilação:

Figura 17: Tempos encontrados após execução do programa como saída para diferentes processos implementados na Árvore RedBlack.


• Estruturas MAPs

Função std::map

Inicialmente map é um contêiner que armazena elementos em pares chave-valor. Os valores de chave geralmente são usados para classificar e identificar exclusivamente os elementos, enquanto os valores mapeados armazenam o contéudo associado a essa chave, podendo diferir os tipos de chave e valor mapeados, sendo necessário incluir a biblioteca map para a sua utilização.

Os tipos de chave e valores usados para a implementação da função map() no algoritmo de testes foram do tipo int e float respectivamentes como é possível visualizar um exemplo de sua implementação abaixo:

map<int, float> map500entries;

Foi necessária a sua utilização para ser obtido resultados de seu tempo de execução para ser feita a pesquisa utilizando os métodos internos da função, verificando se há ganho maiores no processo comparado à pesquisa binária.

Complexidade

Sua complexidade para pesquisa utilizando sua função inclusa find() (como feito no algoritmo) é $O(log{n})$ mesmo no pior caso, pois os elementos são armazenados internamente como uma Árvore Binária de Pesquisa balanceada, sendo n o número de elementos dentro da map().

Função std::unordered_map

Inicialmente map é um contêiner associativo que contém pares de valores-chave com chaves exclusivas, havendo similiaridade com a estrutura de dados Tabela Hash. Internamente, os elementos não são classificados em nenhuma ordem específica, mas organizados em buckets. Em qual bucket um elemento é colocado depende inteiramente do hash de sua chave, permitindo o acesso rápido a elementos individuais, uma vez que o hash é calculado, se referindo ao bucket exato em que o elemento está localizado. Os tipos de chave e valores usados para a implementação da função unordered_map() no algoritmo de testes foram do tipo int e float respectivamentes como é possível visualizar um exemplo de sua implementação abaixo:

unordered_map<int, float> unordered_map500entries;

Foi necessária a sua utilização para ser obtido resultados de seu tempo de execução para ser feita a pesquisa utilizando os métodos internos da função, verificando se há ganho maiores no processo comparado à pesquisa binária.

Complexidade

Sua complexidade para pesquisa utilizando sua função inclusa find() (como feito no algoritmo) é $O(1)$ mesmo no pior caso, pois os elementos são armazenados em uma Tabela Hash e, portanto, a chave atua como um índice durante a pesquisa em mapas não ordenados. Considerando n o número de elementos dentro da unordered_map()

Testes utilizando as estruturas MAPs

Após ser feita a implementação da função std::map e std::unordered_map e sua respectivas funções de pesquisa, foi possível realizar os testes propostos em Apresentação obtendo então os seguintes resultados informados nas tabelas a seguir:

Tabela 9: Tempos médios de comparações para pesquisa utilizando a função map() com diferentes tamanhos de entrada.

Tabela 10: Tempos médios de comparações para pesquisa utilizando a função map() com diferentes tamanhos de entrada.

Onde os mesmos são fornecidos pelo programa desenvolvido da seguinte forma após a sua compilação:

Figura 18: Tempos encontrados após execução do programa como saída para diferentes processos implementados na Árvore RedBlack.


Resultados e Análises

• Após todo o código ser implementado com suas respectivas estruturas funcionando, foi possível realizar testes para obtenção do resultados de tempo de cada tipo de estrutura mencionado na proposta, onde foi possível obter os valores que estão presentes na tabela 2, tabela 4, tabela 6, tabela 8, tabela 9 e tabela 10, mostrando seus tempos médios para os diferentes tamanhos de entradas e processos, possibilitando a criação dos seguintes gráficos a seguir:

Figura 19: Gráficos de tempo (s) em relação aos tipos diferentes de árvores ao serem utilizadas nos processos de pesquisa e remoção com 500 e 5.000 entradas.

Figura 20: Gráficos de tempo (s) em relação aos tipos diferentes de árvores ao serem utilizadas nos processos de pesquisa e remoção com 50.000 e 500.000 entradas.

Figura 21: Gráfico contendo a média de tempo (eixo y) dos diferentes tamanhos de entrada utilizado para a montagem da estrutura de cada árvore (eixo x)

Figura 22: Gráfico contendo a média de tempo (eixo y) dos diferentes tamanhos de entrada utilizado para a pesquisa de cada estruturas citada (eixo x)

Observando que foi utilizado um total de 10.000 números do tipo flutuante para realizar as pesquisas nas respectivas estruturas, considerando que:

  • Há um total de 3 números iguais no conjunto de 10.000 números em relação a entrada de 500 números, sendo 0.6% do total de entradas mencionada;
  • Há um total de 10 números iguais no conjunto de 10.000 números em relação a entrada de 5.000 números, sendo 0.2% do total de entradas mencionada;
  • Há um total de 66 números iguais no conjunto de 10.000 números em relação a entrada de 50.000 números, sendo 1.32%% do total de entradas mencionada;
  • Há um total de 601 números iguais no conjunto de 10.000 números em relação a entrada de 500.000 números, sendo 12.02%% do total de entradas mencionada.

•Todos os teste foram realizados em uma máquina com as seguintes especificações, sendo elementos ocasionais de incertezas nas medições de tempo:

Processador: AMD Ryzen 5 1600;

Mémoria : 16 GB - 1600 Mhz;

Sistema Operacional: Linux - Ubuntu 20.04 LTS


Conclusão

Montagem em estrutura

  • Levando em consideração esses valores apresentados em relação aos resultados obtidos de tempo foi possível analisar através de comparações que para montagem em estrutura a Árvore Binária de Pesquisa se destaca por não possuir nenhuma limitação em sua inserção como as outras árvores possuem para garantir o balanceamento, ou seja, pela Árvore Binária de Pesquisa não ser uma estrutura balanceada sem condições de construção, se torna uma estrutura mais eficiente na montagem. Ao contrário disso, foi possível visualizar que a Árvore RedBlack obteve uma menor eficiência em sua montagem em estrutura por se tratar de uma árvore que, entre as árvores balanceadas envolvidas nos testes, possui um maior número de casos para serem tratadas na inserção em mémoria, tornando o processo de montagem um pouco mais lento, havendo uma diferença de 0,060821334 segundos na montagem em estrutura entre a Árvore Binária de Pesquisa e Árvore RedBlack.

Pesquisa

  • Utilizando de base os resultados obtidos e estruturados, foi possível notar pela comparação das médias uma diferença positiva para a Árvore RedBlack, sendo seguida da Árvore AVL e por último a Árvore Binária de Pesquisa em ranking de velocidade de execução durante as pesquisas para até 50.000 entradas, sendo explicado pela falta de balanceamento da Árvore Binária de Pesquisa, tornando-a mais lenta. Após ser realizado o último testes com um total de 500.000 entradas foi possível observar que obteve uma diferença em relação ao ranking de tempos onde a Árvore Binária de Pesquisa tomou a liderança de mais rápida, seguinda pela Árvore AVL ficando em ultimo lugar a Árvore RedBlack sendo a estrutura que demorou mais tempo para realizar pesquisas, podendo ocasionar essa diferença na maior quantidade de entradas por possuir cerca de 12% de seus elementos presentes nos 10.000 números utilizados para consulta e por ser uma árvore um pouco menos estritamente equilibrida que a Árvore AVL como mencionado nas explicações das estruturas acima. Apesar da estrutura Árvore AVL apresentar resultado mediano em todos os teste é levado em consideração a massa de dados iguais contidas na árvore em relação as 10.000 entradas de consulta pois é vísivel em literatura [1] que a Árvore AVL se destaca na eficiência de pesquisa em relação a Árvore RedBlack.

Remoção

  • Com esses mesmos dados foi concluido que no processo de remoção de elementos das estruturas estudadas, entre elas se destacou novamente a Árvore RedBlack, agora em sincronia com conteúdos encontrados na literatura [1] é possível concordar com os resultados forncedios pelo teste, em sequência no ranking está situado a Árvore AVL e por fim a Árvore Binária de Pesquisa. A Árvore RedBlack se destaca nesse processo em relação a outros tipos de árvores mencionados pois menos rotações são realizadas devido ao balancemaneto relativamente relaxado.

Comparações com vetores

  • Utilizando os testes citados anteriormente em comparação com teste realizados é possível visualizar utilizando como referência o gráfico apresentado na Figura 22 que estruturas do tipo Árvore apresentam maior eficiência em pesquisa de elementos quando comparadas com utilização da Busca Binária em vetores ordenados, havendo um diferença de tempo de 0,00040225 segundos entre a Árvore Binária de Pesquisa (estrutura menos eficiente para pesquisa) e a pesquisa em vetores ordenados, onde para a implementação da Pesquisa Binária foi necessária a utilização da função sort() como mencionada anteriormente na explicação das estruturas, podendo visualizar os tempos de ordenação para os diferentes tamanhos de entradas no gráfico abaixo:

Figura 23: Gráfico contendo a média de tempo (eixo y) dos diferentes tamanhos de entrada utilizado para a ordenação de vetores (eixo x)

Utilização de estruturas MAPs

  • Ao adotar as estruturas MAPs mencionadas em MAPs foi realizado testes, possibilitando as seguintes conclusões.

    Para a utilização da função std::map() em comparação com as demais estruturas analisadas com testes de pesquisa foi possível observar que obteve um eficiência maior tendo uma diferença 0,000828749 segundos entre a Árvore RedBlack (segunda estrutura mais eficiente para pesquisa) e a utilização da estrutura map() para pesquisa, essa eficiência deve se a sua maneira de armazenar elementos internamente como Árvore Binária de Pesquisa balanceada, tendo uma complexidade $O(log{n})$ mesmo no pior caso. É possível observar esse ganho de eficiência em comparação com as outras estruturas no gráfico plotado abaixo:

Figura 24: Gráfico contendo a média de tempo (eixo y) dos diferentes tamanhos de entrada utilizado para a ordenação de diferentes estruturas (eixo x)

Para a utilização da função std::unordered_map() foi feita também testes de pesquisas, possibilitando a sua comparação com os testes de pesquisas feitos com as estruturas presentes no código, onde foi possível observar um ganho de eficiência ainda maior comparado com a ultima estrutura implementada std::map reduzindo o tempo de pesquisa em 0,002289 segundos (quase um terço do seu tempo) entre ela, possuindo uma diferença de 0,00311775 segundos da Árvore RedBlack (estrutura em árvore mais eficiente em pesquisa). Essa estrutura apresenta esse ganho de tempo no processo de pesquisa por armazenar elementos usando Tabela Hash, buscando uma chave específica torna sua complexidade para a operação de $O(1)$. É possível observar esse ganho de eficiência em relação as outras estruturas no gráfico plotado abaixo:

Figura 25: Gráfico contendo a média de tempo (eixo y) dos diferentes tamanhos de entrada utilizado para a ordenação de diferentes estruturas (eixo x)


Bibliotecas

Para o funcionamento do programa, é necessário incluir as seguintes bibliotecas:

  • #include 'iostream'
  • #include 'stdio.h'
  • #include 'stdbool.h'
  • #include 'stdlib.h'
  • #include 'vector'
  • #include 'fstream'
  • #include 'random'
  • #include 'iomanip'
  • #include 'string'
  • #include 'sstream'
  • #include 'algorithm'
  • #include 'bits/stdc++.h'
  • #include 'ctime'
  • #include 'unordered_map'
  • #include 'map'

Referências

[1] CORMEM, Thomas H. et al. Algoritmos: Teoria e Prática. 3. ed. Cambridge, Massachussetts: MIT Press, [2009]. 1292 p. ISBN 9780262033848.

[2] THAPLIYAL, Rohit. Map vs unordered_map in C++. [S. l.]: GeeksforGeeks, 7 jul. 2022. Disponível em: https://www.geeksforgeeks.org/map-vs-unordered_map-c/. Acesso em: 15 out. 2022.

[3]STROUSTRUP, B. Cplusplus. Disponível em: https://cplusplus.com. Acesso em: 17 out. 2022.


Compilação e Execução

O programa feito de acordo com a proposta possui um arquivo Makefile que realiza todo o procedimento de compilação e execução. Para tanto, temos as seguintes diretrizes de execução:

Comando Função
make clean Apaga a última compilação realizada contida na pasta build
make Executa a compilação do programa utilizando o gcc, e o resultado vai para a pasta build
make run Executa o programa da pasta build após a realização da compilação

Autor

Desenvolvido por Pedro Henrique Louback Campos

Aluno do 4° periodo do curso de Engenharia de Computação no CEFET-MG

About

Work related to comparing different types of data structures

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published