Implementando uma árvore binária em Java

Implementando uma árvore binária em Java

1. Introdução

Neste artigo, vamos cobrir a implementação de uma árvore binária em Java.

Para o propósito deste artigo,we’ll use a sorted binary tree that will contain int values.

2. Árvore binária

Uma árvore binária é uma estrutura de dados recursiva onde cada nó pode ter 2 filhos no máximo.

Um tipo comum de árvore binária é uma árvore de pesquisa binária, na qual todo nó tem um valor maior ou igual aos valores do nó na subárvore esquerda e menor ou igual aos valores do nó na subárvore direita árvore.

Aqui está uma rápida representação visual deste tipo de árvore binária:

image

Para a implementação, usaremos uma classe auxiliarNode que armazenará os valores deint e manterá uma referência para cada filho:

class Node {
    int value;
    Node left;
    Node right;

    Node(int value) {
        this.value = value;
        right = null;
        left = null;
    }
}

Então, vamos adicionar o nó inicial de nossa árvore, normalmente chamadoroot:

public class BinaryTree {

    Node root;

    // ...
}

3. Operações Comuns

Agora, vamos ver as operações mais comuns que podemos realizar em uma árvore binária.

3.1. Inserindo elementos

A primeira operação que vamos cobrir é a inserção de novos nós.

Primeiro,we have to find the place where we want to add a new node in order to keep the tree sorted. Seguiremos estas regras a partir do nó raiz:

  • se o valor do novo nó for menor do que o do nó atual, vamos para o filho esquerdo

  • se o valor do novo nó for maior do que o do nó atual, vamos para o filho certo

  • quando o nó atual énull,, chegamos a um nó folha e podemos inserir o novo nó nessa posição

Primeiro, vamos criar um método recursivo para fazer a inserção:

private Node addRecursive(Node current, int value) {
    if (current == null) {
        return new Node(value);
    }

    if (value < current.value) {
        current.left = addRecursive(current.left, value);
    } else if (value > current.value) {
        current.right = addRecursive(current.right, value);
    } else {
        // value already exists
        return current;
    }

    return current;
}

A seguir, criaremos o método público que inicia a recursão do nóroot:

public void add(int value) {
    root = addRecursive(root, value);
}

Agora vamos ver como podemos usar este método para criar a árvore do nosso exemplo:

private BinaryTree createBinaryTree() {
    BinaryTree bt = new BinaryTree();

    bt.add(6);
    bt.add(4);
    bt.add(8);
    bt.add(3);
    bt.add(5);
    bt.add(7);
    bt.add(9);

    return bt;
}

3.2. Encontrando um Elemento

Vamos agora adicionar um método para verificar se a árvore contém um valor específico.

Como antes, primeiro criaremos um método recursivo que atravessa a árvore:

private boolean containsNodeRecursive(Node current, int value) {
    if (current == null) {
        return false;
    }
    if (value == current.value) {
        return true;
    }
    return value < current.value
      ? containsNodeRecursive(current.left, value)
      : containsNodeRecursive(current.right, value);
}

Aqui, estamos pesquisando o valor comparando-o com o valor no nó atual e, em seguida, continue no filho esquerdo ou direito dependendo disso.

A seguir, vamos criar o método público que começa emroot:

public boolean containsNode(int value) {
    return containsNodeRecursive(root, value);
}

Agora, vamos criar um teste simples para verificar se a árvore realmente contém os elementos inseridos:

@Test
public void givenABinaryTree_WhenAddingElements_ThenTreeContainsThoseElements() {
    BinaryTree bt = createBinaryTree();

    assertTrue(bt.containsNode(6));
    assertTrue(bt.containsNode(4));

    assertFalse(bt.containsNode(1));
}

Todos os nós adicionados devem estar contidos na árvore.

3.3. Excluindo um Elemento

Outra operação comum é a exclusão de um nó da árvore.

Primeiro, precisamos encontrar o nó a ser excluído da mesma maneira que fizemos antes:

private Node deleteRecursive(Node current, int value) {
    if (current == null) {
        return null;
    }

    if (value == current.value) {
        // Node to delete found
        // ... code to delete the node will go here
    }
    if (value < current.value) {
        current.left = deleteRecursive(current.left, value);
        return current;
    }
    current.right = deleteRecursive(current.right, value);
    return current;
}

Depois de encontrarmos o nó a ser excluído, há três casos diferentes principais:

  • a node has no children – este é o caso mais simples; só precisamos substituir este nó pornull em seu nó pai

  • a node has exactly one child – no nó pai, substituímos este nó por seu único filho.

  • a node has two children - este é o caso mais complexo porque requer uma reorganização da árvore

