Teste uma lista vinculada de ciclicidade

Teste uma lista vinculada de ciclicidade

1. Introdução

Uma lista vinculada simples é uma sequência de nós conectados terminando com uma referêncianull. No entanto, em alguns cenários, o último nó pode apontar para um nó anterior - criando efetivamente um ciclo.

Na maioria dos casos, queremos ser capazes de detectar e estar ciente desses ciclos; este artigo se concentrará exatamente nisso - detecção e remoção potencial de ciclos.

2. Detectando um Ciclo

Vamos agora explorar alguns algoritmos para detectar ciclos em listas vinculadas.

2.1. Força bruta - O (n ^ 2) Complexidade de tempo

Com esse algoritmo, percorremos a lista usando dois loops aninhados. No loop externo, percorremos um por um. No loop interno, começamos a partir da cabeça e percorremos o número de nós atravessados ​​pelo loop externo naquele momento.

If a node that is visited by the outer loop is visited twice by the inner loop, then a cycle has been detected. Por outro lado, se o loop externo atingir o final da lista, isso implica na ausência de ciclos:

public static  boolean detectCycle(Node head) {
    if (head == null) {
        return false;
    }

    Node it1 = head;
    int nodesTraversedByOuter = 0;
    while (it1 != null && it1.next != null) {
        it1 = it1.next;
        nodesTraversedByOuter++;

        int x = nodesTraversedByOuter;
        Node it2 = head;
        int noOfTimesCurrentNodeVisited = 0;

        while (x > 0) {
            it2 = it2.next;

            if (it2 == it1) {
                noOfTimesCurrentNodeVisited++;
            }

            if (noOfTimesCurrentNodeVisited == 2) {
                return true;
            }

            x--;
        }
    }

    return false;
}

A vantagem dessa abordagem é que ela requer uma quantidade constante de memória. A desvantagem é que o desempenho é muito lento quando grandes listas são fornecidas como uma entrada.

2.2. Hashing - O (n) Complexidade do Espaço

Com esse algoritmo, mantemos um conjunto de nós já visitados. Para cada nó, verificamos se ele existe no conjunto. Caso contrário, nós o adicionamos ao conjunto. A existência de um nó no conjunto significa que já visitamos o nó e antecipa a presença de um ciclo na lista.

Quando encontramos um nó que já existe no conjunto, teríamos descoberto o início do ciclo. Depois de descobrir isso, podemos facilmente quebrar o ciclo, definindo o camponext do nó anterior paranull, conforme demonstrado abaixo:

public static  boolean detectCycle(Node head) {
    if (head == null) {
        return false;
    }

    Set> set = new HashSet<>();
    Node node = head;

    while (node != null) {
        if (set.contains(node)) {
            return true;
        }
        set.add(node);
        node = node.next;
    }

    return false;
}

Nesta solução, visitamos e armazenamos cada nó uma vez. Isso equivale a O (n) complexidade de tempo e O (n) complexidade de espaço, o que, em média, não é ideal para grandes listas.

2.3. Indicadores rápidos e lentos

O seguinte algoritmo para encontrar ciclos pode ser melhor explicadousing a metaphor.

Considere uma pista de corrida onde duas pessoas estão correndo. Dado que a velocidade da segunda pessoa é o dobro da velocidade da primeira pessoa, a segunda pessoa percorrerá a pista duas vezes mais rápido que a primeira e encontrará a primeira pessoa novamente no início da volta.

Aqui, usamos uma abordagem semelhante iterando a lista simultaneamente com um iterador lento e um iterador rápido (velocidade 2x). Depois que os dois iteradores entram em um loop, eles finalmente se encontram em um ponto.

Portanto, se os dois iteradores se encontrarem a qualquer momento, podemos concluir que tropeçamos em um ciclo:

public static  CycleDetectionResult detectCycle(Node head) {
    if (head == null) {
        return new CycleDetectionResult<>(false, null);
    }

    Node slow = head;
    Node fast = head;

    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;

        if (slow == fast) {
            return new CycleDetectionResult<>(true, fast);
        }
    }

    return new CycleDetectionResult<>(false, null);
}

