Eventualmente, é bom revisitar alguns temas clássicos de ciências da computação. Nesse post, vamos ver como detectar o nodo de início de um Loop em uma lista encadeada.
Dessa vez, estaremos usando Java. Entretanto, o código em C# seria bastante parecido.
O que é uma lista encadeada?
Pegando a definição da wikipedia:
Uma lista ligada ou lista encadeada é uma estrutura de dados linear e dinâmica. Ela é composta por células que apontam para o próximo elemento da lista. Para “ter” uma lista ligada/encadeada, basta guardar seu primeiro elemento, e seu último elemento aponta para uma célula nula.
Definindo um Nodo
O primeiro passo para definir uma Lista Encadeada é definir um Nodo. Eis uma implementação simples:
package com.elemarjr.LinkedListsXp; public class Node<T> { private T data; private Node<T> next; public Node() {} public Node(T data) { this(); this.data = data; } public T getData() { return data; } public void setData(T value) { data = value; } public Node<T> getNext() { return next; } public void setNext(Node<T> value) { next = value; } }
Aqui, nada de especial. Temos data armazenando o valor correspondente ao nodo e next apontando para o próximo nodo da lista.
Alguns testes:
package com.elemarjr.LinkedListsXp.Tests; import com.elemarjr.LinkedListsXp.*; import static org.junit.Assert.*; import org.junit.Test; public class NodeTests { @Test public void nodeStoresDataPassedWithCtor() { Node<String> node = new Node<>("abc"); assertEquals(node.getData(), "abc"); } @Test public void nodeStoresNext() { Node<String> node1 = new Node<>("abc"); Node<String> node2 = new Node<>("def"); node1.setNext(node2); assertEquals(node1.getNext(), node2); } @Test public void nodeStoresData() { Node<String> node = new Node<>(); node.setData("abc"); assertEquals(node.getData(), "abc"); } }
Implementando a lista
A lista consiste basicamente de uma referência para o primeiro nodo da lista. Interessante, mesmo é a implementação do método que verifica se a lista é circular e do outro que encontra o primeiro elemento.
package com.elemarjr.LinkedListsXp; import java.util.List; public class LinkedList<T> { Node<T> head; public LinkedList(Node<T> head) { this.head = head; } public LinkedList(List<T> elements) { Node<T> previous = null; for (T element: elements) { Node<T> node = new Node<>(element); if (previous != null) { previous.setNext(node); } else { head = node; } previous = node; } } public Node<T> getHead() { return head; } public void setHead(Node<T> value) { head = value; } public boolean isCircular() { Node<T> slow = head; Node<T> fast = head; while (fast != null && fast.getNext() != null) { slow = slow.getNext(); fast = fast.getNext().getNext(); if (slow == fast) return true; } return false; } public Node<T> firstNodeOfCircularLoop() { Node<T> slow = head, fast = head; while (fast != null && fast.getNext() != null) { slow = slow.getNext(); fast = fast.getNext().getNext(); if (slow == fast) break; } if (fast == null || fast.getNext() == null) { return null; } slow = head; while (slow != fast) { slow = slow.getNext(); fast = fast.getNext(); } return slow; } }
Defini dois construtores Um que aceita uma referência para o Nodo “head” e outro que aceita uma lista que será convertida para lista ligada.
O método que verifica se a lista é circular inicia dois cursores. Um que vale a lista avançando um elemento por vez, e outro que avança dois elementos. Se, em algum momento, ambos os cursores apontarem para o mesmo nodo, então, teremos uma lista circular. Perceba que não há chances do cursor rápido simplesmente pular o cursor lento.
Eis os testes:
package com.elemarjr.LinkedListsXp.Tests; import com.elemarjr.LinkedListsXp.*; import java.util.Arrays; import org.junit.Test; import static org.junit.Assert.*; public class LinkedListTests { @Test public void createLinkedListFromList() { Integer[] values = new Integer[] {1, 2, 3, 4, 5}; LinkedList<Integer> list = new LinkedList<>(Arrays.asList(values)); Node<Integer> head = list.getHead(); int index = 0; while (head != null) { assertEquals(head.getData(),values[index]); index++; head = head.getNext(); } } @Test public void isCircularReturnsFalseWhenListIsNotCircular() { Integer[] values = new Integer[] {1, 2, 3, 4, 5}; LinkedList<Integer> list = new LinkedList<>(Arrays.asList(values)); assertFalse(list.isCircular()); } @Test public void isCircularReturnsTrueWhenListIsCircular() { Node<Integer> one = new Node<>(1); Node<Integer> two = new Node<>(2); one.setNext(two); Node<Integer> three = new Node<>(3); two.setNext(three); Node<Integer> four = new Node<>(4); three.setNext(four); Node<Integer> five = new Node<>(5); four.setNext(five); five.setNext(three); LinkedList<Integer> list = new LinkedList<>(one); assertTrue(list.isCircular()); } @Test public void firstNodeOfCircularLoopReturnsCorrectNode() { Node<Integer> one = new Node<>(1); Node<Integer> two = new Node<>(2); one.setNext(two); Node<Integer> three = new Node<>(3); two.setNext(three); Node<Integer> four = new Node<>(4); three.setNext(four); Node<Integer> five = new Node<>(5); four.setNext(five); five.setNext(three); LinkedList<Integer> list = new LinkedList<>(one); assertEquals(three, list.firstNodeOfCircularLoop()); } @Test public void firstNodeOfCircularLoopReturnsNullWhenListIsNotCircular() { Integer[] values = new Integer[] {1, 2, 3, 4, 5}; LinkedList<Integer> list = new LinkedList<>(Arrays.asList(values)); assertEquals(null, list.firstNodeOfCircularLoop()); } }
Pronto!