Algoritmo de busca binária em JavaScript

No tutorial desta semana veremos como implementar um sistema de busca binária em JavaScript e como estruturas recursivas podem nos auxiliar quando trabalhamo com listas e ordenação.

Estrutura de dados

Uma estrutura de dados refere-se a forma como os dados são armazenados e organizados. Um texto por exemplo é como uma grande cadeia de caracteres, enquanto que os registros armazenados no banco de dados podem ser vistos como uma tabela bidimensional composta por linhas e colunas.

Algumas estruturas de dados são consideradas básicas como matrizes, listas, pilhas e filas. Hoje iremos falar de uma estrutura conhecida como Árvore.

Não se preocupe, pois não será necessário revisar os conceitos de biologia, uma vez que o nome árvore provém do fato de que esta estrutura obedece a uma organização hierárquica semelhante a um organograma.

Um pequeno resumo para entendermos:

  • Cada posição de uma árvore é chamada de ;
  • O nó no topo é o nó raiz;
  • Os nós no outro extremo são nós terminais (ou nós folha);
  • O número de nós seguindo pelo caminho mais longo até chegar ao nó terminal se refere a profundidade da árvore;
  • Um nó qualquer junto com nós abaixo dele forma uma estrutura menor chamada de subárvore ou ramo;
  • Podemos falar em ramo a esquerda ou ramo a direita dependendo de como a árvore é mostrada;
  • Um ancestral direto de um nó é o nó pai, assim como um descendente direto é um nó filho;
  • altura de um nó é a distância entre esse nó e o seu descendente mais afastado;
  • Por fim, uma árvore binária é uma árvore onde cada pai não tem mais de dois filhos.

arvore_binaria

Essas estruturas são simuladas na memória, ou seja, a memória é uma sequência de células de memória endereçáveis, por isso seria inviável organizar os dados como matrizes, listas e árvores, no entanto como essa simulação ocorre já é assunto para outro tópico.

Recursividade

recursividade

Como você já deve saber um laço de repetição implica repetir um conjunto de instruções até atingir a condição desejada.

Enquanto (condição) Faça

    (bloco de código)

Fim Enquanto

A recursão envolve repetir um conjunto de instruções como uma tarefa em si mesma. Um exemplo muito simples para ficar mais claro é o de uma função que retorna a soma dos números até 0.

Função soma(inteiro num) retorna Inteiro

Inicio

   Se (num > 0)

   Então

      Função_Retorna(num + soma(num - 1));

   Senão

      Função_Retorna(0);

   Fim_Se

Fim

Cada vez que a função é chamada em si mesma, passa a ser executada uma nova cópia desta função em que os parâmetros são independentes da chamada anterior. Essa ideia visa dividir um problema em problemas menores do mesmo tipo através de iterações sucessivas, técnica conhecida como divisão e conquista.

Algoritmo de busca binária

Agora podemos falar sobre o algoritmo de busca binária 😀

Vamos supor que desenvolvemos um programa de busca sequencial, então imagine uma lista contendo 10 nomes onde iremos procurar um nome específico utilizando este programa, Joãozinho por exemplo. Sem muito esforço poderíamos comparar este nome com cada um dos nomes desta lista, ao encontrar, nosso problema está resolvido. A questão é, em quantas etapas isso foi realizado, ou seja, quantas vezes foi necessário comparar um nome com outro sequencialmente para saber se Joãozinho estava na lista? Talvez duas ou então cinco, ou no pior dos casos dez vezes, dependendo da ordem em que os nomes aparecem.

Muito bem, agora vamos pensar em outra situação hipotética, uma lista contendo os títulos de todos os livros de uma biblioteca com um acervo de mais de 100 milhões de itens. Neste caso para encontrar o título desejado você teria duas opções, sentar, tomar um café e aguardar a execução do programa ou encontrar uma solução mais eficiente.

Apesar de tentadora a ideia de ficar sentado tomando um café vamos desenvolver o algoritmo.