OndeCycleDetectionResult é uma classe de conveniência para armazenar o resultado: uma variávelboolean que diz se o ciclo existe ou não e se existe, então também contém uma referência ao ponto de encontro dentro do ciclo:

public class CycleDetectionResult {
    boolean cycleExists;
    Node node;
}

Esse método também é conhecido como o 'Algoritmo da tartaruga e do lebre' ou 'Algoritmo de detecção de ciclo de Flyods'.

3. Remoção de ciclos de uma lista

Vamos dar uma olhada em alguns métodos para remover ciclos. All these methods assume that the ‘Flyods Cycle-Finding Algorithm' was used for cycle detection and build on top of it.

3.1. Força Bruta

Uma vez que os iteradores rápido e lento se encontram em um ponto do ciclo, pegamos mais um iterador (digamosptr) e o apontamos para o topo da lista. Começamos a iterar a lista com ptr. Em cada etapa, verificamos septr pode ser acessado do ponto de encontro.

Isso termina quandoptr atinge o início do loop porque esse é o primeiro ponto quando ele entra no loop e se torna acessível a partir do ponto de encontro.

Uma vez que o início do loop (bg) é descoberto, então é trivial encontrar o final do ciclo (nó cujo próximo campo aponta parabg). O próximo ponteiro deste nó final é então definido comonull para remover o ciclo:

public class CycleRemovalBruteForce {
    private static  void removeCycle(
      Node loopNodeParam, Node head) {
        Node it = head;

        while (it != null) {
            if (isNodeReachableFromLoopNode(it, loopNodeParam)) {
                Node loopStart = it;
                findEndNodeAndBreakCycle(loopStart);
                break;
            }
            it = it.next;
        }
    }

    private static  boolean isNodeReachableFromLoopNode(
      Node it, Node loopNodeParam) {
        Node loopNode = loopNodeParam;

        do {
            if (it == loopNode) {
                return true;
            }
            loopNode = loopNode.next;
        } while (loopNode.next != loopNodeParam);

        return false;
    }

    private static  void findEndNodeAndBreakCycle(
      Node loopStartParam) {
        Node loopStart = loopStartParam;

        while (loopStart.next != loopStartParam) {
            loopStart = loopStart.next;
        }

        loopStart.next = null;
    }
}

Infelizmente, esse algoritmo também funciona mal no caso de listas grandes e ciclos grandes, porque temos que percorrer o ciclo várias vezes.

3.2. Solução Otimizada - Contando os Nós de Loop

Vamos definir algumas variáveis ​​primeiro:

  • n = o tamanho da lista

  • k = a distância do topo da lista ao início do ciclo

  • l = o tamanho do ciclo

Temos a seguinte relação entre essas variáveis:k + l = n

Utilizamos esse relacionamento nessa abordagem. Mais particularmente, quando um iterador que começa do início da lista já percorreul nós, então ele tem que viajark mais nós para chegar ao final da lista.

Aqui está o esboço do algoritmo:

  1. Uma vez rápidos e os iteradores lentos se encontrarem, encontre a duração do ciclo. Isso pode ser feito mantendo um dos iteradores no lugar enquanto continua o outro (iterando na velocidade normal, um por um) até atingir o primeiro ponteiro, mantendo a contagem de nós visitados. Isso conta comol

  2. Pegue dois iteradores (ptr1 eptr2) no início da lista. Mova um dos passos do iterador (ptr2)l

  3. Agora itere ambos os iteradores até que eles se encontrem no início do loop, subsequentemente, encontre o final do ciclo e aponte paranull

Isso funciona porqueptr1 esták passos de distância do loop, eptr2, que é avançado porl passos, também precisa dek passos para chegar ao final de o loop (n – l = k).

E aqui está uma implementação simples e potencial:

public class CycleRemovalByCountingLoopNodes {
    private static  void removeCycle(
      Node loopNodeParam, Node head) {
        int cycleLength = calculateCycleLength(loopNodeParam);
        Node cycleLengthAdvancedIterator = head;
        Node it = head;

        for (int i = 0; i < cycleLength; i++) {
            cycleLengthAdvancedIterator
              = cycleLengthAdvancedIterator.next;
        }

        while (it.next != cycleLengthAdvancedIterator.next) {
            it = it.next;
            cycleLengthAdvancedIterator
              = cycleLengthAdvancedIterator.next;
        }

        cycleLengthAdvancedIterator.next = null;
    }

