diff --git a/datastructures/linked_lists/__init__.py b/datastructures/linked_lists/__init__.py index 268fa806..8313fe07 100755 --- a/datastructures/linked_lists/__init__.py +++ b/datastructures/linked_lists/__init__.py @@ -2,6 +2,12 @@ from abc import ABCMeta, abstractmethod from datastructures.linked_lists.exceptions import EmptyLinkedList +from datastructures.linked_lists.linked_list_utils import ( + has_cycle, + detect_node_with_cycle, + cycle_length, + remove_cycle, +) T = TypeVar("T") @@ -379,14 +385,7 @@ def has_cycle(self): :return: True if there is a cycle, False otherwise :rtype: bool """ - fast_pointer = slow_pointer = self.head - while fast_pointer and slow_pointer and fast_pointer.next: - fast_pointer = fast_pointer.next.next - slow_pointer = slow_pointer.next - - if slow_pointer == fast_pointer: - return True - return False + return has_cycle(self.head) def cycle_length(self) -> int: """ @@ -395,105 +394,25 @@ def cycle_length(self) -> int: Returns: int: length of the cycle or number of nodes in the cycle """ - if not self.head: - return 0 - slow_pointer = fast_pointer = self.head - - while fast_pointer and fast_pointer.next: - slow_pointer = slow_pointer.next - fast_pointer = fast_pointer.next.next - - # Cycle detected - if slow_pointer is fast_pointer: - length = 1 - # Move slow pointer by one step to start counting - slow_pointer = slow_pointer.next - - # Continue moving the slow pointer until it meets the fast pointer again - while slow_pointer != fast_pointer: - length += 1 - slow_pointer = slow_pointer.next - - return length - - return 0 + return cycle_length(self.head) def detect_node_with_cycle(self) -> Optional[Node]: """ Detects the node with a cycle and returns it """ - if not self.has_cycle(): - return None - else: - slow_pointer = fast_pointer = self.head - - while fast_pointer and slow_pointer and fast_pointer.next: - fast_pointer = fast_pointer.next.next - slow_pointer = slow_pointer.next - - if slow_pointer == fast_pointer: - break - else: - return None - - while self.head != slow_pointer: - slow_pointer = slow_pointer.next - self.head = self.head.next - return self.head + return detect_node_with_cycle(self.head) - def remove_cycle(self): + def remove_cycle(self) -> Optional[Node]: """ Removes cycle if there exists. This will use the same concept as has_cycle method to check if there is a loop and remove the cycle - if one is found. - 1) Detect Loop using Floyd’s Cycle detection algo and get the pointer to a loop node. - 2) Count the number of nodes in loop. Let the count be k. - 3) Fix one pointer to the head and another to kth node from head. - 4) Move both pointers at the same pace, they will meet at loop starting node. - 5) Get pointer to the last node of loop and make next of it as NULL. - :return: True if the cycle has been removed, False otherwise - :rtype: bool + Returns: + Node: head node with cycle removed """ - fast_pointer = slow_pointer = self.head - - while fast_pointer and slow_pointer and fast_pointer.next: - fast_pointer = fast_pointer.next.next - slow_pointer = slow_pointer.next - - if slow_pointer == fast_pointer: - pointer_1 = pointer_2 = slow_pointer - - # Count the number of nodes in loop - k = 1 - while pointer_1.next != pointer_2: - pointer_1 = pointer_1.next - k += 1 - - # Fix one pointer to head - pointer_1 = self.head - - # And the other pointer to k nodes after head - pointer_2 = self.head - for _ in range(k): - pointer_2 = pointer_2.next - - # Move both pointers at the same place - # they will meet at loop starting node - while pointer_2 != pointer_1: - pointer_1 = pointer_1.next - pointer_2 = pointer_2.next - - # Get pointer to the last node - pointer_2 = pointer_2.next - while pointer_2.next != pointer_1: - pointer_2 = pointer_2.next - - # Set the next node of the loop ending node - # to fix the loop - pointer_2.next = None - return True + if not self.head or not self.head.next: + return self.head - return False + return remove_cycle(self.head) @abstractmethod def alternate_split(self): diff --git a/datastructures/linked_lists/linked_list_utils.py b/datastructures/linked_lists/linked_list_utils.py index 015f336e..6fdb0218 100644 --- a/datastructures/linked_lists/linked_list_utils.py +++ b/datastructures/linked_lists/linked_list_utils.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Optional, Callable, Any from datastructures.linked_lists import Node @@ -19,3 +19,157 @@ def find_middle_node(head: Optional[Node]) -> Optional[Node]: fast_pointer = fast_pointer.next.next return slow_pointer + + +def has_cycle( + head: Optional[Node], func: Optional[Callable[[Node, Node], Any]] = None +) -> bool: + f""" + Checks if a given linked list has a cycle if a head node is provided. A cycle is when a linked list node can be + reached again after traversing the entire linked list + Args: + head(Node): head node of a linked list + func(Callable): An optional callable function that if passed in will be executed + Returns: + bool: True if there is a cycle, False otherwise + """ + if not head or not head.next: + return False + + fast_pointer, slow_pointer = head, head + while fast_pointer and fast_pointer.next: + slow_pointer = slow_pointer.next + fast_pointer = fast_pointer.next.next + + if slow_pointer is fast_pointer: + # do something after detecting cycle passing in the two nodes + if func: + func(slow_pointer, fast_pointer) + return True + return False + + +def cycle_length(head: Optional[Node]) -> int: + """ + Determines the length of the cycle in a linked list if it has one. The length of the cycle is the number + of nodes that are 'caught' in the cycle + Args: + head(Node): head node of linked list + Returns: + int: length of the cycle or number of nodes in the cycle + """ + if not head: + return 0 + slow_pointer = fast_pointer = head + + while fast_pointer and fast_pointer.next: + slow_pointer = slow_pointer.next + fast_pointer = fast_pointer.next.next + + # Cycle detected + if slow_pointer is fast_pointer: + length = 1 + # Move slow-pointer by one step to start counting + slow_pointer = slow_pointer.next + + # Continue moving the slow pointer until it meets the fast pointer again + while slow_pointer is not fast_pointer: + length += 1 + slow_pointer = slow_pointer.next + + return length + return 0 + + +def detect_node_with_cycle(head: Optional[Node]) -> Optional[Node]: + """ + Detects a node with a cycle in a Linked List and returns it. The node with a cycle is the entry point of the loop + Args: + head(Node): head node of a linked list + Returns: + Node: node with a cycle if the linked list has a cycle + """ + slow_pointer = fast_pointer = head + + while fast_pointer and slow_pointer and fast_pointer.next: + fast_pointer = fast_pointer.next.next + slow_pointer = slow_pointer.next + + if slow_pointer == fast_pointer: + break + else: + return None + + current = head + while current is not slow_pointer: + slow_pointer = slow_pointer.next + current = current.next + return current + + +def remove_cycle(head: Optional[Node]) -> Optional[Node]: + """ + Removes cycle from a linked list given the head node + Args: + head(Node): head node of a linked list + Returns: + Node: head node without the cycle + """ + # This is the entry point of the cycle + node_with_cycle = detect_node_with_cycle(head) + + # If there is no node with a cycle, return the head node as is + if not node_with_cycle: + return head + + # Now, with the node with the cycle, we set a current pointer that will move a node at a time, until its next pointer + # points back to the fixed pointer + current = node_with_cycle + fixed_pointer = node_with_cycle + + # Move the pointer on this current until the next pointer reaches the fixed pointer + while current.next is not fixed_pointer: + current = current.next + + # Remove the cycle by setting the next to None + current.next = None + + return head + +def remove_nth_from_end(head: Optional[Node], n: int) -> Optional[Node]: + """ + Removes the nth node from a linked list from the head given the head of the linked list and the position from the + end of the linked list to remove. + Args: + head(Node): head node of linkedlist + n(int): the position of the last node from the tail in the linked list to remove + Returns: + Node: head node of modified linked list + """ + if not head: + return head + + # Initialize two pointers, both starting at the head node + fast = slow = head + + # Move the fast pointer until it reaches position n in the linked list + for _ in range(n): + fast = fast.next + + # If there is no node at this pointers position, then we have reached the end of the linked list and we return the + # next node from the head. This means, we are removing the head node + if not fast: + return head.next + + # Move the fast pointer, until it reaches the end of the linked list and until the slow pointer reaches n nodes from + # the end of the linked list + while fast.next: + fast = fast.next + slow = slow.next + + # Set the next pointer of the node at the slow pointer's position to the next node's next pointer, removing the node in the middle + slow.next = slow.next.next + + # Return the modified head node of the linked list with the node removed + return head +