domingo, 21 de abril de 2013

Árvore Binária de Busca - Método para inserção (java)

No post anterior expliquei alguns dos principais conceitos sobre as Árvores Binárias de Busca, e também mostrei como realizar a busca por um elemento. Neste post vamos ver como pode ser feita a inserção de novos nós.

O método inserir (insert)


A inserção de um novo nó pressupõe a busca pelo local no qual devemos colocá-lo. Sendo assim, o método insert deve se parecer bastante com o método search. A seguir colocarei a minha implementação, que dividi em duas partes, e em seguida explicarei melhor cada uma delas.
public void insert(int newKey){
  if(this.root == null){
   this.root = new Node(newKey);
   this.root.parent = null;
  }
  else
   insertHelper(this.root, newKey);   
}
O método insert descrito acima é bastante simples, recebe apenas a chave a ser inserida como parâmetro e retorna void (não retorna nada). Na verdade este método só cobre o caso da árvore vazia, ou seja, o nó raiz(root) é nulo. Caso a árvore não esteja vazia, ele chamará um método auxiliar para realizar a inserção, passando a chave a ser inserida e o nó raiz da árvore.

Segue o código do método auxiliar.
private void insertHelper(Node node, int newKey){
  if(newKey < node.key){
   if(node.left == null){
    node.left = new Node(newKey);
    node.left.parent = node;
   }
   else
    insertHelper(node.left, newKey);
  }
  else if(newKey > node.key){
   if(node.right == null){
    node.right = new Node(newKey);
    node.right.parent = node;
   }
   else
    insertHelper(node.right, newKey);
  }
  else
   return;
}
Note que existe um "private" na assinatura do método insertHelper, este modificador de acesso é usado para garantir que nenhum objeto fora da classe BST possa utilizar insertHelper. Assim temos a segurança de que ninguém pode inserir novos nós em nossa árvore utilizando o método insertHelper. É importante observar que o método insert, porém, é público, pois ele recebe apenas a chave que vai ser inserida e não pode inserir essa chave em outra árvore que não seja a árvore que o invocou. InsertHelper, por outro lado, poderia receber um nó de qualquer árvore como atributo!

Temos uma chave a ser inserida e um nó, já sabemos como funciona uma ABB, então vamos separar a explicação do algoritmo por casos.

  1. A nova chave (newKey) é menor do que a chave presente no nó:
    • Como newKey é menor do que node.key, temos que seguir pela subárvore à esquerda.
    • Então verificamos se node.left é nulo.
    • Se node.left é nulo, sabemos que newKey deve ser o filho esquerdo de node (linhas 4,5).
    • O próximo comando informa que node é o pai (parent) de node.left (nó recém criado).
    • Se node.left não for nulo, significa que node já tem filho esquerdo, então temos que analisá-lo. Isto será feito chamando novamente o método insertHelper, passando como parâmetro newKey e o filho esquerdo de node.
  2. A nova chave (newKey) é maior do que a chave presente no nó:
    • Como newKey é maior do que node.key, temos que seguir pela subárvore à direita.
    • Então verificamos se node.right é nulo.
    • Se node.right é nulo, sabemos que newKey deve ser o filho direito de node (linhas 12,13).
    • O próximo comando informa que node é o pai (parent) de node.right (nó recém criado).
    • Se node.right não for nulo, significa que node já tem filho direito, então temos que analisá-lo. Isto será feito chamando novamente o método insertHelper, passando como parâmetro newKey e o filho direito de node.
  3. Nenhum dos casos anteriores foi verdadeiro:
    • Se atingirmos o else, obviamente, encontramos um nó com chave igual a newKey.
    • Visto que a chave já está presente na árvore, podemos simplesmente retornar.

O próximo post será sobre algoritmos para percorrimento em árvores!

Qualquer crítica ou sugestão, por favor, postem nos comentários. Assim posso melhorar o texto. :)

Árvore Binária de Busca - Conceitos e o método Search (Java)

Neste post vou falar sobre alguns conceitos e motivações ao das Árvores Binárias de Busca, também abordarei um de seus principais métodos, a busca (search). Os códigos disponibilizados estão em Java, mas os conceitos apresentados podem ser utilizados em qualquer linguagem de programação.

Considerações sobre a Árvore Binária de Busca


Uma Árvore Binária de Busca, que não está vazia, é uma estrutura composta por nós. Existe um nó especial, chamado raiz(root), e os demais nós da árvore podem ser agrupados em dois subconjuntos disjuntos, que são as subárvores esquerda e direita do nó raiz, as quais também são árvores binárias.

