Primeiro algoritmo de pesquisa em Java

Primeiro algoritmo de pesquisa em Java

1. Visão geral

Neste tutorial, vamos aprender sobre o algoritmo de pesquisa em largura primeiro, que nos permite pesquisar um nó em uma árvore ou gráfico, viajando através de seus nós primeiro em largura, em vez de primeiro em profundidade.

Primeiro, vamos passar um pouco de teoria sobre esse algoritmo para árvores e gráficos. Depois disso, vamos mergulhar nas implementações dos algoritmos em Java. Finalmente, vamos cobrir sua complexidade de tempo.

2. Algoritmo de pesquisa de amplitude inicial

A abordagem básica do algoritmo de pesquisa da largura da primeira pesquisa (BFS) é procurar um nó em uma estrutura de árvore ou gráfico, explorando vizinhos antes dos filhos.

Primeiro, veremos como esse algoritmo funciona para árvores. Depois disso, vamos adaptá-lo aos gráficos, que têm a restrição específica de às vezes conter ciclos. Finalmente, discutiremos o desempenho deste algoritmo.

2.1. Árvores

A ideia por trás dethe BFS algorithm for trees émaintain a queue of nodes that will ensure the order of traversal. No início do algoritmo, a fila contém apenas o nó raiz. Repetiremos essas etapas, desde que a fila ainda contenha um ou mais nós:

  • Pop o primeiro nó da fila

  • Se esse nó for o que estamos procurando, a pesquisa acabou

  • Caso contrário, adicione os filhos deste nó ao final da fila e repita as etapas

Execution termination is ensured by the absence of cycles. Veremos como gerenciar os ciclos na próxima seção.

2.2. Gráficos

In the case of graphs, devemos pensar nos ciclos possíveis na estrutura. Se simplesmente aplicarmos o algoritmo anterior em um gráfico com um ciclo, ele fará um loop para sempre. Portanto,we’ll need to keep a collection of the visited nodes and ensure we don’t visit them twice:

  • Pop o primeiro nó da fila

  • Verifique se o nó já foi visitado, caso contrário, pule-o

  • Se esse nó for o que estamos procurando, a pesquisa acabou

  • Caso contrário, adicione-o aos nós visitados

  • Adicione os filhos deste nó à fila e repita essas etapas

3. Implementação em Java

Agora que a teoria foi abordada, vamos colocar as mãos no código e implementar esses algoritmos em Java!

3.1. Árvores

Primeiro, vamos implementar o algoritmo da árvore. Vamos projetar nossa classeTree, que consiste em um valor e filhos representados por uma lista de outrosTrees:

public class Tree {
    private T value;
    private List> children;

    private Tree(T value) {
        this.value = value;
        this.children = new ArrayList<>();
    }

    public static  Tree of(T value) {
        return new Tree<>(value);
    }

    public Tree addChild(T value) {
        Tree newChild = new Tree<>(value);
        children.add(newChild);
        return newChild;
    }
}

Para evitar a criação de ciclos, os filhos são criados pela própria classe, com base em um determinado valor.

Depois disso, vamos fornecer um métodosearch():

public static  Optional> search(T value, Tree root) {
    //...
}

Como mencionamos anteriormente,the BFS algorithm uses a queue to traverse the nodes. Em primeiro lugar, adicionamos nosso nóroot a esta fila:

Queue> queue = new ArrayDeque<>();
queue.add(root);

Então, temos que fazer um loop enquanto a fila não está vazia, e cada vez que retiramos um nó da fila:

while(!queue.isEmpty()) {
    Tree currentNode = queue.remove();
}

If that node is the one we’re searching for, we return it, else we add its children to the queue:

if (currentNode.getValue().equals(value)) {
    return Optional.of(currentNode);
} else {
    queue.addAll(currentNode.getChildren());
}

Finalmente, se visitarmos todos os nós sem encontrar aquele que estamos procurando, retornamos um resultado vazio:

return Optional.empty();

Vamos agora imaginar um exemplo de estrutura de árvore:

Tree Example

Que se traduz no código Java:

Tree root = Tree.of(10);
Tree rootFirstChild = root.addChild(2);
Tree depthMostChild = rootFirstChild.addChild(3);
Tree rootSecondChild = root.addChild(4);

Então, se procurarmos o valor 4, esperamos que o algoritmo atravesse nós com os valores 10, 2 e 4, nessa ordem:

BreadthFirstSearchAlgorithm.search(4, root)

Podemos verificar que, ao registrar o valor dos nós visitados:

[main] INFO  c.b.a.b.BreadthFirstSearchAlgorithm - Visited node with value: 10
[main] INFO  c.b.a.b.BreadthFirstSearchAlgorithm - Visited node with value: 2
[main] INFO  c.b.a.b.BreadthFirstSearchAlgorithm - Visited node with value: 4

3.2. Gráficos

Isso conclui o caso das árvores. Agora vamos ver como lidar com gráficos. Contrarily to trees, graphs can contain cycles. Isso significa, como vimos na seção anterior,we have to remember the nodes we visited to avoid an infinite loop. Veremos em um momento como atualizar o algoritmo para considerar este problema, mas primeiro, vamos definir nossa estrutura de gráfico:

public class Node {
    private T value;
    private Set> neighbors;

