Quantcast
Channel: Baeldung
Viewing all articles
Browse latest Browse all 3525

Test a Linked List for Cyclicity

$
0
0

1. Introduction

A singly linked list is a sequence of connected nodes ending with a null reference. However, in some scenarios, the last node might point at a previous node – effectively creating a cycle.

In most cases, we want to be able to detect and be aware of these cycles; this article will focus on exactly that – detecting and potentially removing cycles.

2. Detecting a Cycle

Let’s now explore a couple of algorithms for detecting cycles in linked lists.

2.1. Brute Force – O(n^2) Time Complexity

With this algorithm, we traverse the list using two nested loops. In the outer loop, we traverse one-by-one. In the inner loop, we start from the head and traverse as many nodes as traversed by outer loop by that time.

If a node that is visited by the outer loop is visited twice by the inner loop, then a cycle has been detected. Conversely, if the outer loop reaches the end of the list, this implies an absence of cycles:

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

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

        int x = nodesTraversedByOuter;
        Node<T> it2 = head;
        int noOfTimesCurrentNodeVisited = 0;

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

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

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

            x--;
        }
    }

    return false;
}

The advantage of this approach is that it requires a constant amount of memory. The disadvantage is that the performance is very slow when large lists are provided as an input.

2.2. Hashing – O(n) Space Complexity

With this algorithm, we maintain a set of already visited nodes. For each node, we check if it exists in the set. If not, then we add it to the set. The existence of a node in the set means we have already visited the node and brings forward the presence of a cycle in the list.

When we encounter a node which already exists in the set, we’d have discovered the beginning of the cycle. After discovering this, we can easily break the cycle by setting the next field of the previous node to null, as demonstrated below:

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

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

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

    return false;
}

In this solution, we visited and stored each node once. This amounts to O(n) time complexity and O(n) space complexity, which, on average, is not optimal for large lists.

2.3. Fast and Slow Pointers

The following algorithm for finding cycles can best be explained using a metaphor.

Consider a race track where two people are racing. Given that the speed of the second person is double that of the first person, the second person will go around the track twice as fast as the first and will meet the first person again at the beginning of the lap.

Here we use a similar approach by iterating through the list simultaneously with a slow iterator and a fast iterator (2x speed). Once both iterators have entered a loop, they will eventually meet at a point.

Hence, if the two iterators meet at any point, then we can conclude that we have stumbled upon a cycle:

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

    Node<T> slow = head;
    Node<T> 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);
}

Where CycleDetectionResult is a convenience class to hold the result: a boolean variable which says whether cycle exists or not and if exists, then this also contains a reference to the meeting point inside the cycle:

public class CycleDetectionResult<T> {
    boolean cycleExists;
    Node<T> node;
}

This method is also known as the ‘The Tortoise and The Hare Algorithm’ or ‘Flyods Cycle-Finding Algorithm’.

3. Removal of Cycles from a List

Let’s have a look at a few methods for removing cycles. All these methods assume that the ‘Flyods Cycle-Finding Algorithm’ was used for cycle detection and build on top of it.

3.1. Brute Force

Once the fast and the slow iterators meet at a point in the cycle, we take one more iterator (say ptr) and point it to the head of the list. We start iterating the list with ptr. At each step, we check if ptr is reachable from the meeting point.

This terminates when ptr reaches the beginning of the loop because that is the first point when it enters the loop and becomes reachable from the meeting point.

Once the beginning of the loop (bg) is discovered, then it is trivial to find the end of the cycle (node whose next field points to bg). The next pointer of this end node is then set to null to remove the cycle:

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

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

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

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

        return false;
    }

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

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

        loopStart.next = null;
    }
}

Unfortunately, this algorithm also performs poorly in case of large lists and large cycles, because we’ve to traverse the cycle multiple times.

3.2. Optimized Solution – Counting the Loop Nodes

Let’s define a few variables first:

  • n = the size of the list
  • k = the distance from the head of the list to the start of the cycle
  • l = the size of the cycle

We have the following relationship between these variables:
k + l = n