Uma das maiores vantagens das Árvores Binárias de Busca, em comparação com outras estruturas de dados, é que os seus algoritmos de ordenação e de busca podem ser muito eficientes. Por exemplo, a operação de busca tem custo de pior caso O(log n) em uma Árvore Binária de Busca, visto que, mesmo no pior caso, não precisamos percorrer todos os nós! As operações de inserção e remoção também tem O(log n) como complexidade de pior caso, pois para inserir ou remover precisamos apenas buscar o nó a ser inserido ou removido.

Os nós de uma Árvore Binária de Busca devem ser construídos de forma que tenham um campo chave(key), do tipo da chave que você precisar guardar, e dois campos do tipo nó, que serão ponteiros para os filhos(subárvores) direita e esquerda. Pode haver também um outro campo do tipo nó que aponte para o pai(parent) do nó em questão. 

Abaixo colocarei um exemplo de código da classe nó(node).
public class Node {
 
 protected int key;
 protected Node left;
 protected Node right;
 protected Node parent;
 
 public Node(int key){
  this.key = key;
 }
 public Node(){}
  
}


Notem que existem dois construtores, um padrão que não recebe argumentos, e o outro recebe um argumento do tipo inteiro. O primeiro retornará um nó com os atributos não inicializados, já o segundo inicializará o atributo key com o inteiro recebido pelo construtor.

Já a implementação da classe da Árvore Binária de Busca pode ser feita da seguinte forma.
public class BST {
 Node root;
 
 public BST(){
  this.root = null;
 }
 //métodos
}

O nome BST vem de Binary Search Tree. A classe tem um nó raiz que é inicialmente nulo, até que seja feita alguma inserção. Não coloquei os métodos para que o código fique resumido, mas os apresentarei um por um, explicando a implementação e propósito dos mesmos.

Antes de tudo, precisamos entender como funciona uma Árvore Binária de busca, para então podermos partir para a implementação de seus métodos. Como já citado anteriormente, cada nó possui apenas dois filhos(subárvores), sendo que seu filho esquerdo é, obrigatoriamente, menor que o pai, enquanto que o filho direito deve ser maior que o pai. A inserção de novos elementos na árvore deve respeitar essa propriedade.

Segue um exemplo de Árvore Binária de busca.

Agora que já sabemos como funciona uma Árvore Binária de Busca, podemos partir para a implementação de alguns métodos que ajudarão a entender melhor esta estrutura de dados.

O método procurar (search)


Esse é o principal método da ABB(Árvore Binária de Busca), visto que uma das maiores motivações ao uso das ABB é o custo reduzido de sua busca! Procurar por um elemento, nesta estrutura de dados, é como procurar uma palavra no dicionário:

  • Abrimos o dicionário em algum ponto (Análogo à raiz da árvore).
  • Se este ponto for maior do que o que procuramos, descartamos a parte direita e vamos para um novo ponto na parte esquerda (Análogo ao filho esquerdo do nó), ou vice-versa.
  • Seguimos o passo anterior até acharmos o que desejamos, ou descobrirmos que a palavra buscada não se encontra no dicionário. 
Vamos então ao código.
public Node search(int key){
  Node currentNode = this.root;
  
  while(currentNode != null){
   if(key == currentNode.key)
    return currentNode;
   else if(key < currentNode.key)
    currentNode = currentNode.left;
   else
    currentNode = currentNode.right;
  }
  return currentNode;
 }

O método search recebe como parâmetro a chave(key), e retorna o nó que a contém. Os passos realizados peloa algoritmo são:
  • O nó atual(currentNode) recebe o nó raiz da árvore;
  • Enquanto o nó atual for diferente de null, faremos:
    1. Se a chave procurada for igual a chave do nó atual, significa que já encontramos, então retornamos o nó atual.
    2. Se a chave procurada for menor que a chave de currentNode, signifca que temos que procurar na subárvore esquerda, então atribuimos o filho esquerdo do nó atual ao nó atual.
    3. Se os dois casos anteriores não forem verdadeiros, a chave procurada deve ser maior que a chave de currentNode, temos que procurar na subárvore direita, sendo assim atribuimos o filho direito do nó atual ao nó atual.
    4. Estes passos serão repetidos até que a chave seja encontrada e caia no caso 1, e a função retorne o nó atual, ou até alcançarmos um nó nulo e a condição do laço seja quebrada, significando que a chave não está presente na árvore.
  • Caso a condição do laço seja quebrada, obviamente, teremos que currentNode é um nó nulo, e o mesmo será retornado.
Um nó é retornado mesmo que a chave não seja encontrada, então devemos ter o cuidado de testar se o nó retornado pela busca é, ou não, nulo.

O próximo post será sobre a inserção em uma árvore!


Qualquer crítica ou sugestão, por favor, coloquem nos comentários! Assim posso melhorar o texto. :)