    public Node(T value) {
        this.value = value;
        this.neighbors = new HashSet<>();
    }

    public void connect(Node node) {
        if (this == node) throw new IllegalArgumentException("Can't connect node to itself");
        this.neighbors.add(node);
        node.neighbors.add(this);
    }
}

Agora, podemos ver que, em oposição às árvores, podemos conectar livremente um nó a outro, dando-nos a possibilidade de criar ciclos. A única exceção é que um nó não pode se conectar a si mesmo.

Também é importante notar que, com esta representação, não há nó raiz. Isso não é um problema, pois também fizemos as conexões entre os nós bidirecionais. Isso significa que seremos capazes de pesquisar o gráfico começando em qualquer nó.

Em primeiro lugar, vamos reutilizar o algoritmo de cima, adaptado à nova estrutura:

public static  Optional> search(T value, Node start) {
    Queue> queue = new ArrayDeque<>();
    queue.add(start);

    Node currentNode;

    while (!queue.isEmpty()) {
        currentNode = queue.remove();

        if (currentNode.getValue().equals(value)) {
            return Optional.of(currentNode);
        } else {
            queue.addAll(currentNode.getNeighbors());
        }
    }

    return Optional.empty();
}

Não podemos executar o algoritmo assim, ou qualquer ciclo o fará funcionar para sempre. Portanto, devemos adicionar instruções para cuidar dos nós já visitados:

while (!queue.isEmpty()) {
    currentNode = queue.remove();
    LOGGER.info("Visited node with value: {}", currentNode.getValue());

    if (currentNode.getValue().equals(value)) {
        return Optional.of(currentNode);
    } else {
        alreadyVisited.add(currentNode);
        queue.addAll(currentNode.getNeighbors());
        queue.removeAll(alreadyVisited);
    }
}

return Optional.empty();

Como podemos ver, primeiro inicializamos umSet que conterá os nós visitados.

Set> alreadyVisited = new HashSet<>();

Então,when the comparison of values fails, we add the node to the visited ones:

alreadyVisited.add(currentNode);

Finalmente,after adding the node’s neighbors to the queue, we remove from it the already visited nodes (que é uma forma alternativa de verificar a presença do nó atual nesse conjunto):

queue.removeAll(alreadyVisited);

Fazendo isso, garantimos que o algoritmo não cairá em um loop infinito.

Vamos ver como funciona por meio de um exemplo. Em primeiro lugar, definiremos um gráfico, com um ciclo:

Graph Example

E o mesmo no código Java:

Node start = new Node<>(10);
Node firstNeighbor = new Node<>(2);
start.connect(firstNeighbor);

Node firstNeighborNeighbor = new Node<>(3);
firstNeighbor.connect(firstNeighborNeighbor);
firstNeighborNeighbor.connect(start);

Node secondNeighbor = new Node<>(4);
start.connect(secondNeighbor);

Digamos novamente que queremos pesquisar o valor 4. Como não há nó raiz, podemos começar a pesquisa com qualquer nó que quisermos e escolheremosfirstNeighborNeighbor:

BreadthFirstSearchAlgorithm.search(4, firstNeighborNeighbor);

Mais uma vez, adicionaremos um registro para ver quais nós são visitados e esperamos que sejam 3, 2, 10 e 4, apenas uma vez cada, nessa ordem:

[main] INFO  c.b.a.b.BreadthFirstSearchAlgorithm - Visited node with value: 3
[main] INFO  c.b.a.b.BreadthFirstSearchAlgorithm - Visited node with value: 2
[main] INFO  c.b.a.b.BreadthFirstSearchAlgorithm - Visited node with value: 10
[main] INFO  c.b.a.b.BreadthFirstSearchAlgorithm - Visited node with value: 4

3.3. Complexidade

Agora que cobrimos os dois algoritmos em Java, vamos falar sobre sua complexidade de tempo. UsaremosBig-O notation para expressá-los.

Vamos começar com o algoritmo da árvore. Ele adiciona um nó à fila no máximo uma vez e, portanto, também o visita uma vez. Thus, if n is the number of nodes in the tree, the time complexity of the algorithm will be O(n).

Agora, para o algoritmo gráfico, as coisas são um pouco mais complicadas. Iremos percorrer cada nó no máximo uma vez, mas para isso faremos uso de operações com complexidade linear, comoaddAll()eremoveAll().

Vamos considerarn o número de nós ec o número de conexões do gráfico. Então, no pior caso (sendo nenhum nó encontrado), podemos usar os métodosaddAll()eremoveAll() para adicionar e remover nós até o número de conexões, nos dandoO(c) complexidade para estes operações. So, provided that c > n, the complexity of the overall algorithm will be O(c). Otherwise, it’ll be O(n). Isso geralmente énoted O(n + c), que pode ser interpretado como uma complexidade dependendo do maior número entrenec.

Por que não tivemos esse problema para a busca por árvore? Como o número de conexões em uma árvore é limitado pelo número de nós. O número de conexões em uma árvore de nósn én – 1.

4. Conclusão

Neste artigo, aprendemos sobre o algoritmo de busca em largura e como implementá-lo em Java.

Após analisar um pouco da teoria, vimos implementações em Java do algoritmo e discutimos sua complexidade.

Como de costume, o código está disponívelover on GitHub.