Vamos ver como podemos implementar o primeiro caso quando o nó é um nó folha:

if (current.left == null && current.right == null) {
    return null;
}

Agora vamos continuar com o caso em que o nó tem um filho:

if (current.right == null) {
    return current.left;
}

if (current.left == null) {
    return current.right;
}

Aqui, estamos retornando o filhonon-null para que possa ser atribuído ao nó pai.

Finalmente, temos que lidar com o caso em que o nó tem dois filhos.

Primeiro, precisamos encontrar o nó que substituirá o nó excluído. Usaremos o menor nó do nó a ser excluído da subárvore direita:

private int findSmallestValue(Node root) {
    return root.left == null ? root.value : findSmallestValue(root.left);
}

Em seguida, atribuímos o menor valor ao nó a ser excluído e, depois disso, vamos excluí-lo da subárvore certa:

int smallestValue = findSmallestValue(current.right);
current.value = smallestValue;
current.right = deleteRecursive(current.right, smallestValue);
return current;

Finalmente, vamos criar o método público que inicia a exclusão deroot:

public void delete(int value) {
    root = deleteRecursive(root, value);
}

Agora, vamos verificar se a exclusão funciona conforme o esperado:

@Test
public void givenABinaryTree_WhenDeletingElements_ThenTreeDoesNotContainThoseElements() {
    BinaryTree bt = createBinaryTree();

    assertTrue(bt.containsNode(9));
    bt.delete(9);
    assertFalse(bt.containsNode(9));
}

4. Atravessando a árvore

Nesta seção, veremos diferentes maneiras de atravessar uma árvore, cobrindo em detalhes as pesquisas em primeiro lugar e em largura.

Usaremos a mesma árvore que usamos antes e mostraremos a ordem de passagem para cada caso.

A pesquisa em profundidade é um tipo de travessia que se aprofunda o máximo possível em cada criança antes de explorar o próximo irmão.

Existem várias maneiras de realizar uma pesquisa aprofundada: em ordem, pré-encomenda e pós-encomenda.

O percurso em ordem consiste em visitar primeiro a subárvore esquerda, depois o nó raiz e, finalmente, a subárvore direita:

public void traverseInOrder(Node node) {
    if (node != null) {
        traverseInOrder(node.left);
        System.out.print(" " + node.value);
        traverseInOrder(node.right);
    }
}

Se chamarmos esse método, a saída do console mostrará o percurso em ordem:

3 4 5 6 7 8 9

A passagem de pré-encomenda visita primeiro o nó raiz, a seguir a subárvore esquerda e, finalmente, a subárvore direita:

public void traversePreOrder(Node node) {
    if (node != null) {
        System.out.print(" " + node.value);
        traversePreOrder(node.left);
        traversePreOrder(node.right);
    }
}

E vamos verificar a passagem da pré-encomenda na saída do console:

6 4 3 5 8 7 9

A travessia pós-pedido visita a subárvore esquerda, a subárvore direita e o nó raiz no final:

public void traversePostOrder(Node node) {
    if (node != null) {
        traversePostOrder(node.left);
        traversePostOrder(node.right);
        System.out.print(" " + node.value);
    }
}

Aqui estão os nós em pós-ordem:

3 5 4 7 9 8 6

Este é outro tipo comum de passagem quevisits all the nodes of a level before going to the next level.

Esse tipo de travessia também é chamado de ordem de nível e visita todos os níveis da árvore, começando pela raiz e da esquerda para a direita.

Para a implementação, usaremos umQueue para manter os nós de cada nível em ordem. Vamos extrair cada nó da lista, imprimir seus valores e, em seguida, adicionar seus filhos à fila:

public void traverseLevelOrder() {
    if (root == null) {
        return;
    }

    Queue nodes = new LinkedList<>();
    nodes.add(root);

    while (!nodes.isEmpty()) {

        Node node = nodes.remove();

        System.out.print(" " + node.value);

        if (node.left != null) {
            nodes.add(node.left);
        }

        if (node.right!= null) {
            nodes.add(node.right);
        }
    }
}

Nesse caso, a ordem dos nós será:

6 4 8 3 5 7 9

5. Conclusão

Neste artigo, vimos como implementar uma árvore binária classificada em Java e suas operações mais comuns.

O código-fonte completo dos exemplos está disponívelover on GitHub.