Skip to content

Commit 4918e94

Browse files
committed
feat(data-structures, linked-lists, remove cycle): utilities to detect and remove cycles
1 parent bdca77c commit 4918e94

File tree

2 files changed

+133
-97
lines changed

2 files changed

+133
-97
lines changed

datastructures/linked_lists/__init__.py

Lines changed: 15 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
from abc import ABCMeta, abstractmethod
33

44
from datastructures.linked_lists.exceptions import EmptyLinkedList
5+
from datastructures.linked_lists.linked_list_utils import (
6+
has_cycle,
7+
detect_node_with_cycle,
8+
cycle_length,
9+
remove_cycle,
10+
)
511

612
T = TypeVar("T")
713

@@ -379,14 +385,7 @@ def has_cycle(self):
379385
:return: True if there is a cycle, False otherwise
380386
:rtype: bool
381387
"""
382-
fast_pointer = slow_pointer = self.head
383-
while fast_pointer and slow_pointer and fast_pointer.next:
384-
fast_pointer = fast_pointer.next.next
385-
slow_pointer = slow_pointer.next
386-
387-
if slow_pointer == fast_pointer:
388-
return True
389-
return False
388+
return has_cycle(self.head)
390389

391390
def cycle_length(self) -> int:
392391
"""
@@ -395,105 +394,25 @@ def cycle_length(self) -> int:
395394
Returns:
396395
int: length of the cycle or number of nodes in the cycle
397396
"""
398-
if not self.head:
399-
return 0
400-
slow_pointer = fast_pointer = self.head
401-
402-
while fast_pointer and fast_pointer.next:
403-
slow_pointer = slow_pointer.next
404-
fast_pointer = fast_pointer.next.next
405-
406-
# Cycle detected
407-
if slow_pointer is fast_pointer:
408-
length = 1
409-
# Move slow pointer by one step to start counting
410-
slow_pointer = slow_pointer.next
411-
412-
# Continue moving the slow pointer until it meets the fast pointer again
413-
while slow_pointer != fast_pointer:
414-
length += 1
415-
slow_pointer = slow_pointer.next
416-
417-
return length
418-
419-
return 0
397+
return cycle_length(self.head)
420398

421399
def detect_node_with_cycle(self) -> Optional[Node]:
422400
"""
423401
Detects the node with a cycle and returns it
424402
"""
425-
if not self.has_cycle():
426-
return None
427-
else:
428-
slow_pointer = fast_pointer = self.head
429-
430-
while fast_pointer and slow_pointer and fast_pointer.next:
431-
fast_pointer = fast_pointer.next.next
432-
slow_pointer = slow_pointer.next
433-
434-
if slow_pointer == fast_pointer:
435-
break
436-
else:
437-
return None
438-
439-
while self.head != slow_pointer:
440-
slow_pointer = slow_pointer.next
441-
self.head = self.head.next
442-
return self.head
403+
return detect_node_with_cycle(self.head)
443404

444-
def remove_cycle(self):
405+
def remove_cycle(self) -> Optional[Node]:
445406
"""
446407
Removes cycle if there exists. This will use the same concept as has_cycle method to check if there is a loop
447408
and remove the cycle
448-
if one is found.
449-
1) Detect Loop using Floyd’s Cycle detection algo and get the pointer to a loop node.
450-
2) Count the number of nodes in loop. Let the count be k.
451-
3) Fix one pointer to the head and another to kth node from head.
452-
4) Move both pointers at the same pace, they will meet at loop starting node.
453-
5) Get pointer to the last node of loop and make next of it as NULL.
454-
:return: True if the cycle has been removed, False otherwise
455-
:rtype: bool
409+
Returns:
410+
Node: head node with cycle removed
456411
"""
457-
fast_pointer = slow_pointer = self.head
458-
459-
while fast_pointer and slow_pointer and fast_pointer.next:
460-
fast_pointer = fast_pointer.next.next
461-
slow_pointer = slow_pointer.next
462-
463-
if slow_pointer == fast_pointer:
464-
pointer_1 = pointer_2 = slow_pointer
465-
466-
# Count the number of nodes in loop
467-
k = 1
468-
while pointer_1.next != pointer_2:
469-
pointer_1 = pointer_1.next
470-
k += 1
471-
472-
# Fix one pointer to head
473-
pointer_1 = self.head
474-
475-
# And the other pointer to k nodes after head
476-
pointer_2 = self.head
477-
for _ in range(k):
478-
pointer_2 = pointer_2.next
479-
480-
# Move both pointers at the same place
481-
# they will meet at loop starting node
482-
while pointer_2 != pointer_1:
483-
pointer_1 = pointer_1.next
484-
pointer_2 = pointer_2.next
485-
486-
# Get pointer to the last node
487-
pointer_2 = pointer_2.next
488-
while pointer_2.next != pointer_1:
489-
pointer_2 = pointer_2.next
490-
491-
# Set the next node of the loop ending node
492-
# to fix the loop
493-
pointer_2.next = None
494-
return True
412+
if not self.head or not self.head.next:
413+
return self.head
495414

496-
return False
415+
return remove_cycle(self.head)
497416

498417
@abstractmethod
499418
def alternate_split(self):

datastructures/linked_lists/linked_list_utils.py

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Optional
1+
from typing import Optional, Callable, Any
22
from datastructures.linked_lists import Node
33

44

@@ -19,3 +19,120 @@ def find_middle_node(head: Optional[Node]) -> Optional[Node]:
1919
fast_pointer = fast_pointer.next.next
2020

2121
return slow_pointer
22+
23+
24+
def has_cycle(
25+
head: Optional[Node], func: Optional[Callable[[Node, Node], Any]] = None
26+
) -> bool:
27+
f"""
28+
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
29+
reached again after traversing the entire linked list
30+
Args:
31+
head(Node): head node of a linked list
32+
func(Callable): An optional callable function that if passed in will be executed
33+
Returns:
34+
bool: True if there is a cycle, False otherwise
35+
"""
36+
if not head or not head.next:
37+
return False
38+
39+
fast_pointer, slow_pointer = head
40+
while fast_pointer and fast_pointer.next:
41+
slow_pointer = slow_pointer.next
42+
fast_pointer = fast_pointer.next.next
43+
44+
if slow_pointer is fast_pointer:
45+
# do something after detecting cycle passing in the two nodes
46+
if func:
47+
func(slow_pointer, fast_pointer)
48+
else:
49+
return True
50+
return False
51+
52+
53+
def cycle_length(head: Optional[Node]) -> int:
54+
"""
55+
Determines the length of the cycle in a linked list if it has one. The length of the cycle is the number
56+
of nodes that are 'caught' in the cycle
57+
Args:
58+
head(Node): head node of linked list
59+
Returns:
60+
int: length of the cycle or number of nodes in the cycle
61+
"""
62+
if not head:
63+
return 0
64+
slow_pointer = fast_pointer = head
65+
66+
while fast_pointer and fast_pointer.next:
67+
slow_pointer = slow_pointer.next
68+
fast_pointer = fast_pointer.next.next
69+
70+
# Cycle detected
71+
if slow_pointer is fast_pointer:
72+
length = 1
73+
# Move slow-pointer by one step to start counting
74+
slow_pointer = slow_pointer.next
75+
76+
# Continue moving the slow pointer until it meets the fast pointer again
77+
while slow_pointer != fast_pointer:
78+
length += 1
79+
slow_pointer = slow_pointer.next
80+
81+
return length
82+
return 0
83+
84+
85+
def detect_node_with_cycle(head: Optional[Node]) -> Optional[Node]:
86+
"""
87+
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
88+
Args:
89+
head(Node): head node of a linked list
90+
Returns:
91+
Node: node with a cycle if the linked list has a cycle
92+
"""
93+
slow_pointer = fast_pointer = head
94+
95+
while fast_pointer and slow_pointer and fast_pointer.next:
96+
fast_pointer = fast_pointer.next.next
97+
slow_pointer = slow_pointer.next
98+
99+
if slow_pointer == fast_pointer:
100+
break
101+
else:
102+
return None
103+
104+
current = head
105+
while current != slow_pointer:
106+
slow_pointer = slow_pointer.next
107+
current = current.next
108+
return current
109+
110+
111+
def remove_cycle(head: Optional[Node]) -> Optional[Node]:
112+
"""
113+
Removes cycle from a linked list given the head node
114+
Args:
115+
head(Node): head node of a linked list
116+
Returns:
117+
Node: head node without the cycle
118+
"""
119+
# This is the entry point of the cycle
120+
node_with_cycle = detect_node_with_cycle(head)
121+
122+
# If there is no node with a cycle, return the head node as is
123+
if not node_with_cycle:
124+
return head
125+
126+
# Now, with the node with the cycle, we set a current pointer that will move a node at a time, until its next pointer
127+
# points back to the fixed pointer
128+
current = node_with_cycle
129+
fixed_pointer = node_with_cycle
130+
131+
# Move the pointer on this current until the next pointer reaches the fixed pointer
132+
while current.next != fixed_pointer:
133+
current = current.next
134+
135+
# Remove the cycle by setting the next to None
136+
current.next = None
137+
138+
return head

0 commit comments

Comments
 (0)