We utilize this relationship in this approach. More particularly, when an iterator that begins from the start of the list, has already traveled l nodes, then it has to travel k more nodes to reach the end of the list.

Here’s the algorithm’s outline:

  1. Once fast and the slow iterators meet, find the length of the cycle. This can be done by keeping one of the iterators in place while continuing the other iterator (iterating at normal speed, one-by-one) till it reaches the first pointer, keeping the count of nodes visited. This counts as l
  2. Take two iterators (ptr1 and ptr2) at the beginning of the list. Move one of the iterator (ptr2) l steps
  3. Now iterate both the iterators until they meet at the start of the loop, subsequently, find the end of the cycle and point it to null

This works because ptr1 is k steps away from the loop, and ptr2, which is advanced by l steps,  also needs k steps to reach the end of the loop (n – l = k).

And here’s a simple, potential implementation:

public class CycleRemovalByCountingLoopNodes {
    private static <T> void removeCycle(
      Node<T> loopNodeParam, Node<T> head) {
        int cycleLength = calculateCycleLength(loopNodeParam);
        Node<T> cycleLengthAdvancedIterator = head;
        Node<T> 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 <T> int calculateCycleLength(
      Node<T> loopNodeParam) {
        Node<T> loopNode = loopNodeParam;
        int length = 1;

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

        return length;
    }
}

Next, let’s focus on a method in which we can even eliminate the step of calculating the loop length.

3.3. Optimized Solution – Without Counting the Loop Nodes

Let’s compare the distances traveled by the fast and slow pointers mathematically.

For that, we need a few more variables:

  • y = distance of the point where the two iterators meet, as seen from the beginning of the cycle
  • z = distance of the point where the two iterators meet, as seen from the end of the cycle (this is also equal to l – y)
  • m = number of times the fast iterator completed the cycle before the slow iterator enters the cycle

Keeping the other variables same as defined in the previous section, the distance equations will be defined as:

  • Distance traveled by slow pointer = k (distance of cycle from head) + y (meeting point inside cycle)
  • Distance traveled by fast pointer = k (distance of cycle from head) + m (no of times fast pointer completed the cycle before slow pointer enters) * l (cycle length) + y (meeting point inside cycle)

We know that distance traveled by the fast pointer is twice that of the slow pointer, hence:

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

which evaluates to:

y = m * l – k

Subtracting both sides from l gives:

l – y = l – m * l + k

or equivalently:

k = (m – 1) * l + z    (where, l – y is z as defined above)

This leads to:

k = (m – 1) Full loop runs + An extra distance z

In other words, if we keep one iterator at the head of the list and one iterator at the meeting point, and move them at the same speed, then, the second iterator will complete m – 1 cycles around the loop and meet the first pointer at the beginning of the cycle. Using this insight we can formulate the algorithm:

  1. Use ‘Flyods Cycle-Finding Algorithm’ to detect the loop. If loop exists, this algorithm would end at a point inside the loop (call this the meeting point)
  2. Take two iterators, one at the head of the list (it1) and one at the meeting point (it2)
  3. Traverse both iterators at the same speed
  4. Since the distance of the loop from head is k (as defined above), the iterator started from head would reach the cycle after k steps
  5. In k steps, iterator it2 would traverse m – 1 cycles of the loop and an extra distance z. Since this pointer was already at a distance of z from the beginning of the cycle, traversing this extra distance z, would bring it also at the beginning of the cycle
  6. Both the iterators meet at the beginning of the cycle, subsequently, we can find the end of the cycle and point it to null

This can be implemented:

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

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

        loopNode.next = null;
    }
}

This is the most optimized approach for detection and removal of cycles from a linked list.

4. Conclusion

In this article, we described various algorithms for detecting a cycle in a list. We looked into algorithms with different computing time and memory space requirements.

Finally, we also showed three methods to remove a cycle, once it is detected using the ‘Flyods Cycle-Finding Algorithm’.

The full code example is available over on Github.


Viewing all articles
Browse latest Browse all 3525

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>