    private static  int calculateCycleLength(
      Node loopNodeParam) {
        Node loopNode = loopNodeParam;
        int length = 1;

        while (loopNode.next != loopNodeParam) {
            length++;
            loopNode = loopNode.next;
        }

        return length;
    }
}

A seguir, vamos nos concentrar em um método em que podemos até mesmo eliminar a etapa de cálculo do comprimento do loop.

3.3. Solução otimizada - sem contar os nós de loop

Vamos comparar as distâncias percorridas pelos ponteiros rápidos e lentos matematicamente.

Para isso, precisamos de mais algumas variáveis:

  • y = distância do ponto onde os dois iteradores se encontram, como visto desde o início do ciclo

  • z = distância do ponto onde os dois iteradores se encontram, como visto a partir do final do ciclo (isso também é igual al – y)

  • m = número de vezes que o iterador rápido completou o ciclo antes que o iterador lento entre no ciclo

Mantendo as outras variáveis ​​iguais às definidas na seção anterior, as equações de distância serão definidas como:

  • Distância percorrida pelo ponteiro lento =k (distância do ciclo da cabeça) +y (ponto de encontro dentro do ciclo)

  • Distância percorrida pelo ponteiro rápido =k (distância do ciclo da cabeça) +m (número de vezes que o ponteiro rápido completou o ciclo antes que o ponteiro lento entre) *l (duração do ciclo) +y (ponto de encontro dentro do ciclo)

Sabemos que a distância percorrida pelo ponteiro rápido é o dobro da distância do ponteiro lento, portanto:

k + m * l + y = 2 * (k + y)

que avalia como:

y = m * l - k

Subtraindo ambos os lados del dá:

l - y = l - m * l + k

ou equivalente:

k = (m - 1) * l + z (onde, l - y é z conforme definido acima)

Isto leva a:

k = (m - 1) Loop completo corre + Uma distância extra z

Em outras palavras, se mantivermos um iterador no topo da lista e um iterador no ponto de encontro, e movê-los na mesma velocidade, então, o segundo iterador completarám – 1 ciclos em torno do loop e encontrará o primeiro ponteiro no início do ciclo. Usando esse insight, podemos formular o algoritmo:

  1. Use o 'Algoritmo de detecção de ciclo Flyods' para detectar o loop. Se o loop existir, esse algoritmo terminará em um ponto dentro do loop (chame isso de ponto de encontro)

  2. Pegue dois iteradores, um no topo da lista (it1) e um no ponto de encontro (it2)

  3. Atravesse os dois iteradores na mesma velocidade

  4. Uma vez que a distância do loop da cabeça é k (conforme definido acima), o iterador iniciado da cabeça alcançaria o ciclo apósk etapas

  5. Nas etapas dek, o iteradorit2 percorreriam – 1 ciclos do loop e uma distância extraz. Uma vez que este ponteiro já estava a uma distância dez do início do ciclo, percorrendo esta distância extraz, traria também no início do ciclo

  6. Ambos os iteradores se encontram no início do ciclo, posteriormente, podemos encontrar o final do ciclo e apontá-lo paranull

Isso pode ser implementado:

public class CycleRemovalWithoutCountingLoopNodes {
    private static  void removeCycle(
      Node meetingPointParam, Node head) {
        Node loopNode = meetingPointParam;
        Node it = head;

        while (loopNode.next != it.next) {
            it = it.next;
            loopNode = loopNode.next;
        }

        loopNode.next = null;
    }
}

Essa é a abordagem mais otimizada para detecção e remoção de ciclos de uma lista vinculada.

4. Conclusão

Neste artigo, descrevemos vários algoritmos para detectar um ciclo em uma lista. Analisamos algoritmos com diferentes requisitos de tempo de computação e espaço de memória.

Finalmente, também mostramos três métodos para remover um ciclo, uma vez que ele é detectado usando o 'Algoritmo de detecção de ciclo Flyods'.

O exemplo de código completo está disponívelover on Github.