A brincadeira funciona assim:

  1. A busca será um sucesso se encontrarmos o valor que procuramos, caso contrário, será uma falha;
  2. Não pode haver valores duplicados;
  3. Começamos a busca vendo se a lista está vazia consultado o valor do nó raiz. Se estiver obviamente a busca falhou. Caso contrário, seguimos para o nó subjacente;
  4. Sempre que avançamos de um nó para outro devemos obedecer às seguintes regras: se o valor for menor do que o valor de entrada, avançamos para o nó da esquerda, se for maior, avançamos para o nó da direita e se for igual a busca foi um sucesso.
  5. Isso é tudo!

Vejamos uma implementação em JavaScript:

function ArvoreBinaria() {
    this.noRaiz = null;
}

ArvoreBinaria.prototype = {

    constructor: ArvoreBinaria,

    // Adiciona um novo valor
    adicionar: function(valor) {

        // Cria um novo objeto para armazenar o valor atual
        var novoNo = {
            valor: valor,
            esq: null,
            dir: null
        };

        var noAtual;

        if (this.noRaiz == null) {
            // Caso a árvore esteja vazia
            this.noRaiz = novoNo;
        } else {
            noAtual = this.noRaiz;

            while (true) {
                if (valor < noAtual.valor) {
                    // Atribuímos um novo nó a esquerda caso não exista
                    // ou seguimos adiante usando o nó da esquerda como nó atual
                    if (noAtual.esq == null) {
                        noAtual.esq = novoNo;
                        break;
                    } else {
                        noAtual = noAtual.esq;
                    }
                } else if (valor > noAtual.valor) {
                    // Atribuímos um novo nó a direita caso não exista
                    // ou seguimos adiante usando o nó da direita como nó atual
                    if (noAtual.dir == null) {
                        noAtual.dir = novoNo;
                        break;
                    } else {
                        noAtual = noAtual.dir;
                    }
                } else {
                    break;
                }
            }
        }
    },

    // Verifica se o valor existe ou não na árvore
    contem: function(valor) {
        var encontrado = false,
            noAtual = this.noRaiz;

        while (!encontrado && noAtual) {

            // Se o valor é menor que o do nó atual, atribua o nó da esquerda
            if (valor < noAtual.valor) {
                noAtual = noAtual.esq;
            }

            // Se o valor é maior que o do nó atual, atribua o nó da direita
            else if (valor > noAtual.valor) {
                noAtual = noAtual.dir;
            }

            // Valor do nó atual é igual ao valor passado como parâmetro
            else {
                encontrado = true;
            }
        }

        return encontrado;

    },

    // Retorna o número de níveis entre o nó atual e o descendente 
    // mais afastado, seguindo o caminho mais longo
    altura: function(no) {
        if (!no) return 0;
        var alturaEsq = this.altura(no.esq);
        var alturaDir = this.altura(no.dir);

        return Math.max(alturaEsq, alturaDir) + 1;
    },

    // Percorre a árvore e exibe os valores em ordem crescente no console
    percorrer: function(no) {
        if (no) {
            this.percorrer(no.esq);
            console.log(no.valor);
            this.percorrer(no.dir);
        }
    },

    // O menor é o nó do mais baixo nível a esquerda
    menorNo: function(no) {
        if (!no) {
            return 0;
        }
        if (no.esq) {
            return this.menorNo(no.esq);
        }
        return no.valor;
    },

    // O maior é o nó do mais baixo nível a direita
    maiorNo: function(no) {
        if (!no) {
            return 0;
        }
        if (no.dir) {
            return this.maiorNo(no.dir);
        }
        return no.valor;
    }
};

Na classe árvore binária adicionamos 6 métodos. Os métodos adicionar e contem seguem a mesma ideia, isto é, conferir nó por nó dentro do laço de repetição. Note que os outros 4 métodos fazem uso da recursividade e nos dão algumas informações extras sobre a estrutura. Podemos ver o resultado de tudo isso logo abaixo:

var arvore = new ArvoreBinaria();
arvore.adicionar(1);
arvore.adicionar(2);
arvore.adicionar(7);
arvore.adicionar(5);

arvore.percorrer(arvore.noRaiz); // 1, 2, 5, 7
arvore.contem(7); // true
arvore.maiorNo(arvore.noRaiz); // 7
arvore.menorNo(arvore.noRaiz); // 1
arvore.altura(arvore.noRaiz); // 4

Referências:

JS: Binary Search Tree

Computer science in JavaScript

Árvores Binárias

Deixe um comentário